org.sakaiproject.citation.impl.BaseConfigurationService.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.citation.impl.BaseConfigurationService.java

Source

/**********************************************************************************
 * $URL:  $
 * $Id:   $
 ***********************************************************************************
 *
 * Copyright (c) 2006, 2007, 2008, 2009 The Sakai Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **********************************************************************************/

package org.sakaiproject.citation.impl;

import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Observable;
import java.util.Observer;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.cover.SecurityService;
import org.sakaiproject.citation.api.ConfigurationService;
import org.sakaiproject.citation.api.SiteOsidConfiguration;
import org.sakaiproject.citation.util.api.OsidConfigurationException;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.cover.EntityManager;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.event.cover.EventTrackingService;
import org.sakaiproject.exception.IdInvalidException;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.IdUsedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.ServerOverloadException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SitePage;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.user.api.UserDirectoryService;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.thoughtworks.xstream.XStream;

import edu.indiana.lib.twinpeaks.util.DomException;

/**
 *
 */
public class BaseConfigurationService implements ConfigurationService, Observer {

    private static Log m_log = LogFactory.getLog(BaseConfigurationService.class);

    /**
     * Locale that will be used if user's locale is not available. 
     */
    private static final String SERVER_DEFAULT_LOCALE = Locale.ENGLISH.getLanguage();

    public static final String DEFAULT_SECONDS_BETWEEN_SAVECITE_REFRESHES = "5";
    public static final int MAXIMUM_SECONDS_BETWEEN_SAVECITE_REFRESHES = 30;
    public static final String PARAM_SECONDS_BETWEEN_SAVECITE_REFRESHES = "secondsBetweenSaveciteRefreshes";

    /*
     * All the following properties will be set by Spring using components.xml
     */
    // enable/disable entire helper
    protected boolean m_citationsEnabledByDefault = false;
    protected boolean m_allowSiteBySiteOverride = false;

    // enable/disable helper features -->
    protected String m_googleSearchEnabled = "false";
    protected String m_librarySearchEnabled = "false";
    protected boolean m_externalSearchEnabled = false;

    protected String m_adminSiteName = "citationsAdmin";
    protected String m_configFolder = "config";
    protected String m_configXml = "sakai/citationsConfig.xml";
    protected String m_categoriesXml = "sakai/databaseHierarchy.xml";

    protected SortedSet<String> m_updatableResources = Collections.synchronizedSortedSet(new TreeSet<String>());

    // configuration XML file location
    protected String m_databaseXml;
    protected String m_siteConfigXml;

    // metasearch engine parameters
    protected String m_metasearchUsername;
    protected String m_metasearchPassword;
    protected String m_metasearchBaseUrl;

    // which osid impl to use
    protected String m_osidImpl;
    // extended repository id (optional, leave as null if not used)
    protected String m_extendedRepositoryId;

    // openURL parameters -->
    protected String m_openUrlLabel;
    protected String m_openUrlResolverAddress;

    // google scholar parameters -->
    protected String m_googleBaseUrl;
    protected String m_sakaiServerKey;
    protected String m_externalSearchUrl;

    // site-specific config/authentication/authorization implementation -->
    protected String m_osidConfig;
    /*
     * End of components.xml properties
     */

    // other config services -->
    protected SessionManager m_sessionManager;
    protected ServerConfigurationService m_serverConfigurationService;

    private TreeSet<String> m_categories;
    private TreeSet<String> m_configs;

    /*
     * Site specific OSID configuration instance
     */
    private static SiteOsidConfiguration m_siteConfigInstance = null;

    /*
     * Dynamic configuration parameters
     */
    protected static Map<String, Map<String, String>> m_configMaps = new HashMap<String, Map<String, String>>();
    protected static String m_configListRef = null;

    /**
     * 
     */
    protected Map<String, List<Map<String, String>>> saveciteClients = new HashMap<String, List<Map<String, String>>>();

    /*
     * Interface methods
     */

    /**
     * Fetch the appropriate XML configuration document for this user
     * @return Configuration XML resource name
     */
    public String getConfigurationXml() throws OsidConfigurationException {
        SiteOsidConfiguration siteConfig = getSiteOsidConfiguration();
        String configXml = null;

        if (siteConfig != null) {
            configXml = siteConfig.getConfigurationXml();
        }

        if (isNull(configXml)) {
            configXml = m_siteConfigXml;
        }
        return configXml;
    }

    /**
     * Is the configuration XML file provided and readable
     * @return true If the XML file is provided and readable, false otherwise
     */
    public boolean isConfigurationXmlAvailable() {
        try {
            String configXml = getConfigurationXml();

            if (configXml == null) {
                return false;
            }
            return exists(configXml);
        } catch (OsidConfigurationException exception) {
            m_log.warn("Unexpected exception: " + exception);
        }
        return false;
    }

    public String getConfigFolderReference() {
        String configFolderRef = null;
        if (!isNull(this.m_adminSiteName) && !isNull(this.m_configFolder)) {
            configFolderRef = "/content/group/" + this.m_adminSiteName + "/" + this.m_configFolder + "/";
        }
        return configFolderRef;

    }

    public String getConfigFolderId() {
        String configFolderId = null;
        if (!isNull(this.m_adminSiteName) && !isNull(this.m_configFolder)) {
            configFolderId = "/group/" + this.m_adminSiteName + "/" + this.m_configFolder + "/";
        }
        return configFolderId;
    }

