// 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);
    }
  }
}

{'avg_line_length': 35.87956204379562, 'max_line_length': 81, 'alphanum_fraction': 0.7135082901027362, 'licenses': ['Apache-2.0'], 'repository_name': 'AnantLabs/google-enterprise-connector-manager', 'path': 'projects/connector-manager/source/java/com/google/enterprise/connector/instantiator/InstanceInfo.java', 'size': 19662, 'lang': 'Java'}