// Copyright 2006 Google Inc. // // Licensed under the Apache 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.apache.org/licenses/LICENSE-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 com.google.enterprise.connector.instantiator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.enterprise.connector.common.PropertiesException; import com.google.enterprise.connector.common.PropertiesUtils; import com.google.enterprise.connector.manager.Context; import com.google.enterprise.connector.persist.PersistentStore; import com.google.enterprise.connector.persist.StoreContext; import com.google.enterprise.connector.scheduler.Schedule; import com.google.enterprise.connector.spi.Connector; import com.google.enterprise.connector.util.filter.DocumentFilterChain; import com.google.enterprise.connector.util.filter.DocumentFilterFactory; import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Map; import java.util.Properties; import java.util.logging.Logger; /** * Container for info about a Connector Instance. Instantiable only through a * static factory that uses Spring. */ final class InstanceInfo { private static final Logger LOGGER = Logger.getLogger(InstanceInfo.class.getName()); private static PersistentStore store; private final TypeInfo typeInfo; private final File connectorDir; private final String connectorName; private final StoreContext storeContext; private final Connector connector; private final DocumentFilterFactory documentFilterFactory; /** * Constructs a InstanceInfo with no backing Connector instance. * * @param connectorName the name of the Connector instance * @param connectorDir the Connector's working directory * @param typeInfo the Connector's prototype * @throws InstanceInfoException */ public InstanceInfo(String connectorName, File connectorDir, TypeInfo typeInfo) throws InstanceInfoException { this(connectorName, connectorDir, typeInfo, null, false); } /** * Constructs a new Connector instance based upon the supplied * configuration map. * * @param connectorName the name of the Connector instance * @param connectorDir the Connector's working directory * @param typeInfo the Connector's prototype * @param config connector Configuration * @throws InstanceInfoException */ public InstanceInfo(String connectorName, File connectorDir, TypeInfo typeInfo, Configuration config) throws InstanceInfoException { this(connectorName, connectorDir, typeInfo, config, true); } /** * Constructs a new Connector instance based upon the supplied * configuration map. * * @param connectorName the name of the Connector instance * @param connectorDir the Connector's working directory * @param typeInfo the Connector's prototype * @param config connector Configuration * @param createConnector if true, create the connector instance * @throws InstanceInfoException */ private InstanceInfo(String connectorName, File connectorDir, TypeInfo typeInfo, Configuration config, boolean createConnector) throws InstanceInfoException { if (connectorName == null || connectorName.length() < 1) { throw new NullConnectorNameException(); } if (connectorDir == null) { throw new NullDirectoryException(); } if (typeInfo == null) { throw new NullTypeInfoException(); } this.connectorName = connectorName; this.connectorDir = connectorDir; this.typeInfo = typeInfo; this.storeContext = new StoreContext(connectorName, typeInfo.getConnectorTypeName()); if (createConnector) { if (config == null) { throw new NullConfigurationException(); } DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); this.connector = makeConnectorWithSpring(connectorName, typeInfo, config, beanFactory); try { this.documentFilterFactory = getDocumentFilterFactory(beanFactory); if (this.documentFilterFactory != null) { LOGGER.config("Connector " + connectorName + " has document filters: " + this.documentFilterFactory.toString()); } } catch (BeansException e) { throw new InstanceInfoException("Failed to load document filters for" + " connector " + connectorName, e); } } else { this.connector = null; this.documentFilterFactory = null; } } /* **** Getters and Setters **** */ public static void setPersistentStore(PersistentStore store) { InstanceInfo.store = store; } /** * @return the connector */ Connector getConnector() { return connector; } /** * @return the name */ String getName() { return connectorName; } /** * @return the typeInfo */ TypeInfo getTypeInfo() { return typeInfo; } /** * @return the connectorDir */ File getConnectorDir() { return connectorDir; } /** * Construct a new Connector Instance based upon the connectorInstance * and connectorDefaults bean definitions. * * @param connectorName the name of the Connector instance. * @param typeInfo the Connector's prototype. * @param config connector Configuration. */ static Connector makeConnectorWithSpring(String connectorName, TypeInfo typeInfo, Configuration config) throws InstanceInfoException { return makeConnectorWithSpring(connectorName, typeInfo, config, new DefaultListableBeanFactory()); } /** * Construct a new Connector Instance based upon the connectorInstance * and connectorDefaults bean definitions. * * @param connectorName the name of the Connector instance. * @param typeInfo the Connector's prototype. * @param config connector Configuration. * @param factory DefaultListableBeanFactory used to create the connector. */ private static Connector makeConnectorWithSpring(String connectorName, TypeInfo typeInfo, Configuration config, DefaultListableBeanFactory factory) throws InstanceInfoException { String name = connectorName; Resource prototype = null; if (config.getXml() != null) { prototype = getByteArrayResource(config.getXml(), Charsets.UTF_8.name(), TypeInfo.CONNECTOR_INSTANCE_XML); } if (prototype == null) { prototype = typeInfo.getConnectorInstancePrototype(); } XmlBeanDefinitionReader beanReader = new XmlBeanDefinitionReader(factory); Resource defaults = typeInfo.getConnectorDefaultPrototype(); try { beanReader.loadBeanDefinitions(prototype); } catch (BeansException e) { throw new FactoryCreationFailureException(e, prototype, name); } // Seems non-intuitive to load these in this order, but we want newer // versions of the connectors to override any default bean definitions // specified in old-style monolithic connectorInstance.xml files. if (defaults != null) { try { beanReader.loadBeanDefinitions(defaults); } catch (BeansException e) { throw new FactoryCreationFailureException(e, defaults, name); } } Context context = Context.getInstance(); EncryptedPropertyPlaceholderConfigurer cfg = null; try { cfg = (EncryptedPropertyPlaceholderConfigurer) context.getBean( factory, null, EncryptedPropertyPlaceholderConfigurer.class); } catch (BeansException e) { throw new BeanInstantiationFailureException(e, prototype, name, EncryptedPropertyPlaceholderConfigurer.class.getName()); } if (cfg == null) { cfg = new EncryptedPropertyPlaceholderConfigurer(); } try { cfg.setLocation(getPropertiesResource(name, config.getMap())); cfg.postProcessBeanFactory(factory); } catch (BeansException e) { throw new PropertyProcessingFailureException(e, prototype, name); } Connector connector = null; try { connector = (Connector) context.getBean(factory, null, Connector.class); } catch (BeansException e) { throw new BeanInstantiationFailureException(e, prototype, name, Connector.class.getName()); } if (connector == null) { throw new NoBeansFoundException(prototype, name, Connector.class); } return connector; } /** * Return a Spring Resource containing the InstanceInfo * configuration Properties. */ @VisibleForTesting static Resource getPropertiesResource(String connectorName, Map<String, String> configMap) throws InstanceInfoException { Properties properties = (configMap == null) ? new Properties() : PropertiesUtils.fromMap(configMap); try { return getByteArrayResource( PropertiesUtils.storeToString(properties, null), PropertiesUtils.PROPERTIES_ENCODING, connectorName + ".properties"); } catch (PropertiesException e) { throw new PropertyProcessingInternalFailureException(e, connectorName); } } /* * Wraps a string as a Spring resource. This function has two purposes: * 1. Convert the string to a byte array using the given encoding. * 2. Workaround a bug in Spring where Resource.getFilename() is required. * * We override ByteArrayResource.getFilename, because * org.springframework.core.io.support.PropertiesLoaderSupport.loadProperties() * tries to fetch the filename extension of the properties Resource * in an attempt to determine whether to parse the properties as XML or * traditional syntax. ByteArrayResource throws an exception when * getFilename() is called because there is no associated filename. * TODO: Remove this hack when Spring Framework SPR-5068 gets fixed: * http://jira.springframework.org/browse/SPR-5068 */ private static ByteArrayResource getByteArrayResource(String value, String encoding, final String filename) { byte[] byteArray; try { byteArray = value.getBytes(encoding); } catch (IOException e) { throw new AssertionError(e); } return new ByteArrayResource(byteArray) { public String getFilename() { return filename; } }; } /** * Looks for {@link DocumentFilterFactory} beans in the connector's * bean factory. * * @param beanFactory DefaultListableBeanFactory used to create the connector. * @return {@link DocumentFilterFactory} for the connector, or {@code null} * if the connector does not define a DocumentFilterFactory. */ private static DocumentFilterFactory getDocumentFilterFactory( DefaultListableBeanFactory beanFactory) throws BeansException { @SuppressWarnings("unchecked") Collection<DocumentFilterFactory> filters = beanFactory.getBeansOfType(DocumentFilterFactory.class).values(); if (filters == null || filters.size() == 0) { // No filters defined. return null; } else if (filters.size() == 1) { // If there is just one, return it. return filters.iterator().next(); } // More than one filter is defined. Look for a single DocumentFilterChain, // which hopefully encapsulates the rest. @SuppressWarnings("unchecked") Collection<DocumentFilterChain> chains = beanFactory.getBeansOfType(DocumentFilterChain.class).values(); if (chains == null || chains.size() == 0) { // No chains defined, so I'll make one. But the order of the filters // should be considered random. return new DocumentFilterChain(Lists.newArrayList(filters)); } else if (chains.size() == 1) { // If there is just one, return it. return chains.iterator().next(); } else { // More than one filter chain is defined??? I will allow it, but... return new DocumentFilterChain(Lists.newArrayList(chains)); } } /** * Returns a connector's {@link DocumentFilterFactory}. Connectors may define * a document filter specific to that connector instance. This filter will * be used in conjuction with the Connector Manager's document filter, and * will act as the source for the Connector Manager's document filter. * * @return {@link DocumentFilterFactory} for the connector */ public DocumentFilterFactory getDocumentFilterFactory() { return documentFilterFactory; } /** * Sets {@code GData} host for Connectors that want it. */ public void setGDataConfig(Map<String, String> gdataConfig) throws PropertyProcessingFailureException { try { PropertyAccessorFactory.forBeanPropertyAccess(connector) .setPropertyValues(new MutablePropertyValues(gdataConfig), true); } catch (BeansException be) { throw new PropertyProcessingFailureException(be, "GData Host", connectorName); } } /* **** Manage the Connector Instance Persistent data store. **** */ /** * Remove this Connector Instance's persistent store state. */ public void removeConnector() { store.removeConnectorState(storeContext); store.removeConnectorSchedule(storeContext); store.removeConnectorConfiguration(storeContext); } /** * Get the configuration data for this connector instance. * * @return the connector type specific configuration data, or {@code null} * if no configuration is stored */ public Configuration getConnectorConfiguration() { return store.getConnectorConfiguration(storeContext); } /** * Set the configuration data for this connector instance. * Writes the supplied configuration through to the persistent store. * * @param configuration the connector type specific configuration data, * or {@code null} to unset any existing configuration. */ public void setConnectorConfiguration(Configuration configuration) { if (configuration == null) { store.removeConnectorConfiguration(storeContext); } else { store.storeConnectorConfiguration(storeContext, configuration); } } /** * Sets the {@link Schedule} for this connector instance. * Writes the modified schedule through to the persistent store. * * @param connectorSchedule Schedule to store or null unset any existing * schedule. */ public void setConnectorSchedule(Schedule connectorSchedule) { if (connectorSchedule == null) { store.removeConnectorSchedule(storeContext); } else { store.storeConnectorSchedule(storeContext, connectorSchedule); } } /** * Gets the schedule for this connector instance. * * @return the Schedule, or null if there is no schedule. * for this connector */ public Schedule getConnectorSchedule() { return store.getConnectorSchedule(storeContext); } /** * Sets the remembered traversal state for this connector instance. * Writes the modified state through to the persistent store. * * @param connectorState String to store or null to erase any previously * saved traversal state. * @throws IllegalStateException if state store is disabled for this connector */ public void setConnectorState(String connectorState) { if (connectorState == null) { store.removeConnectorState(storeContext); } else { store.storeConnectorState(storeContext, connectorState); } } /** * Gets the remembered traversal state for this connector instance. * * @return the state, or null if no state has been stored for this connector * @throws IllegalStateException if state store is disabled for this connector */ public String getConnectorState() { return store.getConnectorState(storeContext); } /* **** InstanceInfoExceptions **** */ static class InstanceInfoException extends InstantiatorException { InstanceInfoException(String message, Throwable cause) { super(message, cause); } InstanceInfoException(String message) { super(message); } } static class NullConnectorNameException extends InstanceInfoException { NullConnectorNameException() { super("Attempt to instantiate a connector with a null or empty name"); } } static class NullDirectoryException extends InstanceInfoException { NullDirectoryException() { super("Attempt to instantiate a connector with a null directory"); } } static class NullTypeInfoException extends InstanceInfoException { NullTypeInfoException() { super("Attempt to instantiate a connector with a null TypeInfo"); } } static class NullConfigurationException extends InstanceInfoException { NullConfigurationException() { super("Attempt to instantiate a connector with a null Configuration"); } } static class FactoryCreationFailureException extends InstanceInfoException { FactoryCreationFailureException(Throwable cause, Resource prototype, String connectorName) { super("Spring factory creation failure for connector " + connectorName + " using resource " + prototype.getDescription(), cause); } } static class NoBeansFoundException extends InstanceInfoException { NoBeansFoundException(Resource prototype, String connectorName, Class<?> clazz) { super("No beans found of type " + clazz.getName() + " for connector " + connectorName + " using resource " + prototype.getDescription()); } } static class BeanInstantiationFailureException extends InstanceInfoException { BeanInstantiationFailureException(Throwable cause, Resource prototype, String connectorName, String beanName) { super("Spring failure while instantiating bean " + beanName + " for connector " + connectorName + " using resource " + prototype.getDescription(), cause); } } static class PropertyProcessingInternalFailureException extends InstanceInfoException { PropertyProcessingInternalFailureException(Throwable cause, String connectorName) { super("Spring internal failure while processing configuration properties" + " for connector " + connectorName, cause); } } static class PropertyProcessingFailureException extends InstanceInfoException { PropertyProcessingFailureException(Throwable cause, Resource prototype, String connectorName) { this(cause, prototype.getDescription(), connectorName); } PropertyProcessingFailureException(Throwable cause, String description, String connectorName) { super("Problem while processing configuration properties for connector " + connectorName + " using resource " + description, cause); } } }