    /**
     * Fetch the appropriate XML database hierarchy document for this user
     * @return Hierarchy XML resource name
     */
    public String getDatabaseHierarchyXml() throws OsidConfigurationException {
        SiteOsidConfiguration siteConfig = getSiteOsidConfiguration();
        String databaseXml = null;

        if (siteConfig != null) {
            databaseXml = siteConfig.getDatabaseHierarchyXml();
        }

        if (isNull(databaseXml)) {
            databaseXml = m_databaseXml;
        }
        return databaseXml;
    }

    /**
     * Is the database hierarchy XML file provided and readable
     * @return true If the XML file is provided and readable, false otherwise
     */
    public boolean isDatabaseHierarchyXmlAvailable() {
        try {
            String dbXml = getDatabaseHierarchyXml();

            if (dbXml == null) {
                return false;
            }
            return exists(dbXml);
        } catch (OsidConfigurationException exception) {
            m_log.warn("Unexpected exception: " + exception);
        }
        return false;
    }

    /**
     * Fetch this user's group affiliations
     * @return A list of group IDs (empty if no IDs exist)
     */
    public List<String> getGroupIds() throws OsidConfigurationException {
        SiteOsidConfiguration siteConfig = getSiteOsidConfiguration();

        if (siteConfig == null) {
            ArrayList<String> emptyList = new ArrayList();

            return emptyList;
        }
        return siteConfig.getGroupIds();
    }

    /**
     * Fetch the site specific Repository OSID package name
     * @return Repository Package (eg org.sakaibrary.osid.repository.xserver)
     */
    public synchronized String getSiteConfigOsidPackageName() {
        String value = getConfigurationParameter("osid-impl");

        return (value != null) ? value : getOsidImpl();
    }

    /**
     * Fetch the site specific extended Repository ID
     * @return The Repository ID
     */
    public synchronized String getSiteConfigExtendedRepositoryId() {
        String value = getConfigurationParameter("extended-repository-id");

        return (value != null) ? value : getExtendedRepositoryId();
    }

    /**
     * Fetch the meta-search username
     * @return the username
     */
    public synchronized String getSiteConfigMetasearchUsername() {
        String value = getConfigurationParameter("metasearch-username");

        return (value != null) ? value : getMetasearchUsername();
    }

    /**
     * Fetch the meta-search password
     * @return the password
     */
    public synchronized String getSiteConfigMetasearchPassword() {
        String value = getConfigurationParameter("metasearch-password");

        return (value != null) ? value : getMetasearchPassword();
    }

    /**
     * Fetch the meta-search base-URL
     * @return the username
     */
    public synchronized String getSiteConfigMetasearchBaseUrl() {
        String value = getConfigurationParameter("metasearch-baseurl");

        return (value != null) ? value : getMetasearchBaseUrl();
    }

    /**
     * Fetch the OpenURL label
     * @return the label text
     */
    public synchronized String getSiteConfigOpenUrlLabel() {
        String value = getConfigurationParameter("openurl-label");

        return (value != null) ? value : getOpenUrlLabel();
    }

    /**
     * Fetch the OpenURL resolver address
     * @return the resolver address (domain name or IP)
     */
    public synchronized String getSiteConfigOpenUrlResolverAddress() {
        String value = getConfigurationParameter("openurl-resolveraddress");

        return (value != null) ? value : getOpenUrlResolverAddress();
    }

    /**
     * Fetch the Google base-URL
     * @return the URL
     */
    public synchronized String getSiteConfigGoogleBaseUrl() {
        String value = getConfigurationParameter("google-baseurl");

        return (value != null) ? value : getGoogleBaseUrl();
    }

    /**
     * Fetch the Sakai server key
     * @return the key text
     */
    public synchronized String getSiteConfigSakaiServerKey() {
        String value = getConfigurationParameter("sakai-serverkey");

        return (value != null) ? value : getSakaiServerKey();
    }

    /**
     * Get the maximum number of databases we can search at one time
     * @return The maximum value (defaults to <code>SEARCHABLE_DATABASES</code>
     *                            if no other value is specified)
     */
    public synchronized int getSiteConfigMaximumSearchableDBs() {
        String configValue = getConfigurationParameter("searchable-databases");
        int searchableDbs = SEARCHABLE_DATABASES;
        /*
         * Supply the default if no value was configured
         */
        if (configValue == null) {
            return searchableDbs;
        }
        /*
         * Make sure we have a good value
         */
        try {
            searchableDbs = Integer.parseInt(configValue);
            if (searchableDbs <= 0) {
                throw new NumberFormatException(configValue);
            }
        } catch (NumberFormatException exception) {
            if (m_log.isDebugEnabled()) {
                m_log.debug("Maximum searchable database exception: " + exception.toString());
            }
            searchableDbs = SEARCHABLE_DATABASES;
        } finally {
            return searchableDbs;
        }
    }

    /**
     * How should we use URLs marked as "preferred" by the OSID implementation?
     * The choices are:
     *<ul>
     * <li> "false" (do not use the preferred URL at all)
     * <li> "title-link" (provide the preferred URL as the title link)
     * <li> "related-link" (provide as a related link, not as the title link)
     *</ul>
     * Note: "false" is the default value if nothing is specified in the
     *       configuration file
     *
     * @return "related-link", "title-link", or "false"
     */
    public synchronized String getSiteConfigUsePreferredUrls() {
        String value = getConfigurationParameter("provide-direct-url");

        if (value == null) {
            return "false";
        }

        value = value.trim().toLowerCase();

        if (!(value.equals("false") || value.equals("related-link") || value.equals("title-link"))) {
            m_log.debug("Invalid value for <provide-direct-url>: \"" + value + "\", using \"false\"");
            value = "false";
        }
        return value;
    }

