org.infoglue.deliver.externalsearch.ExternalSearchManager.java Source code

Java tutorial

Introduction

Here is the source code for org.infoglue.deliver.externalsearch.ExternalSearchManager.java

Source

/* ===============================================================================
 *
 * Part of the InfoGlue Content Management Platform (www.infoglue.org)
 *
 * ===============================================================================
 *
 *  Copyright (C)
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License version 2, as published by the
 * Free Software Foundation. See the file LICENSE.html for more information.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY, including the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc. / 59 Temple
 * Place, Suite 330 / Boston, MA 02111-1307 / USA.
 *
 * ===============================================================================
 */

package org.infoglue.deliver.externalsearch;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.infoglue.cms.exception.ConfigurationError;
import org.infoglue.cms.util.CmsPropertyHandler;
import org.infoglue.deliver.util.CacheNotificationCenter;
import org.infoglue.deliver.util.CacheNotificationListener;
import org.infoglue.deliver.util.ThreadedQueueCacheNotificationListener;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;

/**
 * <p>The ExternalSearchManager class is a singleton responsible for handling all {@link ExternalSearchService} in the system.
 * The manager has three responsibilities:
 * </p>
 * <ul>
 *   <li>Provide a way to get access to each search service (through its {@link #getService(String)} method).</li>
 *   <li>Notify each service about updates in the external search configuration.</li>
 *   <li>Provide the hearth beat loop for the services index updating.</li>
 * </ul>
 * 
 * <p>Under normal circumstances the only method that should be of interest is the <em>getService(String)</em> method.
 * The rest of class's API is used by other parts of the External seach architecture.</p>
 * 
 * @author Erik Stenbcka
 */
