package org.geowebcache;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import javax.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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 GeoWebCache extension points.
* <p>
* An instance of this class needs to be registered in spring context as follows. <code>
* <pre>
* <bean id="gwowebCacheExtensions" class="org.geowebcache.GeoWebCacheExtensions"/>
* </pre>
* </code> It must be a singleton, and must not be loaded lazily. Furthermore, this bean must be
* loaded before any beans that use it.
* </p>
* <p>
* Priority will be respected for extensions that implement {@link GeoWebCacheExtensionPriority} interface.
* </p>
*
* @author Gabriel Roldan based on GeoServer's {@code GeoServerExtensions}
*
*/
public class GeoWebCacheExtensions implements ApplicationContextAware, ApplicationListener {
/**
* logger
*/
public static Log LOGGER = LogFactory.getLog(GeoWebCacheExtensions.class);
/**
* 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 WeakHashMap<Class<?>, String[]> extensionsCache = new WeakHashMap<Class<?>, String[]>(40);
/**
* 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 dont supply their own context.
* </p>
*/
public void setApplicationContext(ApplicationContext context) throws BeansException {
GeoWebCacheExtensions.context = context;
extensionsCache.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) {
String[] names;
if (GeoWebCacheExtensions.context == context) {
names = extensionsCache.get(extensionPoint);
} else {
names = null;
}
if (names == null) {
checkContext(context);
if (context != null) {
try {
names = getBeansNamesOrderedByPriority(extensionPoint, context);
// update cache only if dealing with the same context
if (GeoWebCacheExtensions.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.error("bean lookup error", e);
return Collections.emptyList();
}
} else {
return Collections.emptyList();
}
}
// look up all the beans
List<T> result = new ArrayList<T>(names.length);
for (String name : names) {
T bean = (T) context.getBean(name);
result.add(bean);
}
return result;
}
/**
* We lookup all the beans names that correspond to the provided extension type. If the provided
* extensions type implements the {@link GeoWebCacheExtensionPriority} interface we sort the beans
* names by their priority.
*
* We return the bean names and not the beans themselves because we cache the beans by name rather
* than the bean itself to avoid breaking the singleton directive.
*/
@SuppressWarnings("unchecked")
private static <T> String[] getBeansNamesOrderedByPriority(Class<T> extensionType, ApplicationContext context) {
// asking spring for a map that will contains all beans that match the extensions type indexed by their name
Map<String, T> beans = context.getBeansOfType(extensionType);
if (!GeoWebCacheExtensionPriority.class.isAssignableFrom(extensionType)) {
// no priority so nothing ot do
return beans.keySet().toArray(new String[beans.size()]);
}
// this extension type is priority aware
List<Map.Entry<String, T>> beansEntries = new ArrayList<>(beans.entrySet());
// sorting beans by their priority
Collections.sort(beansEntries, (extensionA, extensionB) -> {
GeoWebCacheExtensionPriority extensionPriorityA = ((Map.Entry<String, GeoWebCacheExtensionPriority>) extensionA).getValue();
GeoWebCacheExtensionPriority extensionPriorityB = ((Map.Entry<String, GeoWebCacheExtensionPriority>) extensionB).getValue();
if (extensionPriorityA.getPriority() < extensionPriorityB.getPriority()) {
return -1;
}
return extensionPriorityA.getPriority() == extensionPriorityB.getPriority() ? 0 : 1;
});
// returning only the beans names
return beansEntries.stream().map(Map.Entry::getKey).toArray(String[]::new);
}
/**
* 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
* @return
*/
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);
return context != null ? context.getBean(name) : null;
}
/**
* 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 IllegalArgumentException
* 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);
return context != null ? bean(type, context) : null;
}
/**
* 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 IllegalArgumentException
* 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 IllegalArgumentException("Multiple beans of type " + type.getName());
}
return beans.get(0);
}
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent)
extensionsCache.clear();
}
/**
* Checks the context, if null will issue a warning.
*/
static void checkContext(ApplicationContext context) {
if (context == null) {
LOGGER.fatal("Extension lookup occured, 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>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 as
// long as we don't provide a way for the user to manually inspect the three contexts
// (when trying to debug why the variable they thing they've set, and so on, see also
// http://jira.codehaus.org/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 = { "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 = System.getProperty(propertyName);
break;
case 1:
if (context != null) {
result = context.getInitParameter(propertyName);
}
break;
case 2:
result = System.getenv(propertyName);
break;
}
if (result == null || result.equalsIgnoreCase("")) {
LOGGER.trace("Found " + typeStrs[j] + ": '" + propertyName + "' to be unset");
} else {
break;
}
}
return result;
}
}