    /**
     * Prefix string for "preferred" URLs (when used as title or related links).
     *
     * This is likely to be the proxy information for the direct URL.
     *
     * @return The prefix String (null if none)
     */
    public synchronized String getSiteConfigPreferredUrlPrefix() {
        return getConfigurationParameter("direct-url-prefix");
    }

    /**
     * Enable/disable Citations Helper by default
     * @param state true to set default 'On'
     */
    public void setCitationsEnabledByDefault(boolean citationsEnabledByDefault) {
        m_citationsEnabledByDefault = citationsEnabledByDefault;
    }

    /**
     * Is the Citations Helper [potentially] available for all sites?
     * @return true if so
     */
    public boolean isCitationsEnabledByDefault() {
        String state = getConfigurationParameter("citations-enabled-by-default");

        if (state != null) {
            m_log.debug("Citations enabled by default (1): " + state.equals("true"));
            return state.equals("true");
        }
        m_log.debug("Citations enabled by default (2): " + m_citationsEnabledByDefault);
        return m_citationsEnabledByDefault;
    }

    /**
     * Enable/disable site by site Citations Helper override
     * @param state true to enable site by site Citations Helper
     */
    public void setAllowSiteBySiteOverride(boolean allowSiteBySiteOverride) {
        m_allowSiteBySiteOverride = allowSiteBySiteOverride;
    }

    /**
     * Is the Citations Helper enabled site-by-site?
     * @return true if so
     */
    public boolean isAllowSiteBySiteOverride() {
        String state = getConfigurationParameter("citations-enabled-site-by-site");

        if (state != null) {
            m_log.debug("Citations enabled site-by-site (1): " + state.equals("true"));
            return state.equals("true");
        }
        m_log.debug("Citations enabled site-by-site (2): " + m_allowSiteBySiteOverride);
        return m_allowSiteBySiteOverride;
    }

    /**
     * Enable/disable Google support (no support for site specific XML configuration)
     * @param state true to enable Google support
     */
    public void setGoogleScholarEnabled(boolean state) {
        String enabled = state ? "true" : "false";

        setGoogleSearchEnabled(enabled);
    }

    /**
     * Is Google search enabled? (no support for site specific XML configuration)
     * @return true if so
     */
    public boolean isGoogleScholarEnabled() {
        String state = getConfigurationParameter("google-scholar-enabled");

        if (state == null) {
            state = getGoogleSearchEnabled();
        }
        m_log.debug("Google enabled: " + state.equals("true"));
        return state.equals("true");
    }

    /**
     * Enable/disable library search support (no support for site specific XML configuration)
     * @param state true to enable support
     */
    public void setLibrarySearchEnabled(boolean state) {
        String enabled = state ? "true" : "false";

        setLibrarySearchEnabled(enabled);
    }

    /**
     * Is library search enabled for any users? (no support for site specific XML configuration)
     * @return true if so
     */
    public boolean isLibrarySearchEnabled() {
        String state = getConfigurationParameter("library-search-enabled");

        if (state == null) {
            state = getLibrarySearchEnabled();
        }
        m_log.debug("Library Search enabled: " + state.equals("true"));
        return state.equals("true");
    }

    /*
     * Helpers
     */

    /**
     * Get a named value from the site-specific XML configuration file
     * @param parameter Configuration parameter to lookup
     * @return Parameter value (null if none [or error])
     */
    protected String getConfigurationParameter(String parameter) {
        Map<String, String> parameterMap = null;

        try {
            SiteOsidConfiguration siteConfig;
            String configXml, configXmlRef;
            /*
             * Fetch the configuration XML resource name
             */
            siteConfig = getSiteOsidConfiguration();
            if (siteConfig == null) {
                return null;
            }

            if ((configXml = siteConfig.getConfigurationXml()) == null) {
                return null;
            }
            /*
             * Construct the full reference and look up the requested configuration
             */
            configXmlRef = this.getConfigFolderReference() + configXml;
            synchronized (this) {
                parameterMap = m_configMaps.get(configXmlRef);
            }
        } catch (OsidConfigurationException exception) {
            m_log.warn("Failed to get a dynamic XML value for " + parameter + ": " + exception);
        }
        /*
         * Finally, return the requested configuration parameter
         */
        return (parameterMap == null) ? null : parameterMap.get(parameter);
    }

    /**
     * Load and initialize the site-specific OSID configuration code
     * @return The initialized, site-specific OSID configuration
     *         object (null on error)
     */
    protected SiteOsidConfiguration getSiteOsidConfiguration() {
        SiteOsidConfiguration siteConfig;

        try {
            siteConfig = getConfigurationHandler(m_osidConfig);
            siteConfig.init();
        } catch (Exception exception) {
            m_log.warn("Failed to get " + m_osidConfig + ": " + exception);
            siteConfig = null;
        }
        return siteConfig;
    }