public class ExternalSearchManager extends ThreadedQueueCacheNotificationListener
        implements Runnable, CacheNotificationListener {
    private static final Logger logger = Logger.getLogger(ExternalSearchManager.class);
    private Gson configParser;
    private Map<String, ExternalSearchService> services;
    private boolean stopped;

    private static ExternalSearchManager manager;

    /** 
     * Used for testing.
     */
    public static void injectManager(ExternalSearchManager fakeManager) {
        manager = fakeManager;
    }

    private synchronized static void initManager() {
        if (manager == null) {
            manager = new ExternalSearchManager();
            Thread thread = new Thread(manager);
            thread.setName("ExternalSearchManager");
            thread.start();
        }
    }

    /**
     * Gets a reference to the manager singleton. If this is the first time
     * the manager is accessed the singleton is created and the hearth beat
     * loop is started.
     * @return The external search manager singleton instance.
     */
    public static ExternalSearchManager getManager() {
        if (manager == null) {
            initManager();
        }
        return manager;
    }

    //////////////////////////////////////////////////////////////////////
    // Instance methods

    public ExternalSearchManager() {
        this.services = new HashMap<String, ExternalSearchService>();
        this.stopped = false;
        CacheNotificationCenter.getCenter().addListener(this);
    }

    private void initGSon() {
        final Type configType = new TypeToken<Map<String, Object>>() {
        }.getType();
        class DelegateDeserializer<T extends ExternalSearchDelegate> implements JsonDeserializer<T> {
            @Override
            public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                    throws JsonParseException {
                try {
                    JsonObject obj = (JsonObject) json;
                    Class<?> clazz = Class.forName(obj.get("class").getAsString());
                    @SuppressWarnings("unchecked")
                    T delegate = (T) clazz.newInstance();

                    if (obj.has("config")) {
                        Map<String, Object> config = context.deserialize(obj.get("config"), configType);
                        delegate.setConfig(config);
                    }

                    return delegate;
                } catch (Exception ex) {
                    throw new JsonParseException(
                            "Failed to deserialize element. Exception message: " + ex.getMessage(), ex);
                }
            }

        }
        class DelegateConfigDeserializer implements JsonDeserializer<Map<String, Object>> {
            @Override
            public Map<String, Object> deserialize(JsonElement json, Type typeOfT,
                    JsonDeserializationContext context) throws JsonParseException {
                try {
                    Map<String, Object> config = new HashMap<String, Object>();
                    JsonObject configObject = json.getAsJsonObject();
                    for (Map.Entry<String, JsonElement> configEntry : configObject.entrySet()) {
                        if (configEntry.getValue().isJsonObject()) {
                            config.put(configEntry.getKey(),
                                    context.deserialize(configEntry.getValue(), configType));
                        } else if (configEntry.getValue().isJsonPrimitive()
                                && ((JsonPrimitive) configEntry.getValue()).isString()) {
                            config.put(configEntry.getKey(), configEntry.getValue().getAsString());
                        }
                    }

                    return config;
                } catch (Exception ex) {
                    throw new JsonParseException(
                            "Failed to deserialize element. Exception message: " + ex.getMessage(), ex);
                }
            }

        }

        GsonBuilder gson = new GsonBuilder();

        Type fieldsType = new TypeToken<Map<String, IndexableField>>() {
        }.getType();

        gson.registerTypeAdapter(fieldsType, new IndexableField.Deserializer());
        gson.registerTypeAdapter(configType, new DelegateConfigDeserializer());
        gson.registerTypeAdapter(DataRetriever.class, new DelegateDeserializer<DataRetriever>());
        gson.registerTypeAdapter(Parser.class, new DelegateDeserializer<Parser>());
        gson.registerTypeAdapter(Indexer.class, new DelegateDeserializer<Indexer>());

        configParser = gson.create();
    }

    /**
     * Attempts to parse the given String as a external search service configurations array. See general documentation
     * of {@link ExternalSearchService} for an explanation of the configuration syntax.
     * 
     * @param configsString A String representation of an external search service configuration array.
     * @return A list of external search service configurations based on the given configurations string
     * @throws ConfigurationError Thrown if the JSON-parsing of the input fails.
     */
    protected List<ExternalSearchServiceConfig> parseConfiguartions(String configsString)
            throws ConfigurationError {
        if (configParser == null) {
            initGSon();
        }

        Type configListType = new TypeToken<List<ExternalSearchServiceConfig>>() {
        }.getType();

        try {
            List<ExternalSearchServiceConfig> result = configParser.fromJson(configsString, configListType);
            if (result == null) {
                return new ArrayList<ExternalSearchServiceConfig>();
            } else {
                return result;
            }
        } catch (JsonSyntaxException jsex) {
            throw new ConfigurationError("The JSON parsing library threw an exception.", jsex);
        }
    }

    private boolean isValidConfig(ExternalSearchServiceConfig config) {
        return config.getName() != null && !config.getName().equals("") && config.getDataRetriever() != null
                && config.getParser() != null && config.getIndexer() != null;
    }

    /**
     * <p>Converts the given String into service configurations and create, delete and notifies
     * services about the change of configuration. The method does not know about current configurations
     * and can therefore not determine if anything has changed in the configurations. It is up to each service
     * to determine if it needs to update its configuration.</p>
     * 
     * <p>If the given String cannot be parsed as a list of configurations the method will return without notifying
     * the services. Passing null as the argument has the same effect.</p>
     * 
     * <p>If a configuration object has a name that does not match any current service a new service it initiated
     * based on the configuration. Similarly if an existing service does not match any of the configurations
     * that service will be removed. Otherwise the service with a name matching the configurations is notified
     * of the new configuration object by a call to its {@link ExternalSearchService#setConfig(ExternalSearchServiceConfig)}.</p>
     * 
     * @param configsString A String representation of an external search service configuration array.
     */
    protected void updateConfigurations(String configsString) {
        if (configsString == null) {
            logger.info("No configuration specified for external search services");
        } else {
            List<ExternalSearchServiceConfig> configs = null;
            try {
                configs = parseConfiguartions(configsString);
            } catch (ConfigurationError cex) {
                logger.error("Failed to parse external search configs. Message: " + cex.getMessage());
                logger.warn("Failed to parse external search configs.", cex);
            }

            if (configs != null) {
                synchronized (services) {
                    // Add added new services and update existing
                    for (ExternalSearchServiceConfig config : configs) {
                        if (!isValidConfig(config)) {
                            logger.warn("Config was invalid will not apply to service. Config: " + config);
                            continue;
                        }

                        if (!services.containsKey(config.getName())) {
                            initService(config);
                        } else {
                            logger.debug("Updating config for service. Name: " + config.getName());
                            services.get(config.getName()).setConfig(config);
                        }
                    }

                    // Remove removed services
                    Iterator<String> serviceNameIterator = services.keySet().iterator();
                    String serviceName;
                    serviceLoop: while (serviceNameIterator.hasNext()) {
                        serviceName = serviceNameIterator.next();
                        for (ExternalSearchServiceConfig config : configs) {
                            if (config.getName() != null && serviceName.equals(config.getName())) {
                                continue serviceLoop;
                            }
                        }
                        services.get(serviceName).destroyService();
                        serviceNameIterator.remove();
                    }
                }
            }
        }
    }

    /**
     * Support method for initializing new services. The new service is added to list
     * if available services.
     * @param config The configuration the new service should be based on.
     */
    protected void initService(ExternalSearchServiceConfig config) {
        logger.info("Initing new service. Name: " + config.getName());
        ExternalSearchService newService = new ExternalSearchService(config);
        services.put(config.getName(), newService);
    }

    /**
     * Initiates an update of the external search configuration. The configuration
     * is read from the <em>CmsPropertyHandler</em>. This method is called automatically
     * by the system whenever the application is notified of a change in the ServerNodeProperties.
     */
    public void updateConfigurations() {
        String configsString = CmsPropertyHandler.getExternalSearchServiceConfigs();

        updateConfigurations(configsString);
    }

    /**
     * Returns a list of all available services. The returned values are not deep copies so operations
     * done on the services will affect the managed services.
     * @return A list of all the current services.
     */
    public Collection<ExternalSearchService> getAllServices() {
        return services.values();
    }

    /**
     * Attempts to get a service with the given <em>serviceName</em>.  If the service does not exist
     * null is returned.
     * 
     * @param serviceName The service to look for
     * @return The service, if found.
     */
    public ExternalSearchService getService(String serviceName) {
        return services.get(serviceName);
    }

    /**
     * Halts the hearth beat loop of the manager. When the manager is stopped the services
     * will still be searchable but they will never have their indexes updated again. If the manager
     * is in the process of notifying the services it will finish that task before stopping.
     */
    public void stopServices() {
        this.stopped = true;
    }

    /**
     * Indicates if the manager has stopped the hearth beat loop.
     * @return True if the manager has stopped, false otherwise.
     */
    public boolean isStopped() {
        return this.stopped;
    }

    //////////////////////////////////////////////////////////////////////
    //  Runnable

    @Override
    public void run() {
        updateConfigurations();

        while (!stopped) {
            try {
                logger.info("Will go through external search services");

                synchronized (services) {
                    for (final ExternalSearchService service : services.values()) {
                        logger.debug("Requesting service indexing start. Service: " + service);
                        service.startIndexing();
                    }
                }

                logger.info("Did go through external search services");

                try {
                    Thread.sleep(30000);
                } catch (InterruptedException iex) {
                }
            } catch (Throwable tr) {
                logger.error("Error in external search manager loop. Message: " + tr.getMessage() + ". Type: "
                        + tr.getClass());
                logger.warn("Error in external search manager loop.", tr);
            }
        }
        logger.warn("The external search manager was stopped!");
    }

    @Override
    protected void handleNotification(String className) {
        if (className.equals("ServerNodeProperties")) {
            logger.info("Received server node properties change");
            updateConfigurations();
        }
    }
}