/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.platform;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import org.geoserver.platform.resource.Paths;
import org.geotools.factory.FactoryRegistry;
import org.geotools.util.SoftValueHashMap;
import org.geotools.util.logging.Logging;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.web.context.WebApplicationContext;
/**
* Utility class uses to process GeoServer extension points.
* <p>
* An instance of this class needs to be registered in spring context as follows.
* <pre><code>
* <bean id="geoserverExtensions" class="org.geoserver.GeoServerExtensions"/>
* </code></pre>
* It must be a singleton, and must not be loaded lazily. Furthermore, this
* bean must be loaded before any beans that use it.
*
* @author Justin Deoliveira, The Open Planning Project
* @author Andrea Aime, The Open Planning Project
*
*/
public class GeoServerExtensions implements ApplicationContextAware, ApplicationListener {
/**
* logger
*/
protected static final Logger LOGGER = Logging.getLogger( "org.geoserver.platform" );
/**
* Caches the names of the beans for a particular type, so that the lookup (expensive)
* wont' be needed. We cache names instead of beans because doing the latter we would
* break the "singleton=false" directive of some beans
*/
static SoftValueHashMap<Class, String[]> extensionsCache = new SoftValueHashMap<Class, String[]>(40);
static ConcurrentHashMap<String, Object> singletonBeanCache = new ConcurrentHashMap<String, Object>();
/**
* Property cache maintained by GeoServerExtensionsHelper allowing temporary override of
* {@link #getProperty(String)} results.
*/
static ConcurrentHashMap<String,String> propertyCache = new ConcurrentHashMap<String,String>();
/**
* File cache maintained by GeoServerExtensionsHelper allowing temporary override of
* {@link #file(String)} results.
*/
static ConcurrentHashMap<String,File> fileCache = new ConcurrentHashMap<String,File>();
/**
* SPI lookups are very expensive, we need to cache them
*/
static SoftValueHashMap<Class, List<Object>> spiCache = new SoftValueHashMap<Class, List<Object>>(40);
/**
* Flag to identify use of spring context via {@link #setApplicationContext(ApplicationContext)} an
* enable additional consistency checks for missing extensions.
*/
static boolean isSpringContext = true;
/**
* A static application context
*/
static ApplicationContext context;
/**
* Sets the web application context to be used for looking up extensions.
* <p>
* This method is called by the spring container, and should never be called
* by client code. If client needs to supply a particular context, methods
* which take a context are available.
* </p>
* <p>
* This is the context that is used for methods which don't supply their
* own context.
* </p>
* @param context ApplicationContext used to lookup extensions
*/
public void setApplicationContext(ApplicationContext context)
throws BeansException {
isSpringContext = true;
GeoServerExtensions.context = context;
extensionsCache.clear();
singletonBeanCache.clear();
propertyCache.clear();
}
/**
* Loads all extensions implementing or extending <code>extensionPoint</code>.
*
* @param extensionPoint The class or interface of the extensions.
* @param context The context in which to perform the lookup.
*
* @return A collection of the extensions, or an empty collection.
*/
@SuppressWarnings("unchecked")
public static final <T> List<T> extensions(Class<T> extensionPoint, ApplicationContext context) {
Collection<String> names;
names = extensionNames(extensionPoint, context);
// lookup extension filters preventing recursion
List<ExtensionFilter> filters;
if(ExtensionFilter.class.isAssignableFrom(extensionPoint)) {
filters = Collections.emptyList();
} else {
filters = extensions(ExtensionFilter.class, context);
}
// look up all the beans
List<T> result = new ArrayList<T>(names.size());
for(String name : names) {
Object bean = getBean(context, name);
if(!excludeBean(name, bean, filters))
result.add( (T) bean);
}
// load from secondary extension providers
if (!ExtensionProvider.class.isAssignableFrom(extensionPoint) &&
!ExtensionFilter.class.isAssignableFrom(extensionPoint)) {
List<Object> secondary = new ArrayList<Object>();
for (ExtensionProvider xp : extensions(ExtensionProvider.class, context)) {
try {
if (extensionPoint.isAssignableFrom(xp.getExtensionPoint())) {
secondary.addAll(xp.getExtensions(extensionPoint));
}
}
catch(Exception e) {
LOGGER.log(Level.WARNING, "Extension provider threw exception", e);
}
}
filter(secondary, filters, result);
}
// load from factory spi
List<Object> spiExtensions = spiCache.get(extensionPoint);
if(spiExtensions == null) {
spiExtensions = new ArrayList<Object>();
Iterator i = FactoryRegistry.lookupProviders(extensionPoint);
while( i.hasNext() ) {
spiExtensions.add( i.next() );
}
spiCache.put(extensionPoint, spiExtensions);
}
// filter the beans coming from SPI (we don't cache the results
// of the filtering, an extension filter can change its mind
// from call to call
filter(spiExtensions, filters, result);
//sort the results based on ExtensionPriority
Collections.sort( result, new Comparator<Object>() {
public int compare(Object o1, Object o2) {
int p1 = ExtensionPriority.LOWEST;
if ( o1 instanceof ExtensionPriority ) {
p1 = ((ExtensionPriority)o1).getPriority();
}
int p2 = ExtensionPriority.LOWEST;
if ( o2 instanceof ExtensionPriority ) {
p2 = ((ExtensionPriority)o2).getPriority();
}
return p1 - p2;
}
});
return result;
}
public static <T> Collection<String> extensionNames(Class<T> extensionPoint) {
return extensionNames(extensionPoint, context);
}
public static <T> Collection<String> extensionNames(Class<T> extensionPoint,
ApplicationContext context) {
String[] names;
if(GeoServerExtensions.context == context){
names = extensionsCache.get(extensionPoint);
}else{
names = null;
}
if(names == null) {
checkContext(context,extensionPoint.getSimpleName());
if ( context != null ) {
try {
names = context.getBeanNamesForType(extensionPoint);
if( names == null ){
names = new String[0];
}
//update cache only if dealing with the same context
if(GeoServerExtensions.context == context){
extensionsCache.put(extensionPoint, names);
}
}
catch( Exception e ) {
//JD: this can happen during testing... if the application
// context has been closed and a non-one time setup test is
// run that triggers an extension lookup
LOGGER.log( Level.WARNING, "bean lookup error", e );
return Collections.emptyList();
}
}
else {
return Collections.emptyList();
}
}
return Arrays.asList(names);
}
private static Object getBean(ApplicationContext context, String name) {
Object bean = singletonBeanCache.get(name);
if(bean == null && context != null) {
bean = context.getBean(name);
if(bean != null && context.isSingleton(name)) {
singletonBeanCache.put(name, bean);
}
}
return bean;
}
private static void filter(List objects, List<ExtensionFilter> filters, List result) {
for (Object bean : objects) {
if(!excludeBean(null, bean, filters))
result.add(bean);
}
}
/**
* Returns true if any of the {@link ExtensionFilter} asks to exclude the bean
*/
private static boolean excludeBean(String beanId, Object bean, List<ExtensionFilter> filters) {
for (ExtensionFilter filter : filters) {
if(filter.exclude(beanId, bean))
return true;
}
return false;
}
/**
* Loads all extensions implementing or extending <code>extensionPoint</code>.
* <p>
* This method uses the "default" application context to perform the lookup.
* See {@link #setApplicationContext(ApplicationContext)}.
* </p>
* @param extensionPoint The class or interface of the extensions.
*
* @return A collection of the extensions, or an empty collection.
*/
public static final <T> List<T> extensions(Class<T> extensionPoint) {
return extensions(extensionPoint, context);
}
/**
* Returns a specific bean given its name
* @param name
*
*/
public static final Object bean(String name) {
return bean(name, context);
}
/**
* Returns a specific bean given its name with a specified application context.
*/
public static final Object bean(String name, ApplicationContext context) {
checkContext(context, name);
if( context != null ){
return getBean(context,name);
}
else {
Object bean = singletonBeanCache.get(name);
return bean;
}
}
/**
* Loads a single bean by its type.
* <p>
* This method returns null if there is no such bean. An exception is thrown
* if multiple beans of the specified type exist.
* </p>
*
* @param type THe type of the bean to lookup.
*
* @throws MultipleBeansException If there are multiple beans of the specified
* type in the context.
*/
public static final <T> T bean(Class<T> type) throws IllegalArgumentException {
checkContext(context,type.getSimpleName());
return bean(type,context);
}
/**
* Loads a single bean by its type from the specified application context.
* <p>
* This method returns null if there is no such bean. An exception is thrown
* if multiple beans of the specified type exist.
* </p>
*
* @param type THe type of the bean to lookup.
* @param context The application context
*
* @throws MultipleBeansException If there are multiple beans of the specified
* type in the context.
*/
public static final <T> T bean(Class<T> type, ApplicationContext context) throws IllegalArgumentException {
List<T> beans = extensions(type,context);
if ( beans.isEmpty() ) {
return null;
}
if ( beans.size() > 1 ) {
throw new MultipleBeansException(type, extensionNames(type, context));
}
return beans.get( 0 );
}
/**
*
* Exception thrown when multiple beans implementing an extension point and only one is expected.
*
*/
public static class MultipleBeansException extends IllegalArgumentException {
/** serialVersionUID */
private static final long serialVersionUID = -8039187466594032626L;
private final Class<?> extensionPoint;
private final Collection<String> availableBeans;
public MultipleBeansException(Class<?> extensionPoint,
Collection<String> availableBeans) {
super("Multiple beans of type " + extensionPoint.getName());
this.extensionPoint = extensionPoint;
this.availableBeans = availableBeans;
}
/**
* @return the extension point
*/
public Class<?> getExtensionPoint() {
return extensionPoint;
}
/**
* @return the names of the beans
*/
public Collection<String> getAvailableBeans() {
return availableBeans;
}
}
public void onApplicationEvent(ApplicationEvent event) {
if(event instanceof ContextRefreshedEvent) {
extensionsCache.clear();
singletonBeanCache.clear();
}
}
/**
* Checks the context, if null will issue a warning.
*/
static void checkContext(ApplicationContext context,String bean) {
if ( context == null && isSpringContext) {
LOGGER.warning( "Extension lookup '"+bean+"', but ApplicationContext is unset.");
}
}
/**
* Looks up for a named string property in the order defined by
* {@link #getProperty(String, ApplicationContext)} using the internally cached spring
* application context.
* <p>
* Care should be taken when using this method. It should not be called during startup or from
* tests cases as the internal context will not have been set.
* </p>
* @param propertyName The property name to lookup.
*
* @return The property value, or null if not found
*/
public static String getProperty(String propertyName) {
return getProperty(propertyName, context);
}
/**
* Looks up for a named string property into the following contexts (in order):
* <ul>
* <li>System Property</li>
* <li>web.xml init parameters (only works if the context is a {@link WebApplicationContext}</li>
* <li>Environment variable</li>
* </ul>
* and returns the first non null, non empty value found.
* @param propertyName The property name to be searched
* @param context The Spring context (may be null)
* @return The property value, or null if not found
*/
public static String getProperty(String propertyName, ApplicationContext context) {
if (context instanceof WebApplicationContext) {
return getProperty(propertyName, ((WebApplicationContext) context).getServletContext());
} else {
return getProperty(propertyName, (ServletContext) null);
}
}
/**
* Looks up for a named string property into the following contexts (in order):
* <ul>
* <li>Test override supplied by GeoServerExtensionsHelper</li>
* <li>System Property</li>
* <li>web.xml init parameters</li>
* <li>Environment variable</li>
* </ul>
* and returns the first non null, non empty value found.
* @param propertyName The property name to be searched
* @param context The servlet context used to look into web.xml (may be null)
* @return The property value, or null if not found
*/
public static String getProperty(String propertyName, ServletContext context) {
// TODO: this code comes from the data directory lookup and it's useful
// until we provide a way for the user to manually inspect the three contexts
// (when trying to debug why the variable they think they've set, and so on, see also
// https://osgeo-org.atlassian.net/browse/GEOS-2343
// Once that is fixed, we can remove the logging code that makes this method more complex
// than strictly necessary
final String[] typeStrs = { "Property override","Java environment variable ", "Servlet context parameter ",
"System environment variable " };
String result = null;
for (int j = 0; j < typeStrs.length; j++) {
// Lookup section
switch (j) {
case 0:
result = propertyCache.get(propertyName);
break;
case 1:
result = System.getProperty(propertyName);
break;
case 2:
if (context != null) {
result = context.getInitParameter(propertyName);
}
break;
case 3:
result = System.getenv(propertyName);
break;
}
if (result == null || result.equalsIgnoreCase("")) {
LOGGER.finer("Found " + typeStrs[j] + ": '" + propertyName + "' to be unset");
} else {
break;
}
}
return result;
}
/**
* Search the context for indicated file.
*
* Example:
* <pre><code>
* File webXML = GeoServerExtensions.file("WEB-INF/web.xml");
* </code></pre>
* @param path File name to search for
* @return Requested file, or null if not found
*/
public static File file(String path) {
if( fileCache.containsKey(path) ){
return fileCache.get(path); // override provided by GeoServerExtensionsHelper
}
if( context instanceof WebApplicationContext){
ServletContext servletContext = ((WebApplicationContext)context).getServletContext();
String filepath = servletContext.getRealPath( path );
if( filepath != null ){
File file = new File( filepath );
if( file.exists() ){
return file;
}
}
else {
List<String> items = Paths.names(path);
int index = 0;
if( index < items.size() ){
filepath = servletContext.getRealPath( items.get(index) );
index++;
if( filepath != null ){
File file = new File(filepath);
while(index < items.size() ){
file = new File( file, items.get(index) );
index++;
}
return file;
}
}
}
}
return null; // unavaialble
}
}