    /**
     * Return a SiteOsidConfiguration instance
     * @return A SiteOsidConfiguration
     */
    public synchronized SiteOsidConfiguration getConfigurationHandler(String osidConfigHandler)
            throws java.lang.ClassNotFoundException, java.lang.InstantiationException,
            java.lang.IllegalAccessException {
        if (m_siteConfigInstance == null) {
            Class configClass = Class.forName(osidConfigHandler);

            m_siteConfigInstance = (SiteOsidConfiguration) configClass.newInstance();
        }
        return m_siteConfigInstance;
    }

    /**
     * Populate cached values from a configuration XML resource.  We always try
     * to parse the resource, regardless of any prior success or failure.
     *
     * @param configurationXml Configuration resource name (this doubles as a
     *                         unique key into the configuration cache)
     */
    public void populateConfig(String configurationXml, InputStream stream) {
        org.w3c.dom.Document document;
        String value;

        /*
         * Parse the XML - if that fails, give up now
         */
        if ((document = parseXmlFromStream(stream)) == null) {
            return;
        }

        synchronized (this) {
            Map<String, String> parameterMap;

            /*
             * Successful parse - save the values
             */
            if ((parameterMap = m_configMaps.get(configurationXml)) == null) {
                parameterMap = new HashMap<String, String>();
            }
            parameterMap.clear();

            saveParameter(document, parameterMap, "citations-enabled-by-default");
            saveParameter(document, parameterMap, "citations-enabled-site-by-site");

            saveParameter(document, parameterMap, "google-scholar-enabled");
            saveParameter(document, parameterMap, "library-search-enabled");

            saveParameter(document, parameterMap, "osid-impl");
            saveParameter(document, parameterMap, "extended-repository-id");

            saveParameter(document, parameterMap, "metasearch-username");
            saveParameter(document, parameterMap, "metasearch-password");
            saveParameter(document, parameterMap, "metasearch-baseurl");
            saveParameter(document, parameterMap, "metasearch-enabled");

            saveParameter(document, parameterMap, "openurl-label");
            saveParameter(document, parameterMap, "openurl-resolveraddress");

            saveParameter(document, parameterMap, "provide-direct-url");
            saveParameter(document, parameterMap, "direct-url-prefix");

            saveParameter(document, parameterMap, "google-baseurl");
            saveParameter(document, parameterMap, "sakai-serverkey");

            saveParameter(document, parameterMap, "searchable-databases");

            saveParameter(document, parameterMap, "config-id"); // obsolete?
            saveParameter(document, parameterMap, "database-xml"); // obsolete?

            saveParameter(document, parameterMap, PARAM_SECONDS_BETWEEN_SAVECITE_REFRESHES);

            saveServletClientMappings(document);

            m_configMaps.put(configurationXml, parameterMap);
        }
    }

    protected void saveServletClientMappings(Document document) {

        Element clientElement = document.getElementById("saveciteClients");

        if (clientElement == null) {
            NodeList mapNodes = document.getElementsByTagName("map");
            if (mapNodes != null) {
                for (int i = 0; i < mapNodes.getLength(); i++) {
                    Element mapElement = (Element) mapNodes.item(i);
                    if (mapElement.hasAttribute("id") && mapElement.getAttribute("id").equals("saveciteClients")) {
                        clientElement = mapElement;
                        break;
                    }
                }
            }
        }

        if (clientElement != null) {
            try {
                XStream xstream = new XStream();
                TransformerFactory transFactory = TransformerFactory.newInstance();
                Transformer transformer = transFactory.newTransformer();
                StringWriter buffer = new StringWriter();
                transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
                transformer.transform(new DOMSource(clientElement), new StreamResult(buffer));
                String str = buffer.toString();
                //           DOMImplementationLS domImplLS = (DOMImplementationLS) document.getImplementation();
                //           LSSerializer serializer = domImplLS.createLSSerializer();
                //           String str = serializer.writeToString(clientElement);
                this.saveciteClients = (Map<String, List<Map<String, String>>>) xstream.fromXML(str);
            } catch (Exception e) {
                m_log.warn("Exception trying to read saveciteClients from config XML", e);
            }
        }

    }

    /**
     * Lookup and save one dynamic configuration parameter
     * @param Configuration XML
     * @param parameterMap Parameter name=value pairs
     * @param name Parameter name
     */
    protected void saveParameter(org.w3c.dom.Document document, Map parameterMap, String name) {
        String value;

        if ((value = getText(document, name)) != null) {
            parameterMap.put(name, value);
        }
    }

    /*
     * XML helpers
     */

    /**
     * Parse an XML resource
     * @param filename The filename (or URI) to parse
     * @return DOM Document (null if parse fails)
     */
    protected Document parseXmlFromStream(InputStream stream) {
        try {
            DocumentBuilder documentBuilder = getXmlDocumentBuilder();

            if (documentBuilder != null) {
                return documentBuilder.parse(stream);
            }
        } catch (Exception exception) {
            m_log.warn("XML parse on \"" + stream + "\" failed: " + exception);
        }
        return null;
    }

    /**
     * Get a DOM Document builder.
     * @return The DocumentBuilder
     * @throws DomException
     */
    protected DocumentBuilder getXmlDocumentBuilder() {
        try {
            DocumentBuilderFactory factory;

            factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(false);
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

            return factory.newDocumentBuilder();
        } catch (Exception exception) {
            m_log.warn("Failed to get XML DocumentBuilder: " + exception);
        }
        return null;
    }

