/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.common.impl.site;
import ch.entwine.weblounge.common.content.image.ImageStyle;
import ch.entwine.weblounge.common.content.page.PageLayout;
import ch.entwine.weblounge.common.content.page.PageTemplate;
import ch.entwine.weblounge.common.impl.content.page.PageTemplateImpl;
import ch.entwine.weblounge.common.impl.language.LanguageUtils;
import ch.entwine.weblounge.common.impl.scheduler.QuartzJob;
import ch.entwine.weblounge.common.impl.scheduler.QuartzJobTrigger;
import ch.entwine.weblounge.common.impl.scheduler.QuartzJobWorker;
import ch.entwine.weblounge.common.impl.scheduler.QuartzTriggerListener;
import ch.entwine.weblounge.common.impl.security.SiteAdminImpl;
import ch.entwine.weblounge.common.impl.testing.IntegrationTestBase;
import ch.entwine.weblounge.common.impl.testing.IntegrationTestGroup;
import ch.entwine.weblounge.common.impl.testing.IntegrationTestParser;
import ch.entwine.weblounge.common.impl.util.classloader.BundleClassLoader;
import ch.entwine.weblounge.common.impl.util.config.ConfigurationUtils;
import ch.entwine.weblounge.common.impl.util.config.OptionsHelper;
import ch.entwine.weblounge.common.impl.util.xml.ValidationErrorHandler;
import ch.entwine.weblounge.common.impl.util.xml.XPathHelper;
import ch.entwine.weblounge.common.impl.util.xml.XPathNamespaceContext;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.language.UnknownLanguageException;
import ch.entwine.weblounge.common.repository.ContentRepository;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.request.RequestListener;
import ch.entwine.weblounge.common.request.WebloungeRequest;
import ch.entwine.weblounge.common.request.WebloungeResponse;
import ch.entwine.weblounge.common.scheduler.Job;
import ch.entwine.weblounge.common.scheduler.JobTrigger;
import ch.entwine.weblounge.common.scheduler.JobWorker;
import ch.entwine.weblounge.common.security.DigestType;
import ch.entwine.weblounge.common.security.Security;
import ch.entwine.weblounge.common.security.User;
import ch.entwine.weblounge.common.security.UserListener;
import ch.entwine.weblounge.common.security.WebloungeUser;
import ch.entwine.weblounge.common.site.Action;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.I18nDictionary;
import ch.entwine.weblounge.common.site.Module;
import ch.entwine.weblounge.common.site.ModuleException;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.site.SiteException;
import ch.entwine.weblounge.common.site.SiteListener;
import ch.entwine.weblounge.common.site.SiteURL;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.testing.IntegrationTest;
import org.apache.commons.lang.StringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
/**
* Default implementation of a site.
*/
public class SiteImpl implements Site {
/** Serial version uid */
private static final long serialVersionUID = 5544198303137698222L;
/** Logging facility */
static final Logger logger = LoggerFactory.getLogger(SiteImpl.class);
/** Bundle property name of the site identifier */
public static final String PROP_IDENTIFIER = "site.identifier";
/** Xml namespace for the site */
public static final String SITE_XMLNS = "http://www.entwinemedia.com/weblounge/3.2/site";
/** Regular expression to test the validity of a site identifier */
private static final String SITE_IDENTIFIER_REGEX = "^[a-zA-Z0-9]+[a-zA-Z0-9-_.]*$";
/** The local roles */
protected Map<String, String> localRoles = null;
/** The site identifier */
protected String identifier = null;
/** Site enabled state */
protected boolean autoStart = true;
/** Site running state */
private boolean isOnline = false;
/** Site description */
protected String name = null;
/** Site administrator */
protected WebloungeUser administrator = null;
/** Page languages */
protected Map<String, Language> languages = null;
/** The default language */
protected Language defaultLanguage = null;
/** Page templates */
protected Map<String, PageTemplate> templates = null;
/** The default page template */
protected PageTemplate defaultTemplate = null;
/** Page layouts */
protected Map<String, PageLayout> layouts = null;
/** The default page template */
protected PageLayout defaultLayout = null;
/** Modules */
protected Map<String, Module> modules = null;
/** The default hostname */
protected SiteURL defaultURL = null;
/** Ordered list of site urls */
protected List<SiteURL> urls = null;
/** Default urls by environment */
protected Map<Environment, SiteURL> defaultURLByEnvironment = null;
/** Jobs */
protected Map<String, QuartzJob> jobs = null;
/** The i18n dictionary */
protected I18nDictionaryImpl i18n = null;
/** The site's content repository */
protected ContentRepository contentRepository = null;
/** Option handling support */
protected OptionsHelper options = null;
/** URL to the security configuration */
protected URL security = null;
/** This site's digest policy */
protected DigestType digestType = DigestType.md5;
/** Request listeners */
private List<RequestListener> requestListeners = null;
/** Site listeners */
private List<SiteListener> siteListeners = null;
/** User listeners */
private List<UserListener> userListeners = null;
/** Scheduling service tracker */
private SchedulingServiceTracker schedulingServiceTracker = null;
/** Quartz scheduler */
private Scheduler scheduler = null;
/** Listener for the quartz scheduler */
private TriggerListener quartzTriggerListener = null;
/** Flag to tell whether we are currently shutting down */
private boolean isShutdownInProgress = false;
/** The site's bundle context */
protected BundleContext bundleContext = null;
/** The current system environment */
protected Environment environment = Environment.Production;
/** The list of integration tests */
private List<IntegrationTest> integrationTests = null;
/** The registered integration tests */
private final List<ServiceRegistration> integrationTestRegistrations = new ArrayList<ServiceRegistration>();
/** Flag to indicate whether the site has been initialized */
private boolean siteInitialized = false;
/** Site bundle initialization properties */
protected Map<String, String> serviceProperties = null;
/**
* Creates a new site that is initially disabled. Use {@link #setEnabled()} to
* enable the site.
*/
public SiteImpl() {
languages = new HashMap<String, Language>();
templates = new HashMap<String, PageTemplate>();
layouts = new HashMap<String, PageLayout>();
modules = new HashMap<String, Module>();
urls = new ArrayList<SiteURL>();
defaultURLByEnvironment = new HashMap<Environment, SiteURL>();
jobs = new HashMap<String, QuartzJob>();
localRoles = new HashMap<String, String>();
i18n = new I18nDictionaryImpl();
options = new OptionsHelper();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#initialize(ch.entwine.weblounge.common.site.Environment)
*/
public void initialize(Environment environment) throws Exception {
this.environment = environment;
// Don't inialize twice
if (!siteInitialized) {
initializeSiteComponents();
}
// Pass the initialization on to the templates
for (PageTemplate template : templates.values()) {
template.setEnvironment(environment);
}
// Initialize modules as well
for (Module module : modules.values()) {
module.initialize(environment);
}
// Switch the options to the new environment
options.setEnvironment(environment);
}
/**
* Initializes the site components like modules, templates, actions etc.
*
* @throws Exception
* if initialization fails
*/
private void initializeSiteComponents() throws Exception {
logger.debug("Initializing site '{}'", this);
final Bundle bundle = bundleContext.getBundle();
// Load i18n dictionary
Enumeration<URL> i18nEnum = bundle.findEntries("site/i18n", "*.xml", true);
while (i18nEnum != null && i18nEnum.hasMoreElements()) {
i18n.addDictionary(i18nEnum.nextElement());
}
// Prepare schema validator
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
URL schemaUrl = SiteImpl.class.getResource("/xsd/module.xsd");
Schema moduleSchema = schemaFactory.newSchema(schemaUrl);
// Set up the document builder
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setSchema(moduleSchema);
docBuilderFactory.setNamespaceAware(true);
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
// Load the modules
final Enumeration<URL> e = bundle.findEntries("site", "module.xml", true);
if (e != null) {
while (e.hasMoreElements()) {
URL moduleXmlUrl = e.nextElement();
int endIndex = moduleXmlUrl.toExternalForm().lastIndexOf('/');
URL moduleUrl = new URL(moduleXmlUrl.toExternalForm().substring(0, endIndex));
logger.debug("Loading module '{}' for site '{}'", moduleXmlUrl, this);
// Load and validate the module descriptor
ValidationErrorHandler errorHandler = new ValidationErrorHandler(moduleXmlUrl);
docBuilder.setErrorHandler(errorHandler);
Document moduleXml = docBuilder.parse(moduleXmlUrl.openStream());
if (errorHandler.hasErrors()) {
logger.error("Errors found while validating module descriptor {}. Site '{}' is not loaded", moduleXml, this);
throw new IllegalStateException("Errors found while validating module descriptor " + moduleXml);
}
// We need the module id even if the module initialization fails to log
// a proper error message
Node moduleNode = moduleXml.getFirstChild();
String moduleId = moduleNode.getAttributes().getNamedItem("id").getNodeValue();
Module module;
try {
module = ModuleImpl.fromXml(moduleNode);
logger.debug("Module '{}' loaded for site '{}'", module, this);
} catch (Throwable t) {
logger.error("Error loading module '{}' of site {}", moduleId, identifier);
if (t instanceof Exception)
throw (Exception) t;
throw new Exception(t);
}
// If module is disabled, don't add it to the site
if (!module.isEnabled()) {
logger.info("Found disabled module '{}' in site '{}'", module, this);
continue;
}
// Make sure there is only one module with this identifier
if (modules.containsKey(module.getIdentifier())) {
logger.warn("A module with id '{}' is already registered in site '{}'", module.getIdentifier(), identifier);
logger.error("Module '{}' is not registered due to conflicting identifier", module.getIdentifier());
continue;
}
// Check inter-module compatibility
for (Module m : modules.values()) {
// Check actions
for (Action a : m.getActions()) {
for (Action action : module.getActions()) {
if (action.getIdentifier().equals(a.getIdentifier())) {
logger.warn("Module '{}' of site '{}' already defines an action with id '{}'", new String[] {
m.getIdentifier(),
identifier,
a.getIdentifier() });
} else if (action.getPath().equals(a.getPath())) {
logger.warn("Module '{}' of site '{}' already defines an action at '{}'", new String[] {
m.getIdentifier(),
identifier,
a.getPath() });
logger.error("Module '{}' of site '{}' is not registered due to conflicting mountpoints", m.getIdentifier(), identifier);
continue;
}
}
}
// Check image styles
for (ImageStyle s : m.getImageStyles()) {
for (ImageStyle style : module.getImageStyles()) {
if (style.getIdentifier().equals(s.getIdentifier())) {
logger.warn("Module '{}' of site '{}' already defines an image style with id '{}'", new String[] {
m.getIdentifier(),
identifier,
s.getIdentifier() });
}
}
}
// Check jobs
for (Job j : m.getJobs()) {
for (Job job : module.getJobs()) {
if (job.getIdentifier().equals(j.getIdentifier())) {
logger.warn("Module '{}' of site '{}' already defines a job with id '{}'", new String[] {
m.getIdentifier(),
identifier,
j.getIdentifier() });
}
}
}
}
addModule(module);
// Do this as last step since we don't want to have i18n dictionaries of
// an invalid or disabled module in the site
String i18nPath = UrlUtils.concat(moduleUrl.getPath(), "i18n");
i18nEnum = bundle.findEntries(i18nPath, "*.xml", true);
while (i18nEnum != null && i18nEnum.hasMoreElements()) {
i18n.addDictionary(i18nEnum.nextElement());
}
}
} else {
logger.debug("Site '{}' has no modules", this);
}
// Look for a job scheduler
logger.debug("Signing up for a job scheduling services");
schedulingServiceTracker = new SchedulingServiceTracker(bundleContext, this);
schedulingServiceTracker.open();
// Load the tests
if (!Environment.Production.equals(environment))
integrationTests = loadIntegrationTests();
else
logger.info("Skipped loading of integration tests due to environment '{}'", environment);
siteInitialized = true;
logger.info("Site '{}' initialized", this);
}
/**
* Returns the site's OSGi bundle context.
*
* @return the bundle context
*/
public BundleContext getBundleContext() {
return bundleContext;
}
/**
* Returns the properties of the site's OSGi service.
*
* @return the site properties
*/
public Map<String, String> getServiceProperties() {
return serviceProperties;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setIdentifier(java.lang.String)
*/
public void setIdentifier(String identifier) {
if (identifier == null)
throw new IllegalArgumentException("Site identifier must not be null");
else if (!Pattern.matches(SITE_IDENTIFIER_REGEX, identifier))
throw new IllegalArgumentException("Site identifier '" + identifier + "' is malformed");
this.identifier = identifier;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getIdentifier()
*/
public String getIdentifier() {
return identifier;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setAutoStart(boolean)
*/
public void setAutoStart(boolean enabled) {
this.autoStart = enabled;
if (isOnline)
stop();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#isStartedAutomatically()
*/
public boolean isStartedAutomatically() {
return autoStart;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setName(java.lang.String)
*/
public void setName(String description) {
this.name = description;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getName()
*/
public String getName() {
return name;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setAdministrator(ch.entwine.weblounge.common.security.WebloungeUser)
*/
public void setAdministrator(WebloungeUser administrator) {
if (administrator != null)
logger.debug("Site administrator is {}", administrator);
else
logger.debug("Site administrator is now undefined");
this.administrator = administrator;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getAdministrator()
*/
public WebloungeUser getAdministrator() {
return administrator;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getI18n()
*/
public I18nDictionary getI18n() {
return i18n;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addTemplate(ch.entwine.weblounge.common.content.page.PageTemplate)
*/
public void addTemplate(PageTemplate template) {
template.setSite(this);
templates.put(template.getIdentifier(), template);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#removeTemplate(ch.entwine.weblounge.common.content.page.PageTemplate)
*/
public void removeTemplate(PageTemplate template) {
if (template == null)
throw new IllegalArgumentException("Template must not be null");
logger.debug("Removing page template '{}'", template.getIdentifier());
templates.remove(template.getIdentifier());
if (template.equals(defaultTemplate)) {
defaultTemplate = null;
logger.debug("Default template is now undefined");
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getTemplate(java.lang.String)
*/
public PageTemplate getTemplate(String template) {
return templates.get(template);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getTemplates()
*/
public PageTemplate[] getTemplates() {
return templates.values().toArray(new PageTemplate[templates.size()]);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setDefaultTemplate(ch.entwine.weblounge.common.content.page.PageTemplate)
*/
public void setDefaultTemplate(PageTemplate template) {
if (template != null) {
template.setSite(this);
templates.put(template.getIdentifier(), template);
logger.debug("Default page template is '{}'", template.getIdentifier());
} else
logger.debug("Default template is now undefined");
this.defaultTemplate = template;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getDefaultTemplate()
*/
public PageTemplate getDefaultTemplate() {
return defaultTemplate;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addLanguage(ch.entwine.weblounge.common.language.Language)
*/
public void addLanguage(Language language) {
if (language != null)
languages.put(language.getIdentifier(), language);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#removeLanguage(ch.entwine.weblounge.common.language.Language)
*/
public void removeLanguage(Language language) {
if (language != null) {
languages.remove(language.getIdentifier());
if (language.equals(defaultLanguage))
defaultLanguage = null;
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getLanguage(java.lang.String)
*/
public Language getLanguage(String languageId) {
return languages.get(languageId);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getLanguages()
*/
public Language[] getLanguages() {
return languages.values().toArray(new Language[languages.size()]);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#supportsLanguage(ch.entwine.weblounge.common.language.Language)
*/
public boolean supportsLanguage(Language language) {
return languages.values().contains(language);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setDefaultLanguage(ch.entwine.weblounge.common.language.Language)
*/
public void setDefaultLanguage(Language language) {
addLanguage(language);
defaultLanguage = language;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getDefaultLanguage()
*/
public Language getDefaultLanguage() {
return defaultLanguage;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addLayout(ch.entwine.weblounge.common.content.page.PageLayout)
*/
public void addLayout(PageLayout layout) {
if (layout == null)
throw new IllegalStateException("Layout must not be null");
layouts.put(layout.getIdentifier(), layout);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#removeLayout(java.lang.String)
*/
public PageLayout removeLayout(String layout) {
if (layout == null)
throw new IllegalStateException("Layout must not be null");
return layouts.remove(layout);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getLayout(java.lang.String)
*/
public PageLayout getLayout(String layout) {
return layouts.get(layout);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getLayouts()
*/
public PageLayout[] getLayouts() {
return layouts.values().toArray(new PageLayout[layouts.size()]);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setDefaultHostname(URL)
*/
public void setDefaultHostname(SiteURL url) {
defaultURL = url;
if (url != null)
addHostname(url);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setDefaultHostname(ch.entwine.weblounge.common.site.SiteURL,
* ch.entwine.weblounge.common.site.Environment)
*/
public void setDefaultHostname(SiteURL url, Environment environment) {
if (url == null)
throw new IllegalArgumentException("Url must not be null");
if (environment == null)
throw new IllegalArgumentException("Environment must not be null");
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addHostname(URL)
*/
public void addHostname(SiteURL url) {
if (url == null)
throw new IllegalArgumentException("Url must not be null");
urls.add(url);
if (url.isDefault()) {
defaultURLByEnvironment.put(url.getEnvironment(), url);
if (Environment.Production.equals(url.getEnvironment()))
defaultURL = url;
}
// Make sure we have a default URL
if (defaultURL == null)
defaultURL = url;
// ... and a default
if (defaultURLByEnvironment.get(url.getEnvironment()) == null)
defaultURLByEnvironment.put(url.getEnvironment(), url);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#removeHostname(URL)
*/
public boolean removeHostname(SiteURL url) {
if (url == null)
throw new IllegalArgumentException("Hostname must not be null");
if (url.equals(defaultURL))
defaultURL = null;
return urls.remove(url);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getHostnames()
*/
public SiteURL[] getHostnames() {
return urls.toArray(new SiteURL[urls.size()]);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getHostname()
*/
public SiteURL getHostname() {
return defaultURL;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getHostname(ch.entwine.weblounge.common.site.Environment)
*/
public SiteURL getHostname(Environment environment) {
SiteURL url = defaultURLByEnvironment.get(environment);
if (url != null)
return url;
return defaultURL;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addModule(ch.entwine.weblounge.common.site.Module)
*/
public void addModule(Module module) throws ModuleException {
if (module == null)
throw new IllegalArgumentException("Module must not be null");
module.setSite(this);
modules.put(module.getIdentifier(), module);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#removeModule(java.lang.String)
*/
public Module removeModule(String module) throws ModuleException {
if (module == null)
throw new IllegalArgumentException("Module must not be null");
Module m = modules.remove(module);
if (m != null)
m.destroy();
return m;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getModule(java.lang.String)
*/
public Module getModule(String module) {
if (module == null)
throw new IllegalArgumentException("Module must not be null");
return modules.get(module);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getModules()
*/
public Module[] getModules() {
return modules.values().toArray(new Module[modules.size()]);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setContentRepository(ch.entwine.weblounge.common.repository.ContentRepository)
*/
public void setContentRepository(ContentRepository repository) {
ContentRepository oldRepository = contentRepository;
this.contentRepository = repository;
if (repository != null) {
logger.debug("Content repository {} connected to site '{}'", repository, this);
fireRepositoryConnected(repository);
} else {
logger.debug("Content repository {} disconnected from site '{}'", oldRepository, this);
fireRepositoryDisconnected(oldRepository);
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setSecurity(java.net.URL)
*/
@Override
public void setSecurity(URL url) {
this.security = url;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getSecurity()
*/
@Override
public URL getSecurity() {
return security;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#setDigestType(ch.entwine.weblounge.common.security.DigestType)
*/
@Override
public void setDigestType(DigestType digest) {
this.digestType = digest;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getDigestType()
*/
@Override
public DigestType getDigestType() {
return digestType;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getContentRepository()
*/
public ContentRepository getContentRepository() {
return contentRepository;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#suggest(java.lang.String,
* java.lang.String, int)
*/
public List<String> suggest(String dictionary, String seed, int count)
throws ContentRepositoryException {
if (contentRepository == null)
throw new IllegalStateException("Cannot suggest while site without a content repository");
return contentRepository.suggest(dictionary, seed, count);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addRequestListener(ch.entwine.weblounge.common.request.RequestListener)
*/
public void addRequestListener(RequestListener listener) {
if (requestListeners == null)
requestListeners = new ArrayList<RequestListener>();
synchronized (requestListeners) {
requestListeners.add(listener);
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#removeRequestListener(ch.entwine.weblounge.common.request.RequestListener)
*/
public void removeRequestListener(RequestListener listener) {
if (requestListeners != null) {
synchronized (requestListeners) {
requestListeners.remove(listener);
}
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addSiteListener(ch.entwine.weblounge.common.site.SiteListener)
*/
public void addSiteListener(SiteListener listener) {
if (siteListeners == null)
siteListeners = new ArrayList<SiteListener>();
synchronized (siteListeners) {
siteListeners.add(listener);
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#removeSiteListener(ch.entwine.weblounge.common.site.SiteListener)
*/
public void removeSiteListener(SiteListener listener) {
if (siteListeners != null) {
synchronized (siteListeners) {
siteListeners.remove(listener);
}
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addUserListener(ch.entwine.weblounge.common.security.UserListener)
*/
public void addUserListener(UserListener listener) {
if (userListeners == null)
userListeners = new ArrayList<UserListener>();
synchronized (userListeners) {
userListeners.add(listener);
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#removeUserListener(ch.entwine.weblounge.common.security.UserListener)
*/
public void removeUserListener(UserListener listener) {
if (userListeners != null) {
synchronized (userListeners) {
userListeners.remove(listener);
}
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#addLocalRole(java.lang.String,
* java.lang.String)
*/
public void addLocalRole(String systemRole, String localRole) {
if (StringUtils.isBlank(systemRole))
throw new IllegalArgumentException("System role name cannot be blank");
if (StringUtils.isBlank(localRole))
throw new IllegalArgumentException("Local role name cannot be blank");
localRoles.put(systemRole, localRole);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#getLocalRole(java.lang.String)
*/
public String getLocalRole(String role) {
if (localRoles.containsKey(role))
return localRoles.get(role);
return null;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#start()
*/
public synchronized void start() throws SiteException, IllegalStateException {
logger.debug("Starting site {}", this);
if (isOnline)
throw new IllegalStateException("Site is already running");
// Start the site modules
synchronized (modules) {
List<Module> started = new ArrayList<Module>(modules.size());
for (Module module : modules.values()) {
if (!module.isEnabled())
continue;
try {
module.start();
started.add(module);
// start jobs
for (Job job : module.getJobs()) {
scheduleJob(job);
}
// actions are being registered automatically
} catch (Throwable t) {
logger.error("Error starting module '{}'", module, t);
}
}
}
// Register the integration tests
if (integrationTests != null && integrationTests.size() > 0) {
for (IntegrationTest test : integrationTests) {
logger.debug("Registering integration test {}", test.getName());
integrationTestRegistrations.add(getBundleContext().registerService(IntegrationTest.class.getName(), test, null));
}
}
// Finally, mark this site as running
isOnline = true;
logger.info("Site '{}' started", this);
// Tell listeners
fireSiteStarted();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#stop()
*/
public synchronized void stop() throws IllegalStateException {
logger.debug("Stopping site '{}'", this);
if (!isOnline)
throw new IllegalStateException("Site is not running");
// Stop jobs
synchronized (jobs) {
for (QuartzJob job : jobs.values()) {
unscheduleJob(job);
}
}
// Shutdown all of the modules
synchronized (modules) {
for (Module module : modules.values()) {
try {
logger.debug("Stopping module '{}'", module);
module.stop();
} catch (Throwable t) {
logger.error("Error stopping module '{}'", module, t);
}
}
}
// Unregister integration tests
logger.debug("Unregistering integration tests");
for (ServiceRegistration registration : integrationTestRegistrations) {
try {
registration.unregister();
} catch (IllegalStateException e) {
// Never mind, the service has been unregistered already
} catch (Throwable t) {
logger.error("Unregistering integration test failed: {}", t.getMessage());
}
}
// Finally, mark this site as stopped
isOnline = false;
logger.info("Site '{}' stopped", this);
// Tell listeners
fireSiteStopped();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#isOnline()
*/
public boolean isOnline() {
return isOnline && contentRepository != null;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.RequestListener#requestStarted(ch.entwine.weblounge.common.request.WebloungeRequest,
* ch.entwine.weblounge.common.request.WebloungeResponse)
*/
public void requestStarted(WebloungeRequest request,
WebloungeResponse response) {
// TODO: Remove
fireRequestStarted(request, response);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.RequestListener#requestDelivered(ch.entwine.weblounge.common.request.WebloungeRequest,
* ch.entwine.weblounge.common.request.WebloungeResponse)
*/
public void requestDelivered(WebloungeRequest request,
WebloungeResponse response) {
// TODO: Remove
fireRequestDelivered(request, response);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.RequestListener#requestFailed(ch.entwine.weblounge.common.request.WebloungeRequest,
* ch.entwine.weblounge.common.request.WebloungeResponse, int)
*/
public void requestFailed(WebloungeRequest request,
WebloungeResponse response, int reason) {
// TODO: Remove
fireRequestFailed(request, response, reason);
}
/**
* Method to fire a <code>requestStarted()</code> message to all registered
* <code>RequestListener</code>s.
*
* @param request
* the started request
* @param response
* the response
*/
protected void fireRequestStarted(WebloungeRequest request,
WebloungeResponse response) {
if (requestListeners == null)
return;
synchronized (requestListeners) {
for (RequestListener listener : requestListeners) {
listener.requestStarted(request, response);
}
}
}
/**
* Method to fire a <code>requestDelivered()</code> message to all registered
* <code>RequestListener</code>s.
*
* @param request
* the delivered request
* @param response
* the response
*/
protected void fireRequestDelivered(WebloungeRequest request,
WebloungeResponse response) {
if (requestListeners == null)
return;
synchronized (requestListeners) {
for (RequestListener listener : requestListeners) {
listener.requestDelivered(request, response);
}
}
}
/**
* Method to fire a <code>requestFailed()</code> message to all registered
* <code>RequestListener</code>s.
*
* @param request
* the failed request
* @param response
* the response
* @param error
* the error code
*/
protected void fireRequestFailed(WebloungeRequest request,
WebloungeResponse response, int error) {
if (requestListeners == null)
return;
synchronized (requestListeners) {
for (RequestListener listener : requestListeners) {
listener.requestFailed(request, response, error);
}
}
}
/**
* This method is called if a user is logged in.
*
* @param user
* the user that logged in
*/
protected void fireUserLoggedIn(User user) {
if (userListeners == null)
return;
synchronized (userListeners) {
for (UserListener listener : userListeners) {
listener.userLoggedIn(user);
}
}
}
/**
* This method is called if a user is logged out.
*
* @param user
* the user that logged out
*/
protected void fireUserLoggedOut(User user) {
if (userListeners == null)
return;
synchronized (userListeners) {
for (UserListener listener : userListeners) {
listener.userLoggedOut(user);
}
}
}
/**
* Method to fire a <code>siteStarted()</code> message to all registered
* <code>SiteListener</code>s.
*/
protected void fireSiteStarted() {
if (siteListeners == null)
return;
synchronized (siteListeners) {
for (SiteListener listener : siteListeners) {
listener.siteStarted(this);
}
}
}
/**
* Method to fire a <code>siteStopped()</code> message to all registered
* <code>SiteListener</code>s.
*/
protected void fireSiteStopped() {
if (siteListeners == null)
return;
synchronized (siteListeners) {
for (SiteListener listener : siteListeners) {
listener.siteStopped(this);
}
}
}
/**
* Method to fire a <code>repositoryConnected()</code> message to all
* registered <code>SiteListener</code>s.
*
* @param repository
* the content repository
*/
protected void fireRepositoryConnected(ContentRepository repository) {
if (siteListeners == null)
return;
synchronized (siteListeners) {
for (SiteListener listener : siteListeners) {
listener.repositoryConnected(this, repository);
}
}
}
/**
* Method to fire a <code>repositoryDisconnected()</code> message to all
* registered <code>SiteListener</code>s.
*
* @param repository
* the content repository
*/
protected void fireRepositoryDisconnected(ContentRepository repository) {
if (siteListeners == null)
return;
synchronized (siteListeners) {
for (SiteListener listener : siteListeners) {
listener.repositoryDisconnected(this, repository);
}
}
}
/* -------------------------------- OSGi -------------------------------- */
/**
* This method is a callback from the service tracker that is started when
* this site is started. It is looking for an implementation of the Quartz
* scheduler. The configuration is expected to be <code>0..1</code>, so there
* should only be one scheduler instance at any given moment.
*
* @param scheduler
* the quartz scheduler
*/
synchronized void setScheduler(Scheduler scheduler) {
this.scheduler = scheduler;
this.quartzTriggerListener = new QuartzTriggerListener(this);
try {
this.scheduler.addTriggerListener(quartzTriggerListener);
if (isOnline) {
synchronized (jobs) {
for (QuartzJob job : jobs.values()) {
scheduleJob(job);
}
}
}
} catch (SchedulerException e) {
logger.error("Error adding trigger listener to quartz scheduler", e);
}
}
/**
* This method is a callback from the service tracker that is started when
* this site is started, indicating that the scheduler service is no longer
* available.
*/
void removeScheduler() {
if (!isShutdownInProgress)
logger.info("Site '{}' can no longer execute jobs (scheduler was taken down)", this);
this.quartzTriggerListener = null;
}
/**
* Callback from the OSGi environment to activate the site. Subclasses should
* make sure to call this super implementation as it will assist in correctly
* setting up the site.
* <p>
* This method should be configured in the <tt>Dynamic Services</tt> section
* of your bundle.
*
* @param context
* the bundle context
* @param properties
* the component properties
* @throws Exception
* if the site activation fails
*/
protected void activate(BundleContext ctx, Map<String, String> properties)
throws Exception {
bundleContext = ctx;
bundleContext.getBundle();
serviceProperties = properties;
// Fix the site identifier
if (getIdentifier() == null) {
String identifier = properties.get(PROP_IDENTIFIER);
if (identifier == null)
throw new IllegalStateException("Property'" + PROP_IDENTIFIER + "' missing from site bundle");
setIdentifier(identifier);
}
}
/**
* Callback from the OSGi environment to deactivate the site. Subclasses
* should make sure to call this super implementation as it will assist in
* correctly shutting down the site.
* <p>
* This method should be configured in the <tt>Dynamic Services</tt> section
* of your bundle.
*
* @param context
* the bundle context
* @param properties
* the component properties
* @throws Exception
* if the site deactivation fails
*/
protected void deactivate(BundleContext context,
Map<String, String> properties) throws Exception {
try {
isShutdownInProgress = true;
logger.debug("Taking down site '{}'", this);
logger.debug("Stopped looking for a job scheduling services");
if (schedulingServiceTracker != null)
schedulingServiceTracker.close();
logger.info("Site '{}' deactivated", this);
} finally {
isShutdownInProgress = false;
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#setOption(java.lang.String,
* java.lang.String)
*/
public void setOption(String name, String value) {
options.setOption(name, value);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#setOption(java.lang.String,
* java.lang.String, ch.entwine.weblounge.common.site.Environment)
*/
public void setOption(String name, String value, Environment environment) {
options.setOption(name, value, environment);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#removeOption(java.lang.String)
*/
public void removeOption(String name) {
options.removeOption(name);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#getOptionValue(java.lang.String)
*/
public String getOptionValue(String name) {
return options.getOptionValue(name);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#getOptionValue(java.lang.String,
* java.lang.String)
*/
public String getOptionValue(String name, String defaultValue) {
return options.getOptionValue(name, defaultValue);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#getOptionValues(java.lang.String)
*/
public String[] getOptionValues(String name) {
return options.getOptionValues(name);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#hasOption(java.lang.String)
*/
public boolean hasOption(String name) {
return options.hasOption(name);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#getOptionNames()
*/
public String[] getOptionNames() {
return options.getOptionNames();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.Customizable#getOptions()
*/
public Map<String, Map<Environment, List<String>>> getOptions() {
return options.getOptions();
}
/**
* Schedules the job with the Quartz job scheduler.
*
* @param job
* the job
*/
private void scheduleJob(Job job) {
if (scheduler == null)
return;
// If this scheduling operation is due to a site restart, the job needs to
// be reset, otherwise fire-once jobs won't work.
job.getTrigger().reset();
// Throw the job at quartz
String groupName = "site " + this.getIdentifier();
String jobIdentifier = job.getIdentifier();
Class<? extends JobWorker> jobClass = job.getWorker();
JobTrigger trigger = job.getTrigger();
synchronized (jobs) {
// Set up the job detail
JobDataMap jobData = new JobDataMap();
jobData.put(QuartzJobWorker.CLASS, jobClass);
jobData.put(QuartzJobWorker.CLASS_LOADER, new BundleClassLoader(bundleContext.getBundle()));
jobData.put(QuartzJobWorker.CONTEXT, job.getContext());
job.getContext().put(Site.class.getName(), this);
job.getContext().put(BundleContext.class.getName(), bundleContext);
JobDetail jobDetail = new JobDetail(jobIdentifier, groupName, QuartzJobWorker.class);
jobDetail.setJobDataMap(jobData);
// Define the trigger
Trigger quartzTrigger = new QuartzJobTrigger(jobIdentifier, groupName, trigger);
quartzTrigger.addTriggerListener(quartzTriggerListener.getName());
// Schedule
try {
Date date = scheduler.scheduleJob(jobDetail, quartzTrigger);
jobs.put(jobIdentifier, new QuartzJob(jobIdentifier, jobClass, trigger));
String repeat = trigger.getNextExecutionAfter(date) != null ? " first" : "";
logger.info("Job '{}' scheduled,{} execution scheduled for {}", new Object[] {
jobIdentifier,
repeat,
date });
} catch (SchedulerException e) {
logger.error("Error trying to schedule job '{}': {}", new Object[] {
jobIdentifier,
e.getMessage(),
e });
}
}
}
/**
* Removes the job from the Quartz job scheduler.
*
* @param job
* the job
*/
private void unscheduleJob(QuartzJob job) {
try {
if (scheduler == null || scheduler.isShutdown())
return;
} catch (SchedulerException e1) {
// Ignore
}
String groupName = "site " + this.getIdentifier();
String jobIdentifier = job.getIdentifier();
try {
if (scheduler.unscheduleJob(jobIdentifier, groupName))
logger.info("Job '{}' unscheduled", jobIdentifier);
} catch (SchedulerException e) {
logger.error("Error trying to schedule job {}: {}", new Object[] {
jobIdentifier,
e.getMessage(),
e });
}
}
/**
* Loads all integration tests.
*
* @return the tests
*/
private List<IntegrationTest> loadIntegrationTests() {
BundleContext ctx = getBundleContext();
logger.info("Loading integration tests for '{}'", this);
// Load test classes
List<IntegrationTest> tests = loadIntegrationTestClasses("/", ctx.getBundle());
for (IntegrationTest test : tests) {
logger.debug("Registering integration test " + test.getClass());
integrationTestRegistrations.add(ctx.registerService(IntegrationTest.class.getName(), test, null));
}
// Find and register site-wide integration tests
logger.debug("Looking for integration tests in site '{}'", this);
Enumeration<URL> siteDirectories = bundleContext.getBundle().findEntries("site", "*", false);
while (siteDirectories != null && siteDirectories.hasMoreElements()) {
URL entry = siteDirectories.nextElement();
if (entry.getPath().endsWith("/tests/")) {
tests.addAll(loadIntegrationTestDefinitions(entry.getPath()));
break;
}
}
// Find and register module integration tests
Enumeration<URL> modules = bundleContext.getBundle().findEntries("site/modules", "*", false);
logger.debug("Looking for integration tests in site '{}' modules", this);
while (modules != null && modules.hasMoreElements()) {
URL module = modules.nextElement();
Enumeration<URL> moduleDirectories = bundleContext.getBundle().findEntries(module.getPath(), "*", false);
while (moduleDirectories != null && moduleDirectories.hasMoreElements()) {
URL entry = moduleDirectories.nextElement();
if (entry.getPath().endsWith("/tests/")) {
tests.addAll(loadIntegrationTestDefinitions(entry.getPath()));
break;
}
}
}
if (tests.size() > 0)
logger.info("Registering {} integration tests for site '{}'", tests.size(), this);
return tests;
}
/**
* Loads and registers the integration tests that are found in the bundle at
* the given location.
*
* @param dir
* the directory containing the test files
*/
private List<IntegrationTest> loadIntegrationTestDefinitions(String dir) {
Enumeration<?> entries = bundleContext.getBundle().findEntries(dir, "*.xml", true);
// Schema validator setup
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
URL schemaUrl = SiteImpl.class.getResource("/xsd/test.xsd");
Schema testSchema = null;
try {
testSchema = schemaFactory.newSchema(schemaUrl);
} catch (SAXException e) {
logger.error("Error loading XML schema for test definitions: {}", e.getMessage());
return Collections.emptyList();
}
// Module.xml document builder setup
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setSchema(testSchema);
docBuilderFactory.setNamespaceAware(true);
// The list of tests
List<IntegrationTest> tests = new ArrayList<IntegrationTest>();
while (entries != null && entries.hasMoreElements()) {
URL entry = (URL) entries.nextElement();
// Validate and read the module descriptor
ValidationErrorHandler errorHandler = new ValidationErrorHandler(entry);
DocumentBuilder docBuilder;
try {
docBuilder = docBuilderFactory.newDocumentBuilder();
docBuilder.setErrorHandler(errorHandler);
Document doc = docBuilder.parse(entry.openStream());
if (errorHandler.hasErrors()) {
logger.warn("Error parsing integration test {}: XML validation failed", entry);
continue;
}
IntegrationTestGroup test = IntegrationTestParser.fromXml(doc.getFirstChild());
test.setSite(this);
test.setGroup(getName());
tests.add(test);
} catch (SAXException e) {
throw new IllegalStateException(e);
} catch (IOException e) {
throw new IllegalStateException(e);
} catch (ParserConfigurationException e) {
throw new IllegalStateException(e);
}
}
return tests;
}
/**
* Loads the integration test classes from the class path and publishes them
* to the OSGi registry.
*
* @param path
* the bundle path to load classes from
* @param bundle
* the bundle
*/
private List<IntegrationTest> loadIntegrationTestClasses(String path,
Bundle bundle) {
List<IntegrationTest> tests = new ArrayList<IntegrationTest>();
// Load the classes in question
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Enumeration<?> entries = bundle.findEntries("/", "*.class", true);
if (entries == null) {
return tests;
}
// Look at the classes and instantiate those that implement the integration
// test interface.
while (entries.hasMoreElements()) {
URL url = (URL) entries.nextElement();
Class<?> c = null;
String className = url.getPath();
try {
className = className.substring(1, className.indexOf(".class"));
className = className.replace('/', '.');
c = loader.loadClass(className);
boolean implementsInterface = Arrays.asList(c.getInterfaces()).contains(IntegrationTest.class);
boolean extendsBaseClass = false;
if (c.getSuperclass() != null) {
extendsBaseClass = IntegrationTestBase.class.getName().equals(c.getSuperclass().getName());
}
if (!implementsInterface && !extendsBaseClass)
continue;
IntegrationTest test = (IntegrationTest) c.newInstance();
test.setSite(this);
tests.add(test);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Implementation " + className + " for integration test of class '" + identifier + "' not found", e);
} catch (NoClassDefFoundError e) {
// We are trying to load each and every class here, so we may as well
// see classes that are not meant to be loaded
logger.debug("The related class " + e.getMessage() + " for potential test case implementation " + className + " could not be found");
} catch (InstantiationException e) {
throw new IllegalStateException("Error instantiating impelementation " + className + " for integration test '" + identifier + "'", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Access violation instantiating implementation " + className + " for integration test '" + identifier + "'", e);
} catch (Throwable t) {
throw new IllegalStateException("Error loading implementation " + className + " for integration test '" + identifier + "'", t);
}
}
return tests;
}
/**
* Initializes this site from an XML node that was generated using
* {@link #toXml()}.
* <p>
* To speed things up, you might consider using the second signature that uses
* an existing <code>XPath</code> instance instead of creating a new one.
*
* @param config
* the site node
* @throws IllegalStateException
* if the site cannot be parsed
* @see #fromXml(Node, XPath)
* @see #toXml()
*/
public static Site fromXml(Node config) throws IllegalStateException {
XPath xpath = XPathFactory.newInstance().newXPath();
// Define the xml namespace
XPathNamespaceContext nsCtx = new XPathNamespaceContext(false);
nsCtx.defineNamespaceURI("ns", SITE_XMLNS);
xpath.setNamespaceContext(nsCtx);
return fromXml(config, xpath);
}
/**
* Initializes this site from an XML node that was generated using
* {@link #toXml()}.
*
* @param config
* the site node
* @param xpathProcessor
* xpath processor to use
* @throws IllegalStateException
* if the site cannot be parsed
* @see #toXml()
*/
public static Site fromXml(Node config, XPath xpathProcessor)
throws IllegalStateException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// identifier
String identifier = XPathHelper.valueOf(config, "@id", xpathProcessor);
if (identifier == null)
throw new IllegalStateException("Unable to create sites without identifier");
// class
Site site = null;
String className = XPathHelper.valueOf(config, "ns:class", xpathProcessor);
if (className != null) {
try {
Class<? extends Site> c = (Class<? extends Site>) classLoader.loadClass(className);
site = c.newInstance();
site.setIdentifier(identifier);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Implementation " + className + " for site '" + identifier + "' not found", e);
} catch (InstantiationException e) {
throw new IllegalStateException("Error instantiating impelementation " + className + " for site '" + identifier + "'", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Access violation instantiating implementation " + className + " for site '" + identifier + "'", e);
} catch (Throwable t) {
throw new IllegalStateException("Error loading implementation " + className + " for site '" + identifier + "'", t);
}
} else {
site = new SiteImpl();
site.setIdentifier(identifier);
}
// name
String name = XPathHelper.valueOf(config, "ns:name", xpathProcessor);
if (name == null)
throw new IllegalStateException("Site '" + identifier + " has no name");
site.setName(name);
// domains
NodeList urlNodes = XPathHelper.selectList(config, "ns:domains/ns:url", xpathProcessor);
if (urlNodes.getLength() == 0)
throw new IllegalStateException("Site '" + identifier + " has no hostname");
String url = null;
try {
for (int i = 0; i < urlNodes.getLength(); i++) {
Node urlNode = urlNodes.item(i);
url = urlNode.getFirstChild().getNodeValue();
boolean defaultUrl = ConfigurationUtils.isDefault(urlNode);
Environment environment = Environment.Production;
Node environmentAttribute = urlNode.getAttributes().getNamedItem("environment");
if (environmentAttribute != null && environmentAttribute.getNodeValue() != null)
environment = Environment.valueOf(StringUtils.capitalize(environmentAttribute.getNodeValue()));
SiteURLImpl siteURL = new SiteURLImpl(new URL(url));
siteURL.setDefault(defaultUrl);
siteURL.setEnvironment(environment);
site.addHostname(siteURL);
}
} catch (MalformedURLException e) {
throw new IllegalStateException("Site '" + identifier + "' defines malformed url: " + url);
}
// languages
NodeList languageNodes = XPathHelper.selectList(config, "ns:languages/ns:language", xpathProcessor);
if (languageNodes.getLength() == 0)
throw new IllegalStateException("Site '" + identifier + " has no languages");
for (int i = 0; i < languageNodes.getLength(); i++) {
Node languageNode = languageNodes.item(i);
Node defaultAttribute = languageNode.getAttributes().getNamedItem("default");
String languageId = languageNode.getFirstChild().getNodeValue();
try {
Language language = LanguageUtils.getLanguage(languageId);
if (ConfigurationUtils.isTrue(defaultAttribute))
site.setDefaultLanguage(language);
else
site.addLanguage(language);
} catch (UnknownLanguageException e) {
throw new IllegalStateException("Site '" + identifier + "' defines unknown language: " + languageId);
}
}
// templates
NodeList templateNodes = XPathHelper.selectList(config, "ns:templates/ns:template", xpathProcessor);
PageTemplate firstTemplate = null;
for (int i = 0; i < templateNodes.getLength(); i++) {
PageTemplate template = PageTemplateImpl.fromXml(templateNodes.item(i), xpathProcessor);
boolean isDefault = ConfigurationUtils.isDefault(templateNodes.item(i));
if (isDefault && site.getDefaultTemplate() != null) {
logger.warn("Site '{}' defines more than one default templates", site.getIdentifier());
} else if (isDefault) {
site.setDefaultTemplate(template);
logger.debug("Site '{}' uses default template '{}'", site.getIdentifier(), template.getIdentifier());
} else {
site.addTemplate(template);
logger.debug("Added template '{}' to site '{}'", template.getIdentifier(), site.getIdentifier());
}
if (firstTemplate == null)
firstTemplate = template;
}
// Make sure we have a default template
if (site.getDefaultTemplate() == null) {
if (firstTemplate == null)
throw new IllegalStateException("Site '" + site.getIdentifier() + "' does not specify any page templates");
logger.warn("Site '{}' does not specify a default template. Using '{}'", site.getIdentifier(), firstTemplate.getIdentifier());
site.setDefaultTemplate(firstTemplate);
}
// security
String securityConfiguration = XPathHelper.valueOf(config, "ns:security/ns:configuration", xpathProcessor);
if (securityConfiguration != null) {
URL securityConfig = null;
// If converting the path into a URL fails, we are assuming that the
// configuration is part of the bundle
try {
securityConfig = new URL(securityConfiguration);
} catch (MalformedURLException e) {
logger.debug("Security configuration {} is pointing to the bundle", securityConfiguration);
securityConfig = SiteImpl.class.getResource(securityConfiguration);
if (securityConfig == null) {
throw new IllegalStateException("Security configuration " + securityConfig + " of site '" + site.getIdentifier() + "' cannot be located inside of bundle", e);
}
}
site.setSecurity(securityConfig);
}
// administrator
Node adminNode = XPathHelper.select(config, "ns:security/ns:administrator", xpathProcessor);
if (adminNode != null) {
site.setAdministrator(SiteAdminImpl.fromXml(adminNode, site, xpathProcessor));
}
// digest policy
Node digestNode = XPathHelper.select(config, "ns:security/ns:digest", xpathProcessor);
if (digestNode != null) {
site.setDigestType(DigestType.valueOf(digestNode.getFirstChild().getNodeValue()));
}
// role definitions
NodeList roleNodes = XPathHelper.selectList(config, "ns:security/ns:roles/ns:*", xpathProcessor);
for (int i = 0; i < roleNodes.getLength(); i++) {
Node roleNode = roleNodes.item(i);
String roleName = roleNode.getLocalName();
String localRoleName = roleNode.getFirstChild().getNodeValue();
if (Security.SITE_ADMIN_ROLE.equals(roleName))
site.addLocalRole(Security.SITE_ADMIN_ROLE, localRoleName);
if (Security.PUBLISHER_ROLE.equals(roleName))
site.addLocalRole(Security.PUBLISHER_ROLE, localRoleName);
if (Security.EDITOR_ROLE.equals(roleName))
site.addLocalRole(Security.EDITOR_ROLE, localRoleName);
if (Security.GUEST_ROLE.equals(roleName))
site.addLocalRole(Security.GUEST_ROLE, localRoleName);
}
// options
Node optionsNode = XPathHelper.select(config, "ns:options", xpathProcessor);
OptionsHelper.fromXml(optionsNode, site, xpathProcessor);
return site;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.site.Site#toXml()
*/
public String toXml() {
StringBuffer b = new StringBuffer();
b.append("<site id=\"");
b.append(identifier);
b.append("\" ");
// schema reference
b.append("xmlns=\"http://www.entwinemedia.com/weblounge/3.2/site\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.entwinemedia.com/weblounge/3.2/site http://www.entwinemedia.com/xsd/weblounge/3.2/site.xsd\"");
b.append(">");
// autostart
b.append("<autostart>").append(autoStart).append("</autostart>");
// name
b.append("<name><![CDATA[").append(name).append("]]></name>");
// class
if (!this.getClass().equals(SiteImpl.class))
b.append("<class>").append(this.getClass().getName()).append("</class>");
// languages
if (languages.size() > 0) {
b.append("<languages>");
for (Language language : languages.values()) {
b.append("<language");
if (language.equals(defaultLanguage))
b.append(" default=\"true\"");
b.append(">");
b.append(language.getIdentifier());
b.append("</language>");
}
b.append("</languages>");
}
// hostnames
if (urls.size() > 0) {
b.append("<domains>");
for (SiteURL url : urls) {
b.append("<url");
if (url.equals(defaultURL))
b.append(" default=\"true\"");
b.append(" environment=\"").append(url.getEnvironment().toString().toLowerCase()).append("\"");
b.append(">");
b.append(url.toExternalForm());
b.append("</url>");
}
b.append("</domains>");
}
// security
if (administrator != null || localRoles.size() > 0) {
b.append("<security>");
if (security != null) {
b.append("<configuration>").append(security.toExternalForm()).append("</configuration>");
}
b.append("<digest>").append(digestType.toString()).append("</digest>");
if (administrator != null)
b.append(administrator.toXml());
if (localRoles.size() > 0) {
b.append("<roles>");
// Administrator role
String administratorRole = localRoles.get(Security.SITE_ADMIN_ROLE);
if (administratorRole != null)
b.append("<" + Security.SITE_ADMIN_ROLE + ">").append(administratorRole).append("</" + Security.SITE_ADMIN_ROLE + ">");
// Publisher Role
String publisherRole = localRoles.get(Security.PUBLISHER_ROLE);
if (publisherRole != null)
b.append("<" + Security.PUBLISHER_ROLE + ">").append(publisherRole).append("</" + Security.PUBLISHER_ROLE + ">");
// Editor Role
String editorRole = localRoles.get(Security.EDITOR_ROLE);
if (publisherRole != null)
b.append("<" + Security.EDITOR_ROLE + ">").append(editorRole).append("</" + Security.EDITOR_ROLE + ">");
// Guest Role
String guestRole = localRoles.get(Security.GUEST_ROLE);
if (publisherRole != null)
b.append("<" + Security.GUEST_ROLE + ">").append(guestRole).append("</" + Security.GUEST_ROLE + ">");
b.append("</roles>");
}
b.append("</security>");
}
// templates
if (templates.size() > 0) {
b.append("<templates>");
for (PageTemplate template : templates.values()) {
b.append(template.toXml());
}
b.append("</templates>");
}
// Options
b.append(options.toXml());
b.append("</site>");
return b.toString();
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return identifier;
}
}