// 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.manager;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.enterprise.connector.common.PropertiesException;
import com.google.enterprise.connector.common.PropertiesUtils;
import com.google.enterprise.connector.instantiator.Instantiator;
import com.google.enterprise.connector.instantiator.InstantiatorException;
import com.google.enterprise.connector.instantiator.SpringInstantiator;
import com.google.enterprise.connector.instantiator.ThreadPool;
import com.google.enterprise.connector.pusher.GsaFeedConnection;
import com.google.enterprise.connector.scheduler.TraversalScheduler;
import com.google.enterprise.connector.spi.SimpleTraversalContext;
import com.google.enterprise.connector.spi.TraversalContext;
import com.google.enterprise.connector.traversal.ProductionTraversalContext;
import com.google.enterprise.connector.util.database.JdbcDatabase;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.web.context.support.XmlWebApplicationContext;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Static services for establishing the application context. This consists of
* configuration, instantiating singletons, start up, etc.
* This code supports two context types: servlet (as a web application within
* an application server) and standalone.
* When we run junit tests, we use a standalone context.
* Use the methods setStandaloneContext and setServletContext to select the
* context type.
* <p>
* Also the interface used for event publishing. Wraps the event publishing
* functionality of the established context.
*/
// TODO (jlacey): Context and ConnectorCoordinatorImpl are dangerously close
// to encountering deadlock issues, calling each other from synchronized
// methods. The most likely scenerio for deadlock would probably be when
// registering the CM with a new GSA. Be wary when adding additional
// synchronization to these classes.
public class Context {
public static final String GSA_FEED_PROTOCOL_PROPERTY_KEY =
"gsa.feed.protocol";
public static final String GSA_FEED_HOST_PROPERTY_KEY = "gsa.feed.host";
public static final String GSA_FEED_PORT_PROPERTY_KEY = "gsa.feed.port";
public static final String GSA_FEED_PORT_DEFAULT = "19900";
public static final String GSA_FEED_VALIDATE_CERTIFICATE_PROPERTY_KEY =
"gsa.feed.validateCertificate";
public static final String GSA_FEED_VALIDATE_CERTIFICATE_DEFAULT = "false";
/** Indicates that the HTTPS port has not been set or should not be used. */
public static final int GSA_FEED_SECURE_PORT_INVALID = -1;
public static final String GSA_FEED_SECURE_PORT_PROPERTY_KEY =
"gsa.feed.securePort";
public static final String GSA_FEED_SECURE_PORT_DEFAULT = "19902";
public static final String TEED_FEED_FILE_PROPERTY_KEY = "teedFeedFile";
public static final String MANAGER_LOCKED_PROPERTY_KEY = "manager.locked";
public static final String FEED_CONTENTURL_PREFIX_PROPERTY_KEY =
"feed.contenturl.prefix";
public static final String FEED_CONTENTURL_SERVLET = "/getDocumentContent";
public static final String FEED_DISABLE_INHERITED_ACLS =
"feed.disable.inherited.acls";
public static final String DEFAULT_JUNIT_CONTEXT_LOCATION =
"testdata/mocktestdata/applicationContext.xml";
public static final String DEFAULT_JUNIT_COMMON_DIR_PATH =
"testdata/mocktestdata/";
/**
* Id of the Spring Bean used to declare the order services are to be loaded.
*/
public static final String ORDERED_SERVICES_BEAN_NAME = "OrderedServices";
private static final String APPLICATION_CONTEXT_PROPERTIES_BEAN_NAME =
"ApplicationContextProperties";
/** This is the comment written to the ApplicationContextProperties file. */
private static final String CONNECTOR_MANGER_CONFIG_HEADER =
" Google Search Appliance Connector Manager Configuration\n"
+ "\n"
+ " The 'gsa.feed.protocol' property specifies the URL protocol for\n"
+ " the feed host on the GSA. The supported values are 'http' and\n"
+ " 'https'.\n"
+ " For example:\n"
+ " gsa.feed.protocol=http\n"
+ " gsa.feed.protocol=http\n"
+ "\n"
+ " The 'gsa.feed.host' property specifies the host name or IP address\n"
+ " for the feed host on the GSA.\n"
+ " For example:\n"
+ " gsa.feed.host=172.24.2.0\n"
+ "\n"
+ " The 'gsa.feed.port' property specifies the HTTP host port for the\n"
+ " feed host on the GSA.\n"
+ " For example:\n"
+ " gsa.feed.port=19900\n"
+ "\n"
+ " The 'gsa.feed.securePort' property specifies the HTTPS host port\n"
+ " for the feed host on the GSA. This port will be used if the\n"
+ " 'gsa.feed.protocol property' is set to 'https'.\n"
+ " For example:\n"
+ " gsa.feed.securePort=19902\n"
+ "\n"
+ " The 'gsa.feed.validateCertificate' property specifies whether to\n"
+ " validate the GSA certificate when sending SSL feeds. If the GSA\n"
+ " certificate is installed in the Tomcat keystore, this should be\n"
+ " set to 'true', otherwise it must be set to 'false'.\n"
+ " For example:\n"
+ " gsa.feed.validateCertificate=false\n"
+ "\n"
+ " The 'manager.locked' property is used to lock out the Admin Servlet\n"
+ " and prevent it from making changes to this configuration file.\n"
+ " Specifically, the ability to set the FeedConnection properties will\n"
+ " be locked out. If it is set to 'true' or missing the Servlet will\n"
+ " not be allowed to update this file.\n"
+ " NOTE: This property will automatically be changed to 'true' upon\n"
+ " successful update of the file by the Servlet. Therefore, once the\n"
+ " FeedConnection properties are successfully updated by the Servlet\n"
+ " subsequent updates will be locked out until the flag is manually\n"
+ " reset to 'false'.\n"
+ " For example:\n"
+ " manager.locked=false\n"
+ "\n"
+ " The 'feedLoggingLevel' property controls the logging of the feed\n"
+ " record to a log file. The log record will contain the feed XML\n"
+ " without the content data. Set this property to 'ALL' to enable feed\n"
+ " logging, 'OFF' to disable. Customers and developers can use this\n"
+ " functionality to observe the feed record and metadata information\n"
+ " the connector manager sends to the GSA.\n"
+ " For example:\n"
+ " feedLoggingLevel=OFF\n"
+ "\n"
+ " If you set the 'teedFeedFile' property to the name of an existing\n"
+ " file, whenever the connector manager feeds content to the GSA, it\n"
+ " will write a duplicate copy of the feed XML to the file specified by\n"
+ " the teedFeedFile property. GSA customers and third-party developers\n"
+ " can use this functionality to observe the content the connector\n"
+ " manager sends to the GSA and reproduce any issue which may arise.\n"
+ " NOTE: The teedFeedFile will contain all feed data sent to the GSA,\n"
+ " including document content and metadata. The teedFeedFile can\n"
+ " therefore grow quite large very quickly.\n"
+ " For example:\n"
+ " teedFeedFile=/tmp/CMTeedFeedFile"
+ "\n"
+ " The 'feed.timezone' property defines the default time zone used\n"
+ " for Date metadata values for Documents. A null or empty string\n"
+ " indicates that the system timezone of the machine running the\n"
+ " Connector Manager should be used. Standard TimeZone identifiers\n"
+ " may be used. For example:\n"
+ " feed.timezone=America/Los_Angeles\n"
+ " If a standard TimeZone identifier is unavailable, then a custom\n"
+ " TimeZone identifier can be constructed as +/-hours[minutes] offset\n"
+ " from GMT. For example:\n"
+ " feed.timezone=GMT+10 # GMT + 10 hours\n"
+ " feed.timezone=GMT+0630 # GMT + 6 hours, 30 minutes\n"
+ " feed.timezone=GMT-0800 # GMT - 8 hours, 0 minutes\n"
+ "\n"
+ " The 'feed.file.size' property sets the target size, in bytes, of\n"
+ " an accumulated feed file. The Connector Manager tries to collect\n"
+ " many feed Documents into a single feed file to improve the\n"
+ " efficiency of sending feed data to the GSA. Specifying too small\n"
+ " a value may result in many small feeds which might overrun the\n"
+ " GSA's feed processor. However, specifying too large a feed size\n"
+ " reduces concurrency and may result in OutOfMemory errors in the\n"
+ " Java VM, especially if using multiple Connector Instances.\n"
+ " The default target feed size is 10MB.\n"
+ " For example:\n"
+ " feed.file.size=10485760\n"
+ "\n"
+ " The 'feed.document.size.limit' property defines the maximum\n"
+ " allowed size, in bytes, of a Document's content. Documents whose\n"
+ " content exceeds this size will still have metadata indexed,\n"
+ " however the content itself will not be fed. The default value\n"
+ " is 30MB, the maximum file size accepted by the GSA.\n"
+ " For example:\n"
+ " feed.document.size.limit=31457280\n"
+ "\n"
+ " The 'feed.contenturl.prefix' property is used for contentUrl generation.\n"
+ " The prefix should include protocol, host and port, web app,\n"
+ " and servlet to point back at this Connector Manager instance.\n"
+ " For example:\n"
+ " http://localhost:8080/connector-manager/getDocumentContent\n"
+ "\n"
+ " The 'feed.disable.inherited.acls' property is used to explicitly\n"
+ " disable using ACLs with inheritance, even if the GSA appears to\n"
+ " support the feature. This is necessary in some multibox scenarios\n"
+ " where the GSA does not support ACL inheritance. The default is\n"
+ " 'false'.\n"
+ " feed.disable.inherited.acls=false\n"
+ "\n"
+ " The 'retriever.compression' property is used for content URL feed\n"
+ " content retrieval. If 'true', document content retrieved using the\n"
+ " content URL will be gzip compressed (if the requesting client\n"
+ " supports compression). If 'false', content is returned\n"
+ " uncompressed. Compression may benefit architectures with slow\n"
+ " network communications between the GSA and the Connector Manager\n"
+ " (such as a WAN). However, use of compression may cause excessive\n"
+ " CPU load on both the GSA and the Connector Manager. The default\n"
+ " value is 'false'.\n"
+ " retriever.compression=false\n"
+ "\n"
+ " Whether to use client certificates for authentication instead of\n"
+ " relying on IP addresses. When you enable this option, your servlet\n"
+ " container must be running HTTPS, otherwise there is no way for the\n"
+ " client to provide a client certificate. The default is 'false'.\n"
+ " retriever.useClientCertificateSecurity=false\n"
+ "\n"
+ " This is a comma-delimited list of additional hosts to allow to\n"
+ " retrieve documents. If in client certificate security mode, the\n"
+ " Common Name of the Subject of the provided client certificate is\n"
+ " checked against this list. When not in client certificate security\n"
+ " mode, these hosts are resolved to IPs at startup and the client's\n"
+ " IP is checked against those IPs. gsa.feed.host is implicitly in\n"
+ " the list. The default is empty.\n"
+ " retriever.allowedHosts=\n"
+ "\n"
+ " The 'feed.backlog.*' properties are used to throttle back the\n"
+ " document feed if the GSA has fallen behind processing outstanding\n"
+ " feed items. The Connector Manager periodically polls the GSA,\n"
+ " fetching the count of unprocessed feed items (the backlog count).\n"
+ " If the backlog count exceeds the ceiling value, feeding is paused.\n"
+ " Once the backlog count drops down below the floor value, feeding\n"
+ " resumes.\n For example:\n"
+ " Stop feeding the GSA if its backlog exceeds this value.\n"
+ " feed.backlog.ceiling=4000\n"
+ " Resume feeding the GSA if its backlog falls below this value.\n"
+ " feed.backlog.floor=1000\n"
+ " How often to check for feed backlog (in seconds).\n"
+ " feed.backlog.interval=120\n"
+ "\n"
+ " The 'traversal.batch.size' property defines the optimal number\n"
+ " of items to return in each repository traversal batch. The batch\n"
+ " size represents the size of the roll-back that occurs during a\n"
+ " failure condition. Batch sizes that are too small may incur\n"
+ " excessive processing overhead. Batch sizes that are too large\n"
+ " may produce OutOfMemory conditions within a Connector or result\n"
+ " in early termination of the batch if processing time exceeds the\n"
+ " traversal.time.limit. For example:\n"
+ " traversal.batch.size=1000\n"
+ "\n"
+ " The 'traversal.poll.interval' property defines the number of\n"
+ " seconds to wait after a traversal of the repository finds no new\n"
+ " content before looking again. Short intervals allow new content\n"
+ " to be readily available for search, at the cost of increased\n"
+ " repository access. Long intervals add latency before new\n"
+ " content becomes available for search. By default, the Connector\n"
+ " Manager waits 5 minutes (300 seconds) before retraversing the\n"
+ " repository if no new content was found on the last traversal.\n"
+ " For example:\n"
+ " traversal.poll.interval=300\n"
+ "\n"
+ " The 'traversal.time.limit' property defines the number of\n"
+ " seconds a traversal batch should run before gracefully exiting.\n"
+ " Traversals that exceed this time period risk cancelation.\n"
+ " The default time limit is 2 hours (7200 seconds).\n"
+ " For example:\n"
+ " traversal.time.limit=7200\n"
+ "\n"
+ " The 'traversal.enabled' property is used to enable or disable\n"
+ " Traversals and Feeds for all connector instances in this\n"
+ " Connector Manager. Disabling Traversal would be desirable if\n"
+ " configuring a Connector Manager deployment that only authorizes\n"
+ " search results. Traversals are enabled by default.\n"
+ " traversal.enabled=false\n"
+ "\n"
+ " The 'config.change.detect.interval' property specifies how often\n"
+ " (in seconds) to look for asynchronous configuration changes.\n"
+ " Values <= 0 imply never. For stand-alone deployments, long\n"
+ " intervals or never are probably sufficient. For clustered\n"
+ " deployments with a shared configuration store, 60 to 300 seconds\n"
+ " is probably sufficient. The default configuration change\n"
+ " detection interval is 15 minutes (900 seconds).\n"
+ " config.change.detect.interval=900\n"
+ "\n"
+ "The 'jdbc.datasource.*' properties specify JDBC configuration\n"
+ "required to access external databases. By default, the\n"
+ "Connector Manager uses an embedded H2 database to store\n"
+ "Connector Configurations and Traversal State. No additional\n"
+ "configuration is need when using the embedded database.\n"
+ "However, customers with High Availability requirements may\n"
+ "desire to use an enterprise-class HA database instead.\n"
+ "\n"
+ "The 'jdbc.datasource.type' property identifies the\n"
+ "database vendor driver to use for the default data store.\n"
+ "The supported values for this property are 'h2', 'mysql',\n"
+ "'oracle', 'sqlserver'. These match the values of the\n"
+ "SpiConstants.DatabaseType constants.\n"
+ "jdbc.datasource.type=sqlserver\n"
+ "\n"
+ "The 'jdbc.datasource.*.url' property specifies the\n"
+ "DataSource Connection URL for each DataSource. The\n"
+ "'jdbc.datasource.*.user' and 'jdbc.datasource.*.password'\n"
+ "properties specify the login credentials for that database.\n"
+ "The password value should be encrypted using the Connector\n"
+ "Manager's EncryptPassword utility.\n"
+ "More than one JDBC DataSource may be configured at once,\n"
+ "however only one may be identified as 'jdbc.datasource.type'.\n"
+ "This makes is convenient to migrate configurations from\n"
+ "one database implementation to another using the MigrateStore\n"
+ "utility. For instance, you could use MigrateStore to move\n"
+ "Connector Configurations from the embedded H2 database to\n"
+ "an external corporate SQL Server database.\n"
+ "\n"
+ "Microsoft SQL Server JDBC DataSource configuration.\n"
+ "jdbc.datasource.sqlserver.url=jdbc:sqlserver://myserver;DatabaseName=google_connectors\n"
+ "jdbc.datasource.sqlserver.user=google_admin\n"
+ "jdbc.datasource.sqlserver.password=\n"
+ "\n"
+ "Oracle JDBC DataSource configuration.\n"
+ "jdbc.datasource.oracle.url=jdbc:oracle:thin:@myserver:1521:myserver\n"
+ "jdbc.datasource.oracle.user=google_admin\n"
+ "jdbc.datasource.oracle.password=\n"
+ "\n"
+ "MySQL JDBC DataSource configuration.\n"
+ "jdbc.datasource.mysql.url=jdbc:mysql://myserver/google_connectors\n"
+ "jdbc.datasource.mysql.user=google_admin\n"
+ "jdbc.datasource.mysql.password=\n"
+ "\n";
private static final Logger LOGGER =
Logger.getLogger(Context.class.getName());
private static final GenericApplicationContext genericApplicationContext =
new GenericApplicationContext();
private static Context INSTANCE = new Context();
private Throwable initFailureCause = null;
private boolean started = false;
private boolean isServletContext = false;
private boolean isFeeding = true;
private String commonDirPath = null;
// singletons
private Manager manager = null;
private TraversalScheduler traversalScheduler = null;
private TraversalContext traversalContext = null;
private SpringInstantiator instantiator = null;
private String standaloneContextLocation;
private String standaloneContextBaseDir;
private boolean isTeedFeedFileInitialized = false;
private String teedFeedFile = null;
private boolean isGsaFeedHostInitialized = false;
private String gsaFeedHost = null;
/**
* The prefix that will be used for contentUrl generation.
* The prefix should include protocol, host and port, web app,
* and servlet to point back at this Connector Manager instance.
* For example:
* {@code http://localhost:8080/connector-manager/getDocumentContent}
*/
private String contentUrlPrefix = null;
private int propertiesVersion = 0;
/**
* @param feeding to feed or not to feed
*/
public void setFeeding(boolean feeding) {
LOGGER.config("Traversal and Feeds are "
+ ((feeding) ? "enabled." : "disabled."));
this.isFeeding = feeding;
}
/**
* @return feeding to feed or not to feed
*/
public boolean isFeeding() {
return this.isFeeding;
}
public static Context getInstance() {
return INSTANCE;
}
ApplicationContext applicationContext = null;
private Context() {
// Private to ensure singleton.
}
private void initializeStandaloneApplicationContext() {
if (applicationContext != null) {
// too late - someone else already established a context. this might
// happen with multiple junit tests that each want to establish a context.
// so long as they use the same context location, it's ok. if they want a
// different context location, they should refresh() - see below
return;
}
applicationContext = genericApplicationContext; // avoid recursion
if (standaloneContextLocation == null) {
standaloneContextLocation = DEFAULT_JUNIT_CONTEXT_LOCATION;
}
if (standaloneContextBaseDir == null) {
standaloneContextBaseDir = System.getProperty("user.dir");
}
if (commonDirPath == null) {
commonDirPath = DEFAULT_JUNIT_COMMON_DIR_PATH;
}
LOGGER.info("context file: " + standaloneContextLocation);
LOGGER.info("context base directory: " + standaloneContextBaseDir);
LOGGER.info("common dir path: " + commonDirPath);
try {
applicationContext = new AnchoredFileSystemXmlApplicationContext(
standaloneContextBaseDir, standaloneContextLocation);
} catch (Throwable t) {
setInitFailureCause(t);
LOGGER.log(Level.SEVERE, "Connector Manager Startup failed: ", t);
throw new IllegalStateException("Connector Manager Startup failed", t);
}
}
/**
* Establishes that we are operating within the standalone context. In
* this case, we use a FileSystemApplicationContext.
*
* @param contextLocation the name of the context XML file used for
* instantiation.
* @param commonDirPath the location of the common directory which contains
* ConnectorType and Connector instantiation configuration data.
*/
public void setStandaloneContext(String contextLocation,
String commonDirPath) {
setStandaloneContext(contextLocation, null, commonDirPath);
}
/**
* Establishes that we are operating within the standalone context. In
* this case, we use a FileSystemApplicationContext.
*
* @param contextLocation the name of the context XML file used for
* instantiation.
* @param contextBaseDir base directory for relative file paths in
* the contextLocation.
* @param commonDirPath the location of the common directory which contains
* ConnectorType and Connector instantiation configuration data.
*/
public void setStandaloneContext(String contextLocation,
String contextBaseDir,
String commonDirPath) {
this.standaloneContextLocation = contextLocation;
this.standaloneContextBaseDir = contextBaseDir;
this.commonDirPath = commonDirPath;
initializeStandaloneApplicationContext();
}
/**
* Establishes that we are operating from a servlet context. In this case, we
* use an XmlWebApplicationContext, which finds its config from the servlet
* context - WEB-INF/applicationContext.xml.
*
* @param servletApplicationContext the web application servlet context.
* @param commonDirPath the location of the common directory which contains
* ConnectorType and Connector instantiation configuration data.
*/
public void setServletContext(ApplicationContext servletApplicationContext,
String commonDirPath) {
this.applicationContext = servletApplicationContext;
this.commonDirPath = commonDirPath;
isServletContext = true;
}
/**
* Saves the cause of a Connector Manager initialization failure.
* That cause will be rethrown upon further attempts to use the
* unitialized Context.
*
* @param cause the cause of initialization failure
*/
public void setInitFailureCause(Throwable cause) {
this.initFailureCause = cause;
}
/*
* Choose a default context, if it wasn't specified in any other way. For now,
* we choose servlet context by default.
*/
private synchronized void initApplicationContext() {
if (applicationContext == null) {
if (initFailureCause != null) {
throw new IllegalStateException("Connector Manager Startup failed",
initFailureCause);
}
initializeStandaloneApplicationContext();
}
}
/**
* Start up the Scheduler.
*/
private void startScheduler() {
traversalScheduler =
(TraversalScheduler) getRequiredBean("TraversalScheduler",
TraversalScheduler.class);
if (traversalScheduler != null) {
traversalScheduler.init();
}
}
/**
* Start up the Instantiator.
*/
private void startInstantiator() {
instantiator =
(SpringInstantiator) getBean("Instantiator", SpringInstantiator.class);
if (instantiator != null) {
instantiator.init();
}
}
/**
* Do everything necessary to start up the application.
*/
public synchronized void start() {
if (started) {
return;
}
initApplicationContext();
try {
startInstantiator();
if (isFeeding) {
startScheduler();
}
startServices();
} finally {
started = true;
}
}
/**
* Starts any services declared as part of the application.
*/
private void startServices() {
for (ContextService service : getServices()) {
service.start();
}
}
/**
* Gets a service by name. Returns a matching bean if found or null
* otherwise.
*
* @param serviceName the name of the service to find.
* @return if there is a single bean with the given name it will be returned.
* If there are multiple beans with the same name, the first one found
* will be returned. If there are no beans with the given name, null
* will be returned.
*/
public ContextService findService(String serviceName) {
return (ContextService) getBean(serviceName, null);
}
/**
* Returns an ordered list of services attached to the context. Collection is
* ordered according to the startup order of the services.
* <p>
* To get the list in reverse order use {@link Collections#reverse(List)}.
*
* @return an ordered list of ContextService objects. If no services are
* registered an empty list will be returned.
*/
public List<ContextService> getServices() {
// TODO: Investigate the use of the GenericBeanFactoryAccessor here.
Map<?, ?> orderedServices = (Map<?, ?>)
getBean(ORDERED_SERVICES_BEAN_NAME, null);
Map<?, ?> services = applicationContext.getBeansOfType(ContextService.class);
List<ContextService> result = new ArrayList<ContextService>();
if (orderedServices != null) {
for (Iterator<?> iter = orderedServices.keySet().iterator();
iter.hasNext(); ) {
ContextService service =
(ContextService) orderedServices.get(iter.next());
result.add(service);
}
}
for (Iterator<?> iter = services.values().iterator(); iter.hasNext(); ) {
ContextService service = (ContextService) iter.next();
if (!result.contains(service)) {
result.add(service);
}
}
return result;
}
/**
* Get a bean from the application context that we MUST have to operate.
*
* @param beanName the name of the bean we're looking for. Typically, the same
* as its most general interface.
* @param clazz the class of the bean we're looking for.
* @return if there is a single bean of the required type, we return it,
* regardless of name. If there are multiple beans of the required
* type, we return the one with the required name, if present, or the
* first one we find, if there is none of the right name.
* @throws IllegalStateException if there are no beans of the right type, or
* if there is an instantiation problem.
*/
public Object getRequiredBean(String beanName, Class<?> clazz) {
try {
Object object = getBean(beanName, clazz);
if (object != null) {
return object;
}
throw new IllegalStateException("The context has no " + beanName);
} catch (BeansException e) {
throw new IllegalStateException("Spring failure - can't instantiate "
+ beanName + ": (" + e.toString() + ")");
}
}
/**
* Get an optional bean from the application context.
*
* @param beanName the name of the bean we're looking for. Typically,
* the same as its most general interface.
* @param clazz the class of the bean we're looking for.
* @return if there is a single bean of the required type, we return it,
* regardless of name. If there are multiple beans of the required
* type, we return the one with the required name, if present, or the
* first one we find, if there is none of the right name. Returns
* null if no bean of the appropriate name or type is found.
* @throws BeansException if there is an instantiation problem.
*/
public Object getBean(String beanName, Class<?> clazz)
throws BeansException {
initApplicationContext();
return getBean(applicationContext, beanName, clazz);
}
/**
* Get a bean from the supplied BeanFactory. First look for a bean with
* the given name and type. If none is found, look for the first bean
* of the specified type.
*
* @param factory a ListableBeanFactory
* @param beanName the name of the bean we're looking for. Typically, the same
* as its most general interface. If null, return the first bean
* of the requested type.
* @param clazz the class of the bean we're looking for. If null, return
* any bean of the specified name.
* @return if there is a single bean of the required type, we return it,
* regardless of name. If there are multiple beans of the required
* type, we return the one with the required name, if present, or the
* first one we find, if there is none of the right name. Returns
* null if no bean of the appropriate name or type is found.
* @throws BeansException if there is an instantiation problem.
*/
public Object getBean(ListableBeanFactory factory, String beanName,
Class<?> clazz) throws BeansException {
Object result = null;
// First, look for a bean with the specified name and type.
try {
if (beanName != null && beanName.length() > 0) {
result = factory.getBean(beanName, clazz);
if (result != null) {
return result;
}
}
} catch (NoSuchBeanDefinitionException e) {
// Not a problem yet. Look for any bean of the appropriate type.
}
// If no bean type was specified, we are done.
if (clazz == null) {
return null;
}
// Get the list of beans defined in the bean factory of the required type.
String[] beanList = factory.getBeanNamesForType(clazz);
// Make sure there is at least one
if (beanList.length < 1) {
return null;
}
// If more beans were found issue a warning.
if (beanList.length > 1) {
StringBuilder buf = new StringBuilder();
for (int i = 1; i < beanList.length; i++) {
buf.append(" ");
buf.append(beanList[i]);
}
LOGGER.warning("Resource contains multiple " + clazz.getName() +
" definitions. Using the first: " + beanList[0] +
". Skipping: " + buf);
}
return factory.getBean(beanList[0]);
}
/**
* Gets the singleton {@link Manager}.
*
* @return the Manager
*/
public Manager getManager() {
if (manager != null) {
return manager;
}
manager = (Manager) getRequiredBean("Manager", Manager.class);
return manager;
}
/**
* Gets the singleton {@link Instantiator}.
*
* @return the Instantiator
*/
public Instantiator getInstantiator() {
if (instantiator != null) {
return instantiator;
}
instantiator = (SpringInstantiator)
getRequiredBean("Instantiator", SpringInstantiator.class);
return instantiator;
}
/**
* Gets the singleton TraversalContext.
*
* @return the TraversalContext
*/
public TraversalContext getTraversalContext() {
if (traversalContext != null) {
return traversalContext;
}
try {
traversalContext = (TraversalContext) getRequiredBean("TraversalContext",
TraversalContext.class);
} catch (IllegalStateException e) {
LOGGER.warning("Can't find suitable " + TraversalContext.class.getName()
+ " bean in context, using default.");
traversalContext = new ProductionTraversalContext();
}
// Lazily initialize supportsInheritedAcls and supportsDenyAcls,
// since they usually require communicating with the GSA.
if (traversalContext instanceof SimpleTraversalContext) {
SimpleTraversalContext simpleContext =
(SimpleTraversalContext) traversalContext;
Properties props = getConnectorManagerProperties();
GsaFeedConnection feeder = getGsaFeedConnection();
initTraversalContext(simpleContext, props, feeder);
}
return traversalContext;
}
@VisibleForTesting
void initTraversalContext(SimpleTraversalContext simpleContext,
Properties props, GsaFeedConnection feeder) {
// Inherited ACLs can be disabled, but may still be used in feeds.
// See ProductionManager.getDocumentMetaData for an explanation.
// GSA 6.14 and earlier are not supported.
simpleContext.setSupportsInheritedAcls(
!Boolean.valueOf(props.getProperty(FEED_DISABLE_INHERITED_ACLS)));
simpleContext.setSupportsDenyAcls(true);
}
/**
* Throws out the current context instance and gets another one. For testing
* only. This could really boolux things up if it were used in production!
*/
public static void refresh() {
INSTANCE = new Context();
}
/**
* Gets the applicationContext. For testing only.
*
* @return the applicationContext
*/
public ApplicationContext getApplicationContext() {
initApplicationContext();
return applicationContext;
}
/**
* Retrieves the ApplicationContext configLocations,
* or null if none specified.
*
* @return String array of configLocations.
*/
public String[] getConfigLocations() {
initApplicationContext();
if (isServletContext) {
return ((XmlWebApplicationContext) applicationContext)
.getConfigLocations();
} else {
return ((AnchoredFileSystemXmlApplicationContext) applicationContext)
.getConfigLocations();
}
}
public synchronized void shutdown(boolean force) {
if (started) {
LOGGER.info("Shutdown initiated...");
stopServices(force);
if (null != traversalScheduler) {
traversalScheduler.shutdown();
traversalScheduler = null;
}
if (null != instantiator) {
instantiator.shutdown(force,
ThreadPool.DEFAULT_SHUTDOWN_TIMEOUT_MILLIS);
instantiator = null;
}
closeDatabases();
started = false;
}
}
/**
* Shuts down any Spring-configured JdbcDatabase instances.
*/
@SuppressWarnings("unchecked")
private void closeDatabases() {
Collection<JdbcDatabase> databases = (Collection<JdbcDatabase>)
applicationContext.getBeansOfType(JdbcDatabase.class).values();
for (JdbcDatabase database : databases) {
try {
database.shutdown();
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to shut down database connection", e);
}
}
}
/**
* Stops any services declared as part of the application.
*/
private void stopServices(boolean force) {
initApplicationContext();
List<ContextService> services = getServices();
Collections.reverse(services);
for (ContextService service : services) {
service.stop(force);
}
}
/**
* Retrieves the prefix for the Common directory file depending on whether
* it is a standalone context or servlet context.
*
* @return prefix for the Repository file.
*/
public String getCommonDirPath() {
initApplicationContext();
return commonDirPath;
}
private String getPropFileName() throws InstantiatorException {
String propFileName = null;
try {
propFileName =
(String) applicationContext.getBean(
APPLICATION_CONTEXT_PROPERTIES_BEAN_NAME, java.lang.String.class);
} catch (BeansException e) {
throw new InstantiatorException("Spring exception while getting "
+ APPLICATION_CONTEXT_PROPERTIES_BEAN_NAME + " bean", e);
}
if (Strings.isNullOrEmpty(propFileName)) {
throw new InstantiatorException("Null or empty file name returned from "
+ "Spring while getting " + APPLICATION_CONTEXT_PROPERTIES_BEAN_NAME
+ " bean");
}
return propFileName;
}
private File getPropFile(String propFileName) throws InstantiatorException {
Resource propResource = applicationContext.getResource(propFileName);
File propFile;
try {
propFile = propResource.getFile();
} catch (IOException e) {
throw new InstantiatorException(e);
}
return propFile;
}
/**
* Returns the configuration Properties for the Connector Manager.
*/
public Properties getConnectorManagerProperties() {
try {
return loadConnectorManagerProperties();
} catch (PropertiesException pe) {
LOGGER.log(Level.WARNING, "Unable to read application context"
+ " properties.", pe);
return new Properties();
}
}
/**
* Loads the configuration Properties for the Connector Manager.
*
* @return the configuration Properties for the Connector Manager.
* @throws PropertiesException if error loading properties
*/
public synchronized Properties loadConnectorManagerProperties()
throws PropertiesException {
initApplicationContext();
String propFileName = "";
try {
// Get the properties out of the CM properties file if present.
propFileName = getPropFileName();
File propFile = getPropFile(propFileName);
Properties props = PropertiesUtils.loadFromFile(propFile);
propertiesVersion = PropertiesUtils.getPropertiesVersion(props);
return props;
} catch (InstantiatorException ie) {
throw new PropertiesException("Unable to read application context"
+ " properties file " + propFileName, ie);
}
}
/**
* Stores the configuration Properties for the Connector Manager.
*
* @param props the configuration Properties to store
* @throws PropertiesException if error storing properties
*/
public synchronized void storeConnectorManagerProperties(Properties props)
throws PropertiesException {
String propFileName = "";
try {
// Get the properties out of the CM properties file if present.
propFileName = getPropFileName();
File propFile = getPropFile(propFileName);
PropertiesUtils.storeToFile(props, propFile,
CONNECTOR_MANGER_CONFIG_HEADER);
} catch (InstantiatorException ie) {
throw new PropertiesException("Unable to save application context"
+ " properties file " + propFileName, ie);
}
}
/**
* Returns a Properties containing just the GSA feed URL properties.
*/
public Properties getConnectorManagerConfig() {
// Get the properties out of the CM properties file if present.
Properties props = getConnectorManagerProperties();
Properties result = new Properties();
String protocol = props.getProperty(GSA_FEED_PROTOCOL_PROPERTY_KEY);
if (protocol != null) {
result.setProperty(GSA_FEED_PROTOCOL_PROPERTY_KEY, protocol);
}
result.setProperty(GSA_FEED_HOST_PROPERTY_KEY,
props.getProperty(GSA_FEED_HOST_PROPERTY_KEY));
result.setProperty(GSA_FEED_PORT_PROPERTY_KEY,
props.getProperty(GSA_FEED_PORT_PROPERTY_KEY, GSA_FEED_PORT_DEFAULT));
String securePort = props.getProperty(GSA_FEED_SECURE_PORT_PROPERTY_KEY);
if (securePort != null && Integer.parseInt(securePort) >= 0) {
result.setProperty(GSA_FEED_SECURE_PORT_PROPERTY_KEY, securePort);
}
return result;
}
public synchronized void setConnectorManagerConfig(String feederGateProtocol,
String feederGateHost, int feederGatePort, int feederGateSecurePort,
String connectorManagerUrl) throws InstantiatorException {
initApplicationContext();
setConnectorManagerConfig(feederGateProtocol, feederGateHost,
feederGatePort, feederGateSecurePort, getGsaFeedConnection(),
connectorManagerUrl);
}
private GsaFeedConnection getGsaFeedConnection() {
try {
return (GsaFeedConnection)
applicationContext.getBean("FeedConnection", GsaFeedConnection.class);
} catch (BeansException be) {
// The configured FeedConnection isn't a GSA, so it doesn't care
// about the GSA host and port.
LOGGER.config("The FeedConnection is not to a GSA: " + be.getMessage());
return null;
}
}
@VisibleForTesting
void setConnectorManagerConfig(String feederGateProtocol,
String feederGateHost, int feederGatePort, int feederGateSecurePort,
GsaFeedConnection feeder, String connectorManagerUrl)
throws InstantiatorException {
// Update the feed host and port in the CM properties file.
String propFileName = getPropFileName();
File propFile = getPropFile(propFileName);
Properties props;
try {
props = PropertiesUtils.loadFromFile(propFile);
} catch (PropertiesException e) {
LOGGER.log(Level.WARNING, "Unable to read application context properties"
+ " file "+ propFileName + "; attempting instantiation stand-alone.",
e);
props = new Properties();
}
props.put(GSA_FEED_HOST_PROPERTY_KEY, feederGateHost);
props.put(GSA_FEED_PORT_PROPERTY_KEY, Integer.toString(feederGatePort));
// Do not overwrite the protocol or secure port if we didn't get a
// value from the GSA. This is for backward compatibility when
// manually setting the securePort or protocol for older GSAs.
int explicitSecurePort;
if (feederGateSecurePort < 0) {
String securePort = props.getProperty(GSA_FEED_SECURE_PORT_PROPERTY_KEY);
if (Strings.isNullOrEmpty(securePort)) {
// If no secure port is specified, use the default in case the
// protocol is set to "https", but do not store this value.
feederGateSecurePort = Integer.parseInt(GSA_FEED_SECURE_PORT_DEFAULT);
explicitSecurePort = GSA_FEED_SECURE_PORT_INVALID;
} else {
feederGateSecurePort = Integer.parseInt(securePort);
explicitSecurePort = feederGateSecurePort;
}
} else {
props.put(GSA_FEED_SECURE_PORT_PROPERTY_KEY,
Integer.toString(feederGateSecurePort));
explicitSecurePort = feederGateSecurePort;
}
if (Strings.isNullOrEmpty(feederGateProtocol)) {
String protocol = props.getProperty(GSA_FEED_PROTOCOL_PROPERTY_KEY);
if (Strings.isNullOrEmpty(protocol)) {
// Pick a protocol based on an explicitly configured secure port.
feederGateProtocol = (explicitSecurePort < 0) ? "http" : "https";
} else {
feederGateProtocol = protocol;
}
} else {
props.put(GSA_FEED_PROTOCOL_PROPERTY_KEY, feederGateProtocol);
}
if (!Strings.isNullOrEmpty(connectorManagerUrl)) {
contentUrlPrefix = connectorManagerUrl + FEED_CONTENTURL_SERVLET;
props.put(FEED_CONTENTURL_PREFIX_PROPERTY_KEY, contentUrlPrefix);
} else {
contentUrlPrefix = null;
}
// Lock down the manager at this point.
props.put(MANAGER_LOCKED_PROPERTY_KEY, Boolean.TRUE.toString());
try {
PropertiesUtils.storeToFile(props, propFile,
CONNECTOR_MANGER_CONFIG_HEADER);
} catch (PropertiesException e) {
LOGGER.log(Level.WARNING, "Unable to save application context properties"
+ " file " + propFileName + ". ", e);
throw new InstantiatorException(e);
}
// This property is not overwritten, but is logged.
boolean validateCertificate = Boolean.parseBoolean(props.getProperty(
GSA_FEED_VALIDATE_CERTIFICATE_PROPERTY_KEY,
GSA_FEED_VALIDATE_CERTIFICATE_DEFAULT));
LOGGER.info("Updated Connector Manager Config: "
+ GSA_FEED_PROTOCOL_PROPERTY_KEY + "=" + feederGateProtocol + "; "
+ GSA_FEED_HOST_PROPERTY_KEY + "=" + feederGateHost + "; "
+ GSA_FEED_PORT_PROPERTY_KEY + "=" + feederGatePort + "; "
+ GSA_FEED_VALIDATE_CERTIFICATE_PROPERTY_KEY + "="
+ validateCertificate + "; "
+ GSA_FEED_SECURE_PORT_PROPERTY_KEY + "=" + feederGateSecurePort + "; "
+ FEED_CONTENTURL_PREFIX_PROPERTY_KEY + "="
+ props.getProperty(FEED_CONTENTURL_PREFIX_PROPERTY_KEY) + "; "
+ MANAGER_LOCKED_PROPERTY_KEY + "="
+ props.getProperty(MANAGER_LOCKED_PROPERTY_KEY));
// Update our local cached feed host.
gsaFeedHost = feederGateHost;
isGsaFeedHostInitialized = true;
// TODO: The following should probably be done in ProductionManager.
// Notify the GsaFeedConnection of new host and port.
if (feeder != null) {
try {
feeder.setFeedHostAndPort(feederGateProtocol, feederGateHost,
feederGatePort, feederGateSecurePort);
// Update the validateCertificate flag. We do this here so that
// the value can be updated without restarting Tomcat.
feeder.setValidateCertificate(validateCertificate);
} catch (MalformedURLException e) {
throw new InstantiatorException("Invalid GSA Feed specification", e);
}
}
// Notify GData aware Connectors.
if (instantiator != null) {
instantiator.setGDataConfig();
}
}
/**
* Reads <code>teedFeedFile</code> from the application context properties file.
* See google-enterprise-connector-manager/projects/connector-manager/etc/applicationContext.properties
* for additional documentation.
*/
public synchronized String getTeedFeedFile() {
initApplicationContext();
if (!isTeedFeedFileInitialized) {
teedFeedFile = getProperty(TEED_FEED_FILE_PROPERTY_KEY, null);
isTeedFeedFileInitialized = true;
}
return teedFeedFile;
}
/**
* Reads <code>gsa.feed.host</code> from the application context properties file.
* See google-enterprise-connector-manager/projects/connector-manager/etc/applicationContext.properties
* for additional documentation.
*/
public synchronized String getGsaFeedHost() {
initApplicationContext();
if (!isGsaFeedHostInitialized) {
gsaFeedHost = getProperty(GSA_FEED_HOST_PROPERTY_KEY, null);
isGsaFeedHostInitialized = true;
}
return gsaFeedHost;
}
/**
* Reads <code>feed.contenturl.prefix</code> from the application context
* properties file.
* See google-enterprise-connector-manager/projects/connector-manager/etc/applicationContext.properties
* for additional documentation.
*/
public synchronized String getContentUrlPrefix() {
initApplicationContext();
if (contentUrlPrefix == null) {
contentUrlPrefix = getProperty(FEED_CONTENTURL_PREFIX_PROPERTY_KEY, null);
}
return contentUrlPrefix;
}
@VisibleForTesting
public synchronized void setContentUrlPrefix(String contentUrlPrefix) {
this.contentUrlPrefix = contentUrlPrefix;
}
/**
* Reads <code>manager.locked</code> property from the application context
* properties file.
*
* @return true if the property does not exist. Returns true if the property
* is set to 'true', ignoring case. Returns false otherwise.
*/
public synchronized boolean getIsManagerLocked() {
initApplicationContext();
String isManagerLocked = getProperty(MANAGER_LOCKED_PROPERTY_KEY, null);
if (isManagerLocked != null) {
return Boolean.valueOf(isManagerLocked).booleanValue();
}
// Consider older, but uninitialized properties files to be unlocked.
if (propertiesVersion < 2 && "localhost".equals(getGsaFeedHost())) {
return false;
}
return true;
}
/**
* Reads a property from the application context properties file.
*
* @param key the property name
* @param defaultValue if property does not exist
*/
private String getProperty(String key, String defaultValue) {
Properties props = getConnectorManagerProperties();
return props.getProperty(key, defaultValue);
}
/**
* Notify all listeners registered with this context of an application event.
* Events may be framework events or application-specific events.
*
* @param event the event to publish.
*/
public void publishEvent(ApplicationEvent event) {
initApplicationContext();
applicationContext.publishEvent(event);
}
}