    /**
     * "Normalize" XML text node content to create a simple string
     * @param original Original text
     * @param update Text to add to the original string (a space separates the two)
     * @return Concatenated contents (trimmed)
     */
    protected String normalizeText(String original, String update) {
        StringBuilder result;

        if (original == null) {
            return (update == null) ? "" : update.trim();
        }

        result = new StringBuilder(original.trim());
        result.append(' ');
        result.append(update.trim());

        return result.toString();
    }

    /**
     * Get the text associated with this element
     * @param root The document containing the text element
     * @return Text (trimmed of leading/trailing whitespace, null if none)
     */
    protected String getText(Document root, String elementName) {
        return getText(root.getDocumentElement(), elementName);
    }

    /**
     * Get the text associated with this element
     * @param root The root node of the text element
     * @return Text (trimmed of leading/trailing whitespace, null if none)
     */
    protected String getText(Element root, String elementName) {
        NodeList nodeList;
        Node parent;
        String text;

        nodeList = root.getElementsByTagName(elementName);
        if (nodeList.getLength() == 0) {
            return null;
        }

        text = null;
        parent = (Element) nodeList.item(0);

        for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
            switch (child.getNodeType()) {
            case Node.TEXT_NODE:
                text = normalizeText(text, child.getNodeValue());
                break;

            default:
                break;
            }
        }
        return text == null ? text : text.trim();
    }

    /*
     * Inititialize and destroy
     */
    public void init() {
        m_log.info("init()");

        EventTrackingService.addObserver(this);

        SiteService siteService = (SiteService) ComponentManager.get(SiteService.class);
        ContentHostingService contentService = (ContentHostingService) ComponentManager
                .get(ContentHostingService.class);

        m_log.info("init() site m_adminSiteName == \"" + this.m_adminSiteName + "\"");
        if (isNull(this.m_adminSiteName)) {
            // can't create
            m_log.info("init() m_adminSiteName is null");
        } else if (siteService.siteExists(this.m_adminSiteName)) {
            // no need to create
            m_log.info("init() site " + this.m_adminSiteName + " already exists");
        } else if (!m_serverConfigurationService.getBoolean("citationsAdmin.autocreate", true)) {
            //do not autocreate
            m_log.info("init() skipping autocreate of citationsAdmin site");
        } else {
            SecurityAdvisor pushed = null;
            // need to create
            try {
                pushed = enableSecurityAdvisor();
                Session s = m_sessionManager.getCurrentSession();
                s.setUserId(UserDirectoryService.ADMIN_ID);

                Site adminSite = siteService.addSite(this.m_adminSiteName, "project");
                adminSite.setTitle("Citations Admin");
                adminSite.setPublished(true);
                adminSite.setJoinable(false);

                // add Resources tool
                SitePage page = adminSite.addPage();
                page.setTitle("Resources");
                page.addTool("sakai.resources");

                siteService.save(adminSite);
                m_log.debug("init() site " + this.m_adminSiteName + " has been created");
            } catch (IdInvalidException e) {
                // TODO Auto-generated catch block
                m_log.warn("IdInvalidException ", e);
            } catch (IdUsedException e) {
                // we've already verified that the site doesn't exist but
                // this can occur if site was created by another server
                // in a cluster that is starting up at the same time.
                m_log.warn("IdUsedException ", e);
            } catch (PermissionException e) {
                // TODO Auto-generated catch block
                m_log.warn("PermissionException ", e);
            } catch (IdUnusedException e) {
                // TODO Auto-generated catch block
                m_log.warn("IdUnusedException ", e);
            } finally {
                if (pushed != null) {
                    boolean found = false;
                    while (SecurityService.hasAdvisors() && !found) {
                        SecurityAdvisor popped = SecurityService.popAdvisor();
                        found = pushed == popped;
                    }
                }
            }
        }

        for (String config : this.m_configs) {
            String configFileRef = this.getConfigFolderReference() + config;

            updateConfig(configFileRef);
        }
    }

    public void destroy() {
        m_log.info("destroy()");
    }

    /*
     * Getters/setters for components.xml parameters
     */

    /**
     * @return the OSID package name
     */
    public String getOsidImpl() {
        return m_osidImpl;
    }

    /**
     * @param osidImpl the OSID package name
     */
    public void setOsidImpl(String osidImpl) {
        m_osidImpl = osidImpl;
    }

    /**
     * @return the extended Repository ID
     */
    public String getExtendedRepositoryId() {
        return m_extendedRepositoryId;
    }

    /**
     * @param extendedRepositoryId the extended Repository ID
     */
    public void setExtendedRepositoryId(String extendedRepositoryId) {
        m_extendedRepositoryId = extendedRepositoryId;
    }

    /**
     * @return the m_metasearchUsername
     */
    public String getMetasearchUsername() {
        return m_metasearchUsername;
    }

    /**
     * @param username the m_metasearchUsername to set
     */
    public void setMetasearchUsername(String username) {
        m_metasearchUsername = username;
    }

    /**
     * @return the m_metasearchBaseUrl
     */
    public String getMetasearchBaseUrl() {
        return m_metasearchBaseUrl;
    }

    /**
     * @param baseUrl the m_metasearchBaseUrl to set
     */
    public void setMetasearchBaseUrl(String baseUrl) {
        m_metasearchBaseUrl = baseUrl;
    }

    /**
     * @return the m_metasearchPassword
     */
    public String getMetasearchPassword() {
        return m_metasearchPassword;
    }

    /**
     * @param password the m_metasearchPassword to set
     */
    public void setMetasearchPassword(String password) {
        m_metasearchPassword = password;
    }

    /**
     * @return the Google base URL
     */
    public String getGoogleBaseUrl() {
        return m_googleBaseUrl;
    }

    /**
     * @param googleBaseUrl the base URL to set
     */
    public void setGoogleBaseUrl(String googleBaseUrl) {
        m_googleBaseUrl = googleBaseUrl;
    }

    /**
     * @return the sakaiServerKey
     */
    public String getSakaiServerKey() {
        return m_sakaiServerKey;
    }

    /**
     * @param sakaiServerKey the sakaiServerKey to set
     */
    public void setSakaiServerKey(String sakaiId) {
        m_sakaiServerKey = sakaiId;
    }

    /**
     * @return the OpenURL label
     */
    public String getOpenUrlLabel() {
        return m_openUrlLabel;
    }

    /**
     * @param set the OpenURL label
     */
    public void setOpenUrlLabel(String openUrlLabel) {
        m_openUrlLabel = openUrlLabel;
    }

    /**
     * @return the OpenURL resolver address
     */
    public String getOpenUrlResolverAddress() {
        return m_openUrlResolverAddress;
    }

    /**
     * @param set the OpenURL resolver address
     */
    public void setOpenUrlResolverAddress(String openUrlResolverAddress) {
        m_openUrlResolverAddress = openUrlResolverAddress;
    }

    /**
     * @return the database hierarchy XML filename/URI
     */
    public String getDatabaseXml() {
        return m_databaseXml;
    }

    /**
     * @param set the database hierarchy XML filename/URI
     */
    public void setDatabaseXml(String databaseXml) {
        m_databaseXml = databaseXml;
    }

    /**
     * @return the configuration XML filename/URI
     */
    public String getSiteConfigXml() {
        return m_siteConfigXml;
    }

    /**
     * @param set the configuration XML filename/URI
     */
    public void setSiteConfigXml(String siteConfigXml) {
        m_siteConfigXml = siteConfigXml;
    }

    /**
     * @return the serverConfigurationService
     */
    public ServerConfigurationService getServerConfigurationService() {
        return m_serverConfigurationService;
    }

    /**
     * @param serverConfigurationService the serverConfigurationService to set
     */
    public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) {
        m_serverConfigurationService = serverConfigurationService;
    }

    /**
     * @param sessionManager the SessionManager to save
     */
    public void setSessionManager(SessionManager sessionManager) {
        m_sessionManager = sessionManager;
    }

    /**
     * @return the SessionManager
     */
    public SessionManager getSessionManager() {
        return m_sessionManager;
    }

    /**
     * @return the site specific "OSID configuration" package name
     */
    public String getOsidConfig() {
        return m_osidConfig;
    }

    /**
     * @param osidConfig the site specific "OSID configuration" package name
     */
    public void setOsidConfig(String osidConfig) {
        m_osidConfig = osidConfig;
    }

    /**
     * @return Google search support status
     */
    public String getGoogleSearchEnabled() {
        return m_googleSearchEnabled;
    }

    /**
     * @param googleSearchEnabled ("true" or "false")
     */
    public void setGoogleSearchEnabled(String googleSearchEnabled) {
        if (googleSearchEnabled.equalsIgnoreCase("true") || googleSearchEnabled.equalsIgnoreCase("false")) {
            m_googleSearchEnabled = googleSearchEnabled;
            return;
        }

        m_log.warn("Invalid Google support setting \"" + googleSearchEnabled + "\", disabling Google search");

        m_googleSearchEnabled = "false";
    }

    /**
     * @return library search support status
     */
    public String getLibrarySearchEnabled() {
        return m_librarySearchEnabled;
    }

    /**
     * @param librarySearchEnabled ("true" or "false")
     */
    public void setLibrarySearchEnabled(String librarySearchEnabled) {
        if (librarySearchEnabled.equalsIgnoreCase("true") || librarySearchEnabled.equalsIgnoreCase("false")) {
            m_librarySearchEnabled = librarySearchEnabled;
            return;
        }

        m_log.warn("Invalid library support setting \"" + librarySearchEnabled + "\", disabling library search");

        m_librarySearchEnabled = "false";
    }

    /**
     * @return the adminSiteName
     */
    public String getAdminSiteName() {
        return m_adminSiteName;
    }

    /**
     * @param adminSiteName the adminSiteName to set
     */
    public void setAdminSiteName(String adminSiteName) {
        this.m_adminSiteName = adminSiteName;
    }

    /**
     * @return the configFolder
     */
    public String getConfigFolder() {
        return m_configFolder;
    }

    /**
     * @param configFolder the configFolder to set
     */
    public void setConfigFolder(String configFolder) {
        this.m_configFolder = configFolder;
    }

    /**
     * @return the configFile
     */
    public String getConfigXmlCache() {
        StringBuilder buf = new StringBuilder();

        for (Iterator<String> it = this.m_configs.iterator(); it.hasNext();) {
            String str = it.next();
            buf.append(str);
            if (it.hasNext()) {
                buf.append(',');
            }
        }
        return buf.toString();
    }

    /**
     * @param configFile the configFile to set
     */
    public void setConfigXmlCache(String configXml) {
        this.m_configs = new TreeSet<String>();

        if (!isNull(configXml)) {
            String[] configs = configXml.split("\\s*,\\s*");
            for (String config : configs) {
                this.m_configs.add(config);
            }
        }
    }

    /**
     * @return the categoriesXml resource names
     */
    public String getDatabaseXmlCache() {
        StringBuilder buf = new StringBuilder();

        for (Iterator<String> it = this.m_categories.iterator(); it.hasNext();) {
            String str = it.next();

            buf.append(str);
            if (it.hasNext()) {
                buf.append(',');
            }
        }
        return buf.toString();
    }

    /**
     * @param categoriesXml the categoriesXml to set
     */
    public void setDatabaseXmlCache(String categoriesXml) {
        this.m_categories = new TreeSet<String>();

        if (!isNull(categoriesXml)) {
            String[] categories = categoriesXml.split("\\s*,\\s*");
            for (String category : categories) {
                this.m_categories.add(category);
            }
        }
    }

    /**
     * @return the saveciteClients
     */
    public Map<String, List<Map<String, String>>> getSaveciteClients() {
        return saveciteClients;
    }

    /**
     * @param saveciteClients the saveciteClients to set
     */
    public void setSaveciteClients(Map<String, List<Map<String, String>>> saveciteClients) {
        m_log.info("saveciteClients updated");
        this.saveciteClients = saveciteClients;
        if (m_log.isDebugEnabled()) {
            if (this.saveciteClients == null) {
                m_log.debug("setSaveciteClients() called but saveciteClients is null");
                return;
            }
            StringBuilder buf = new StringBuilder("setSaveciteClients()\n");
            buf.append('\n');
            buf.append('\n');
            addMapToStringBuilder(buf, this.saveciteClients, 4, 4);
            buf.append('\n');
            buf.append('\n');
            m_log.debug(buf.toString());
        }
    }

    public List<Map<String, String>> getSaveciteClientsForLocale(Locale locale) {
        List<Map<String, String>> clients = null;
        if (this.saveciteClients == null || this.saveciteClients.isEmpty()) {
            clients = new ArrayList<Map<String, String>>();
        } else if (this.saveciteClients.containsKey(locale.toString())) {
            clients = this.saveciteClients.get(locale.toString());
        } else if (this.saveciteClients.containsKey(locale.getLanguage() + "_" + locale.getCountry())) {
            clients = this.saveciteClients.get(locale.getLanguage() + "_" + locale.getCountry());
        } else if (this.saveciteClients.containsKey(locale.getLanguage())) {
            clients = this.saveciteClients.get(locale.getLanguage());
        } else if (this.saveciteClients.containsKey(SERVER_DEFAULT_LOCALE)) {
            clients = this.saveciteClients.get(SERVER_DEFAULT_LOCALE);
        } else {
            clients = new ArrayList<Map<String, String>>();
        }
        return clients;
    }

    protected void addMapToStringBuilder(StringBuilder buf, Map map, int indent, int indentIncrement) {

        for (Entry<String, Object> entry : ((Map<String, Object>) map).entrySet()) {
            String key = entry.getKey();
            for (int i = 0; i < indent; i++) {
                buf.append(' ');
            }
            buf.append(key);
            Object val = entry.getValue();
            if (val instanceof Map) {
                buf.append('\n');
                addMapToStringBuilder(buf, (Map) val, indent + indentIncrement, indentIncrement);
            } else if (val instanceof List) {
                addListToStringBuilder(buf, (List) val, indent + indentIncrement, indentIncrement);
            } else {
                buf.append(" == ");
                buf.append(val);
                buf.append('\n');
            }
        }

    }

    protected void addListToStringBuilder(StringBuilder buf, List list, int indent, int indentIncrement) {

        buf.append('\n');
        for (Object val : list) {
            if (val instanceof Map) {
                addMapToStringBuilder(buf, (Map) val, indent + indentIncrement, indentIncrement);
            } else if (val instanceof List) {
                for (int i = 0; i < indent; i++) {
                    buf.append(' ');
                }
                buf.append("----------\n");
                for (int i = 0; i < indent; i++) {
                    buf.append(' ');
                }
                addListToStringBuilder(buf, (List) val, indent + indentIncrement, indentIncrement);
            } else {
                for (int i = 0; i < indent; i++) {
                    buf.append(' ');
                }
                buf.append(val);
                buf.append('\n');
            }
        }

    }

    public Collection<String> getAllCategoryXml() {
        return new TreeSet<String>(this.m_categories);
    }

    /*
     * Configuration update
     */

    /**
     * Called when an observed object chnages (@see java.util.Observer#update)
     * @param arg0 - The observed object
     * @param arg1 - Event argument
     */
    public void update(Observable arg0, Object arg1) {
        if (arg1 instanceof Event) {
            Event event = (Event) arg1;
            /*
             * Modified?  If so (and this is our file) update the the configuration
             */
            if (event.getModify()) {
                String refstr = event.getResource();

                if (this.m_updatableResources.contains(refstr)) {
                    m_log.debug("Updating configuration from " + refstr);
                    updateConfig(refstr);
                }
            }
        }
    }

    /**
     * Update configuration data from an XML resource
     * @param configFileRef XML configuration reference (/content/...)
     */
    protected void updateConfig(String configFileRef) {
        Reference ref = EntityManager.newReference(configFileRef);

        SecurityAdvisor pushed = null;
        if (ref != null) {
            try {
                ContentResource resource;
                /*
                 * Fetch configuration details from our XML resource
                 */
                pushed = enableSecurityAdvisor();

                resource = org.sakaiproject.content.cover.ContentHostingService.getResource(ref.getId());
                if (resource != null) {
                    populateConfig(configFileRef, resource.streamContent());
                }
            } catch (PermissionException e) {
                m_log.warn("Exception: " + e + ", continuing");
            } catch (IdUnusedException e) {
                m_log.warn("Citations configuration XML is missing (" + configFileRef
                        + "); Citations ConfigurationService will watch for its creation");
            } catch (TypeException e) {
                m_log.warn("Exception: " + e + ", continuing");
            } catch (ServerOverloadException e) {
                m_log.warn("Exception: " + e + ", continuing");
            } finally {
                if (pushed != null) {
                    boolean found = false;
                    while (SecurityService.hasAdvisors() && !found) {
                        SecurityAdvisor popped = SecurityService.popAdvisor();
                        found = popped == pushed;
                    }
                }
            }

        }
        /*
         * Always add our reference to the list of observed resources
         */
        m_updatableResources.add(configFileRef);
    }

    /*
     * Miscellanous helpers
     */

    /**
     * Does the specified configuration/hierarchy resource exist?
     * @param resourceName Resource name
     * @return true If we can access this content
     */
    private boolean exists(String resourceName) {
        SecurityAdvisor pushed = null;
        try {
            String configFolderRef = getConfigFolderReference();

            if (!isNull(configFolderRef) && !isNull(resourceName)) {
                String referenceName = configFolderRef + resourceName;

                Reference reference = EntityManager.newReference(referenceName);
                if (reference == null)
                    return false;

                pushed = enableSecurityAdvisor();
                ContentResource resource = org.sakaiproject.content.cover.ContentHostingService
                        .getResource(reference.getId());

                return (resource != null);
            }
        } catch (IdUnusedException exception) {
            m_log.debug("exists() failed find resource: " + exception);
        } catch (Exception exception) {
            m_log.warn("exists() failed find resource: ", exception);
        } finally {
            if (pushed != null) {
                boolean found = false;
                while (SecurityService.hasAdvisors() && !found) {
                    SecurityAdvisor popped = SecurityService.popAdvisor();
                    found = popped == pushed;
                }
            }
        }
        return false;
    }

    /**
     * Establish a security advisor to allow the "embedded" azg work to occur
     * with no need for additional security permissions.
     * @return the advisor
     */
    protected SecurityAdvisor enableSecurityAdvisor() {
        SecurityAdvisor advisor = new SecurityAdvisor() {
            public SecurityAdvice isAllowed(String userId, String function, String reference) {
                return SecurityAdvice.ALLOWED;
            }
        };
        // put in a security advisor so we can create citationAdmin site without need
        // of further permissions
        SecurityService.pushAdvisor(advisor);
        return advisor;
    }

    /**
     * Null (or empty) String?
     * @param string String to check
     * @return true if so
     */
    private boolean isNull(String string) {
        return (string == null) || (string.trim().equals(""));
    }

    /**
     * Is library search enabled for the current user?
     * @return true if so
     */
    public boolean librarySearchEnabled() {
        return isLibrarySearchEnabled() && isConfigurationXmlAvailable() && isDatabaseHierarchyXmlAvailable();
    }

    public void setExternalSearchEnabled(boolean state) {
        this.m_externalSearchEnabled = state;
    }

    public boolean isExternalSerarchEnabled() {
        boolean enabled = m_externalSearchEnabled;
        String state = getConfigurationParameter("external-search-enabled");

        if (state != null) {
            enabled = "true".equals(state);
        }

        m_log.debug("External Search enabled: " + enabled);
        return enabled && getExternalSearchUrl() != null;
    }

    public void setExternalSearchUrl(String url) {
        this.m_externalSearchUrl = url;
    }

    public String getExternalSearchUrl() {
        String url = getConfigurationParameter("external-search-url");
        if (url == null) {
            url = m_externalSearchUrl;
        }
        return url;
    }

    /*
     * (non-Javadoc)
     * @see org.sakaiproject.citation.api.ConfigurationService#getSecondsBetweenSaveciteRefreshes()
     */
    public String getSecondsBetweenSaveciteRefreshes() {
        String secondsBetweenRefreshes = this.getConfigurationParameter(PARAM_SECONDS_BETWEEN_SAVECITE_REFRESHES);
        if (secondsBetweenRefreshes == null) {
            // no stored value for secondsBetweenRefreshes; use default
            secondsBetweenRefreshes = DEFAULT_SECONDS_BETWEEN_SAVECITE_REFRESHES;
        } else {
            try {
                int num = Integer.parseInt(secondsBetweenRefreshes);
                if (num < 1) {
                    // stored value of secondsBetweenRefreshes is too small; use default
                    secondsBetweenRefreshes = DEFAULT_SECONDS_BETWEEN_SAVECITE_REFRESHES;
                } else if (num > MAXIMUM_SECONDS_BETWEEN_SAVECITE_REFRESHES) {
                    // stored value of secondsBetweenRefreshes is too big; use max
                    secondsBetweenRefreshes = Integer.toString(MAXIMUM_SECONDS_BETWEEN_SAVECITE_REFRESHES);
                }
            } catch (NumberFormatException e) {
                // stored value of secondsBetweenRefreshes is not a number; use default
                secondsBetweenRefreshes = DEFAULT_SECONDS_BETWEEN_SAVECITE_REFRESHES;
            }
        }
        return secondsBetweenRefreshes;
    }
}