/* (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;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableSet;
import java.util.Properties;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.commons.io.IOUtils;
import org.geoserver.ManifestLoader.AboutModel.ManifestModel;
import org.geoserver.config.GeoServer;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Paths;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resource.Type;
import org.geoserver.platform.resource.Resources;
import org.geotools.factory.GeoTools;
import org.geotools.util.logging.Logging;
/**
* @author cancellieri carlo - GeoSolutions SAS
*
*/
public class ManifestLoader {
private static final Logger LOGGER = Logging.getLogger(ManifestLoader.class.toString());
// SETTIMGS
public final static String RESOURCE_NAME_REGEX = "resourceNameRegex";
public final static String RESOURCE_ATTRIBUTE_EXCLUSIONS = "resourceAttributeExclusions";
public final static String VERSION_ATTRIBUTE_INCLUSIONS = "versionAttributeInclusions";
public final static String PROPERTIES_FILE = "manifest.properties";
// loaded settings form PROPERTIES_FILE
private static Properties props;
private static Pattern resourceNameRegex;
private static String resourceAttributeExclusions[];
private static String versionAttributeInclusions[];
private static ClassLoader classLoader;
/**
* @throws Exception
*
*/
public ManifestLoader(GeoServerResourceLoader loader) throws Exception {
classLoader = loader.getClassLoader();
props = new Properties();
// load from jar or classpath
InputStream is = null;
try {
is = classLoader.getResourceAsStream("org/geoserver/" + PROPERTIES_FILE);
if (is != null) {
props.load(is);
}
} catch (IOException e) {
LOGGER.log(Level.FINER, e.getMessage(), e);
} finally {
IOUtils.closeQuietly(is);
}
// override settings from datadir
try {
// datadir search
Resource resource = loader.get( PROPERTIES_FILE );
if (resource.getType() == Type.RESOURCE) {
is = resource.in();
props.load(is);
}
} catch (IOException e2) {
LOGGER.log(Level.FINER, e2.getMessage(), e2);
} finally {
IOUtils.closeQuietly(is);
}
try {
resourceNameRegex = Pattern.compile(props.getProperty(RESOURCE_NAME_REGEX)
+ "!/META-INF/MANIFEST.MF");
} catch (PatternSyntaxException e) {
LOGGER.log(java.util.logging.Level.SEVERE, e.getLocalizedMessage(), e);
throw e;
}
String ae = props.getProperty(RESOURCE_ATTRIBUTE_EXCLUSIONS);
if (ae != null) {
resourceAttributeExclusions = ae.split(",");
} else {
resourceAttributeExclusions = new String[0];
}
String ai = props.getProperty(VERSION_ATTRIBUTE_INCLUSIONS);
if (ai != null) {
versionAttributeInclusions = ai.split(",");
} else {
// defaults
throw new Exception("Include attribute array cannot be null");
}
}
/**
* load an about model
*
* @param loader
*
* @throws IllegalArgumentException if arguments are null
*/
private static AboutModel getAboutModel(final ClassLoader loader)
throws IllegalArgumentException {
if (loader == null) {
throw new IllegalArgumentException("Unable to run with null arguments");
}
final AboutModel model = new AboutModel();
Map<String, Manifest> manifests = loadManifest(loader);
Iterator<java.util.Map.Entry<String, Manifest>> it = manifests.entrySet().iterator();
while (it.hasNext()) {
java.util.Map.Entry<String, Manifest> entry = it.next();
model.add(ManifestModel.parseManifest(trimName(entry.getKey()), entry.getValue(),
new ManifestModel.ExcludeAttributeFilter(resourceAttributeExclusions)));
}
return model;
}
private static Map<String, Manifest> loadManifest(final ClassLoader loader)
throws IllegalArgumentException {
if (loader == null) {
throw new IllegalArgumentException("Unable to run with null arguments");
}
Map<String, Manifest> manifests = new HashMap<String, Manifest>();
try {
Enumeration<URL> resources = loader.getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
InputStream is = null;
try {
URL resource = resources.nextElement();
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Loading resources: " + resource.getFile());
is = resource.openStream();
manifests.put(resource.getPath(), new Manifest(is));
} catch (IOException e) {
// handle
LOGGER.log(java.util.logging.Level.SEVERE,
"Error loading resources file: " + e.getLocalizedMessage(), e);
} finally {
IOUtils.closeQuietly(is);
}
}
} catch (IOException e) {
LOGGER.log(java.util.logging.Level.SEVERE,
"Error loading resources file: " + e.getLocalizedMessage(), e);
}
return manifests;
}
private static String trimName(String path) {
Matcher m = resourceNameRegex.matcher(path);
if (m.matches())
return m.group(1);
else {
String name = path.substring(0, path.length() - 22);
return name.substring(name.lastIndexOf('/') + 1);
}
}
/**
* @return load the AboutModel of all the loaded resources
*/
public static AboutModel getResources() {
return getAboutModel(classLoader);
}
public static Manifest getManifest(Class<?> clz) {
String resource = "/" + clz.getName().replace(".", "/") + ".class";
String fullPath = clz.getResource(resource).toString();
String archivePath = fullPath.substring(0, fullPath.length() - resource.length());
if (archivePath.endsWith("\\WEB-INF\\classes") || archivePath.endsWith("/WEB-INF/classes")) {
archivePath = archivePath.substring(0, archivePath.length() - "/WEB-INF/classes".length()); // Required for wars
}
try (InputStream input = new URL(archivePath + "/META-INF/MANIFEST.MF").openStream()) {
return new Manifest(input);
} catch (Exception e) {
throw new RuntimeException("Loading MANIFEST for class " + clz + " failed!", e);
}
}
/**
* @return dynamically built AboutModel of the geoserver's versions
*/
public static AboutModel getVersions() {
if (classLoader == null) {
throw new IllegalArgumentException("Unable to run with null classLoader");
}
// load metadata
Map<String, Manifest> manifests = ManifestLoader.loadManifest(classLoader);
// start building the model
AboutModel model = new AboutModel();
try {
// prepare the GeoServer metadata key
String geoserverPath = GeoServer.class.getProtectionDomain().getCodeSource()
.getLocation().toURI().toString();
geoserverPath = geoserverPath + "!/META-INF/MANIFEST.MF";
Class geoserver_class = GeoServer.class;
Manifest manifest = ManifestLoader.getManifest(geoserver_class);
if (manifest != null) {
model.add(ManifestModel.parseManifest("GeoServer", manifest,
new ManifestModel.IncludeAttributeFilter(versionAttributeInclusions)));
}
} catch (Exception e) {
// be safe
LOGGER.log(Level.FINE, "Error looking up geoserver package", e);
}
try {
// prepare the GeoTools metadata key
String path = GeoTools.class.getProtectionDomain().getCodeSource().getLocation()
.toURI().toString();
path = path + "!/META-INF/MANIFEST.MF";
Class geoserver_class = GeoTools.class;
Manifest manifest = ManifestLoader.getManifest(geoserver_class);
if (manifest != null) {
model.add(ManifestModel.parseManifest("GeoTools", manifest,
new ManifestModel.IncludeAttributeFilter(versionAttributeInclusions)));
}
// ManifestModel manifest = new ManifestModel("GeoTools");
// manifest.putEntry("Version", GeoTools.getVersion().toString());
// manifest.putEntry("Git-Revision", GeoTools.getBuildRevision().toString());
// manifest.putEntry("Build-Timestamp", GeoTools.getBuildTimestamp());
// model.add(manifest);
} catch (Exception e) {
// be safe
LOGGER.log(Level.FINE, "Error looking up geoserver package", e);
}
try {
// prepare the GeoWebCache metadata key
String path = Class.forName("org.geowebcache.GeoWebCache").getProtectionDomain().getCodeSource().getLocation()
.toURI().toString();
path = path + "!/META-INF/MANIFEST.MF";
Class geoserver_class = Class.forName("org.geowebcache.GeoWebCache");
Manifest manifest = ManifestLoader.getManifest(geoserver_class);
if (manifest != null) {
model.add(ManifestModel.parseManifest("GeoWebCache", manifest,
new ManifestModel.IncludeAttributeFilter(versionAttributeInclusions)));
}
// Package p = GeoWebCache.class.getPackage();
// if (p != null) {
// ManifestModel manifest = new ManifestModel("GeoWebCache");
// manifest.putEntry("Version",
// p.getSpecificationVersion() != null ? p.getSpecificationVersion() : "");
// manifest.putEntry("Git-Revision",
// p.getImplementationVersion() != null ? p.getImplementationVersion() : "");
// model.add(manifest);
// }
} catch (Exception e) {
// be safe
LOGGER.log(Level.FINE, "Error looking up org.geowebcache package", e);
}
return model;
}
/**
* This is the model used to store resources from the class loader.
*
* @author Cancellieri Carlo - GeoSolutions SAS
*/
public static class AboutModel {
private TreeSet<ManifestModel> manifests;
/**
* Type for the Model:<br>
* {@link AboutModelType#VERSIONS} - means this model contains versions<br>
* {@link AboutModelType#RESOURCES} - means this model contains resources
*/
public enum AboutModelType {
VERSIONS, RESOURCES;
}
public AboutModel() {
manifests = new TreeSet<ManifestModel>(new ManifestModel.ManifestComparator());
}
/**
*
* @param am
* @throws IllegalArgumentException
*/
public AboutModel(AboutModel am) throws IllegalArgumentException {
if (am == null) {
throw new IllegalArgumentException("Unable to initialize model with a null model");
}
manifests = new TreeSet<ManifestModel>(am.getManifests());
}
private AboutModel(NavigableSet<ManifestModel> manifests) throws IllegalArgumentException {
if (manifests == null) {
throw new IllegalArgumentException(
"Unable to initialize model with a null manifests tree");
}
this.manifests = new TreeSet<ManifestModel>(manifests);
}
/**
* Filter resources from the used model generating a new one containing only resources having the name between from and to string.<br>
* Note that objects are shared between models so changes to objects in the filtered model will also affect the current model.
*
* @param from
* @param to
* @return the filtered model
* @throws IllegalArgumentException if from or to are null
*/
public AboutModel filterNameByRange(String from, String to) throws IllegalArgumentException {
if (from == null || to == null) {
throw new IllegalArgumentException("Unable to parse from or to are null");
}
return new AboutModel(getManifests().subSet(new ManifestModel(from), true,
new ManifestModel(to), true));
}
/**
* Filter resources from the used model generating a new one containing only resources having the name matching the passed regular expression.<br>
* Note that objects are shared between models so changes to objects in the filtered model will also affect the current model.
*
* @param regex regular expression
* @return a filtered model
* @throws IllegalArgumentException if the regex is null
*/
public AboutModel filterNameByRegex(String regex) throws IllegalArgumentException {
if (regex == null) {
throw new IllegalArgumentException("Unable to parse regex is null");
}
AboutModel am = new AboutModel(new TreeSet<ManifestModel>(
new ManifestModel.ManifestComparator()));
Iterator<ManifestModel> it = manifests.iterator();
while (it.hasNext()) {
ManifestModel tModel = it.next();
// filter over properties
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(tModel.getName());
if (tModel.getName().matches(regex)) {
am.getManifests().add(tModel);
}
}
return am;
}
/**
* Filter resources from the used model generating a new one containing only resources having a key matching the passed string.<br>
* Note that objects are shared between models so changes to objects in the filtered model will also affect the current model.
*
* @param key the key to match
* @return a filtered model
* @throws IllegalArgumentException if the key is null
*/
public AboutModel filterPropertyByKey(String key) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("Unable to parse key is null");
}
AboutModel am = new AboutModel();
Iterator<ManifestModel> it = manifests.iterator();
while (it.hasNext()) {
ManifestModel tModel = it.next();
if (filterPropertyByKey(tModel, key)) {
am.getManifests().add(tModel);
}
}
return am;
}
/**
*
* Filter resources from the used model generating a new one containing only resources having a property matching the passed string.<br>
* Note that objects are shared between models so changes to objects in the filtered model will also affect the current model.
*
* @param value the value of the property
* @return the filtered model
* @throws IllegalArgumentException if the value is null
*/
public AboutModel filterPropertyByValue(final String value) throws IllegalArgumentException {
if (value == null) {
throw new IllegalArgumentException("Unable to parse: value is null");
}
AboutModel am = new AboutModel();
Iterator<ManifestModel> it = manifests.iterator();
while (it.hasNext()) {
ManifestModel tModel = it.next();
if (filterByPropertyValue(tModel, value)) {
am.getManifests().add(tModel);
}
}
return am;
}
/**
*
* Filter resources from the used model generating a new one containing only resources having a property key matching the passed key string
* with a value matching the passed value string.<br>
* Note that objects are shared between models so changes to objects in the filtered model will also affect the current model.
*
* @param value the value of the property
* @param key the name of the property
* @return the filtered model
* @throws IllegalArgumentException if the key or the value are null
*/
public AboutModel filterPropertyByKeyValue(final String value, final String key)
throws IllegalArgumentException {
if (value == null && key == null) {
throw new IllegalArgumentException("Unable to parse: property or key are null");
}
AboutModel am = new AboutModel();
Iterator<ManifestModel> it = manifests.iterator();
while (it.hasNext()) {
ManifestModel tModel = it.next();
if (filterPropertyByKeyValue(tModel, key, value)) {
am.getManifests().add(tModel);
}
}
return am;
}
private boolean filterPropertyByKeyValue(final ManifestModel tModel, final String key,
final String value) {
// filter over properties
for (Entry<String, String> e : tModel.getEntries().entrySet()) {
if (e.getKey().matches(key) && e.getValue().matches(value)) {
// property mane matches
return true;
}
}
return false;
}
private boolean filterPropertyByKey(final ManifestModel tModel, final String key) {
// filter over properties
for (Entry<String, String> e : tModel.getEntries().entrySet()) {
if (e.getKey().matches(key)) {
// property mane matches
return true;
}
}
return false;
}
private boolean filterByPropertyValue(final ManifestModel tModel, final String value) {
// filter over values matches
for (Entry<String, String> e : tModel.getEntries().entrySet()) {
if (e.getValue().matches(value)) {
// property mane matches
return true;
}
}
return false;
}
/**
* Add a manifest file as resource with the given name
*
* @param name
* @param manifest
* @return true if this set did not already contain the specified name
*/
public boolean add(final String name, final Manifest manifest) {
return manifests.add(ManifestModel.parseManifest(name, manifest,
new ManifestModel.ExcludeAttributeFilter(resourceAttributeExclusions)));
}
/**
* Add a manifest file as resource
*
* @param manifest
* @return true if this set did not already contain the specified name
*/
public boolean add(final ManifestModel manifest) {
return manifests.add(manifest);
}
/**
* remove the resource named 'name'
*
* @param name (if null false is returned)
* @return true if this set contained the specified element
*/
public boolean remove(final String name) {
if (name != null) {
return manifests.remove(new ManifestModel(name));
}
return false;
}
public TreeSet<ManifestModel> getManifests() {
return manifests;
}
/**
* This is the model used to store one resource from the class loader.
*
* @author Cancellieri Carlo - GeoSolutions SAS
*
*/
public static class ManifestModel {
private final String name;
private final Map<String, String> entries;
/**
* A comparator useful to compare {@link ManifestModel}s by name
*/
public static class ManifestComparator implements Comparator<ManifestModel> {
@Override
public int compare(ManifestModel o1, ManifestModel o2) {
return o1.getName().compareTo(o2.getName());
}
}
/**
* @return the name of the model
*/
public String getName() {
return name;
}
public Map<String, String> getEntries() {
return entries;
}
public ManifestModel(final String name) {
this.name = name;
this.entries = new HashMap<String, String>();
}
public void putAllEntries(Map<String, String> entries) {
this.entries.putAll(entries);
}
public void putEntry(final String name, final String value) {
entries.put(name, value);
}
/**
* A parser for {@link Manifest} bean which generates {@link ManifestModel}s
*
* @param name the name to assign to the generated model
* @param m the manifest bean to load
* @return the generated model
*/
private static ManifestModel parseManifest(final String name, final Manifest manifest,
final AttributesFilter<Map<String, String>> filter) {
final ManifestModel m = new ManifestModel(name);
// Main attributes
try {
m.putAllEntries(filter.filter(manifest.getMainAttributes()));
} catch (Exception e1) {
LOGGER.log(Level.FINER, e1.getMessage(), e1);
}
//
Map<String, Attributes> attrs = manifest.getEntries();
for (java.util.Map.Entry<String, Attributes> entry : attrs.entrySet()) {
try {
m.putAllEntries(filter.filter(entry.getValue()));
} catch (Exception e) {
LOGGER.log(Level.FINER, e.getMessage(), e);
}
}
return m;
}
/**
* Interface used to define Attributes filter in {@link ManifestModel#parseManifest(String, Manifest, AttributesFilter)}
*
* @author cancellieri
*
* @param <T> the type return for the filter function
*/
public interface AttributesFilter<T> {
T filter(final Attributes at) throws Exception;
}
/**
* INTERSECTION: create a map of properties from an attributes including only those matching the include array elements<br>
*
* This implementation also supports attribute renaming using into the include array the pattern:<br>
* include= { "attrName1:replaceName1", "attrName2:replaceName2", ...}<br>
*
*/
public static class IncludeAttributeFilter implements
AttributesFilter<Map<String, String>> {
private final String[] include;
public IncludeAttributeFilter(final String[] include) {
super();
this.include = include;
}
@Override
public Map<String, String> filter(final Attributes at) throws Exception {
return filterIncludingAttributes(at, include);
}
/**
* @param at
* @param include
* @return a map of properties
*/
private static Map<String, String> filterIncludingAttributes(final Attributes at,
String[] include) {
if (at == null)
throw new IllegalArgumentException("Null argument");
Map<String, String> ret = new HashMap<String, String>();
if (include == null) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "No includes: including all");
final Iterator<java.util.Map.Entry<Object, Object>> it = at.entrySet()
.iterator();
while (it.hasNext()) {
java.util.Map.Entry<Object, Object> entry = it.next();
String attrName = ((Attributes.Name) entry.getKey()).toString();
ret.put(attrName, entry.getValue().toString());
}
} else {
// for each attribute
final Iterator<java.util.Map.Entry<Object, Object>> it = at.entrySet()
.iterator();
while (it.hasNext()) {
java.util.Map.Entry<Object, Object> entry = it.next();
String attrName = ((Attributes.Name) entry.getKey()).toString();
// search into including array to filter over attributes
int i = 0;
while (i < include.length) {
// split key in original_key:replace_key
String key[] = include[i++].split(":");
if (attrName.matches(key[0]) == true) {
ret.put(key.length > 1 ? key[1] : key[0], entry.getValue()
.toString());
break;
}
}
}
}
return ret;
}
}
/**
* COMPLEMENT: create a map of properties from an attributes excluding those matching the exclude array elements
*/
public static class ExcludeAttributeFilter implements
AttributesFilter<Map<String, String>> {
private final String[] exclude;
public ExcludeAttributeFilter(final String[] exclude) {
super();
this.exclude = exclude;
}
@Override
public Map<String, String> filter(final Attributes at) throws Exception {
return filterExcludingAttributes(at, exclude);
}
/**
* @param at the attribute to parse
* @param exclude the list of properties to exlude
* @return a map
*/
private static Map<String, String> filterExcludingAttributes(final Attributes at,
String[] exclude) {
if (at == null)
throw new IllegalArgumentException("Null arguments");
if (exclude == null) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "No exceptions");
exclude = new String[0];
}
Map<String, String> ret = new HashMap<String, String>();
// for each attribute
final Iterator<Object> it = at.keySet().iterator();
while (it.hasNext()) {
String attrName = ((Attributes.Name) it.next()).toString();
boolean skip = false;
// search into including array to filter over attributes
int i = 0;
while (i < exclude.length) {
if (attrName.matches(exclude[i++]) == true) {
skip = true;
break;
}
}
if (!skip)
ret.put(attrName, at.getValue(attrName));
}
return ret;
}
}
}
}
}