/* (c) 2014 - 2016 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.wfs.xslt; import java.io.IOException; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.apache.commons.beanutils.BeanUtils; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.util.EcoreUtil; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.config.GeoServer; import org.geoserver.ows.Dispatcher; import org.geoserver.ows.Request; import org.geoserver.ows.Response; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.Operation; import org.geoserver.platform.ServiceException; import org.geoserver.wfs.WFSException; import org.geoserver.wfs.WFSGetFeatureOutputFormat; import org.geoserver.wfs.request.FeatureCollectionResponse; import org.geoserver.wfs.request.GetFeatureRequest; import org.geoserver.wfs.xslt.config.TransformInfo; import org.geoserver.wfs.xslt.config.TransformRepository; import org.geotools.feature.FeatureCollection; import org.opengis.feature.Feature; import org.opengis.feature.type.FeatureType; import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; /** * Output format based on XLST transformations * * @author Andrea Aime - GeoSolutions */ public class XSLTOutputFormat extends WFSGetFeatureOutputFormat implements ApplicationContextAware, DisposableBean { static Map<String, String> formats = new ConcurrentHashMap<String, String>(); ExecutorService executor = Executors.newCachedThreadPool(); private TransformRepository repository; private List<Response> responses; public XSLTOutputFormat(GeoServer gs, TransformRepository repository) { // initialize with the key set of formats, so that it will change as // we register new formats super(gs, formats.keySet()); this.repository = repository; } @Override public boolean canHandle(Operation operation) { // if we don't have formats configured, we cannot respond if(formats.isEmpty()) { LOGGER.log(Level.FINE, "Empty formats"); return false; } if(!super.canHandle(operation)) { return false; } // check the format matches, the Dispatcher just does a case insensitive match, // but WFS is supposed to be case sensitive and so is the XSLT code Request request = Dispatcher.REQUEST.get(); if(request != null && (request.getOutputFormat() == null || !formats.containsKey(request.getOutputFormat()))) { LOGGER.log(Level.FINE, "Formats are: " + formats); return false; } else { return true; } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // find all the responses we could use as a source List<Response> all = GeoServerExtensions.extensions(Response.class, applicationContext); responses = new ArrayList<Response>(); for (Response response : all) { if (response.getBinding().equals(FeatureCollectionResponse.class) && response != this) { responses.add(response); } } } public static void updateFormats(Set<String> newFormats) { if (!formats.equals(newFormats)) { Map<String, String> replacement = new HashMap<String, String>(); for (String format : newFormats) { replacement.put(format, format); } formats.clear(); formats.putAll(replacement); } } @Override public String getMimeType(Object value, Operation operation) throws ServiceException { try { TransformInfo info = locateTransformation((FeatureCollectionResponse) value, operation); return info.mimeType(); } catch(IOException e) { throw new WFSException("Failed to load the required transformation", e); } } @Override public String getAttachmentFileName(Object value, Operation operation) { try { FeatureCollectionResponse featureCollections = (FeatureCollectionResponse) value; TransformInfo info = locateTransformation(featureCollections, operation); // concatenate all feature types requested StringBuilder sb = new StringBuilder(); for (FeatureCollection<FeatureType, Feature> fc : featureCollections.getFeatures()) { sb.append(fc.getSchema().getName().getLocalPart()); sb.append("_"); } sb.setLength(sb.length() - 1); String extension = info.getFileExtension(); if(extension == null) { extension = ".txt"; sb.append(extension); } if(!extension.startsWith(".")) { sb.append("."); } sb.append(extension); return sb.toString(); } catch(IOException e) { throw new WFSException("Failed to locate the XSLT transformation", e); } } @Override protected void write(final FeatureCollectionResponse featureCollection, OutputStream output, Operation operation) throws IOException, ServiceException { // get the transformation we need TransformInfo info = locateTransformation(featureCollection, operation); Transformer transformer = repository.getTransformer(info); // force Xalan to indent the output if(transformer.getOutputProperties() != null && "yes".equals(transformer.getOutputProperties().getProperty("indent"))) { try { transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); } catch(IllegalArgumentException e) { LOGGER.log(Level.FINE, "Could not set indent amount", e); // in case it's not Xalan } } // prepare the fake operation we're providing to the source output format final Operation sourceOperation = buildSourceOperation(operation, info); // lookup the operation we are going to use final Response sourceResponse = findSourceResponse(sourceOperation, info); if (sourceResponse == null) { throw new WFSException( "Could not locate a response that can generate the desired source format '" + info.getSourceFormat() + "' for transformation '" + info.getName() + "'"); } // prepare the stream connections, so that we can do the transformation on the fly PipedInputStream pis = new PipedInputStream(); final PipedOutputStream pos = new PipedOutputStream(pis); // submit the source output format execution, tracking exceptions Future<Void> future = executor.submit(new Callable<Void>() { @Override public Void call() throws Exception { try { sourceResponse.write(featureCollection, pos, sourceOperation); } finally { // close the stream to make sure the transformation won't keep on waiting pos.close(); } return null; } }); // run the transformation TransformerException transformerException = null; try { transformer.transform(new StreamSource(pis), new StreamResult(output)); } catch (TransformerException e) { transformerException = e; } finally { pis.close(); } // now handle exceptions, starting from the source try { future.get(); } catch (Exception e) { throw new WFSException( "Failed to run the output format generating the source for the XSTL transformation", e); } if (transformerException != null) { throw new WFSException("Failed to run the the XSTL transformation", transformerException); } } private Operation buildSourceOperation(Operation operation, TransformInfo info) { try { EObject originalParam = (EObject) operation.getParameters()[0]; EObject copy = EcoreUtil.copy(originalParam); BeanUtils.setProperty(copy, "outputFormat", info.getSourceFormat()); final Operation sourceOperation = new Operation(operation.getId(), operation.getService(), operation.getMethod(), new Object[] { copy }); return sourceOperation; } catch (Exception e) { throw new WFSException( "Failed to create the source operation for XSLT, this is unexpected", e); } } private Response findSourceResponse(Operation sourceOperation, TransformInfo info) { for (Response r : responses) { if (r.getOutputFormats().contains(info.getSourceFormat()) && r.canHandle(sourceOperation)) { return r; } } return null; } private TransformInfo locateTransformation(FeatureCollectionResponse collections, Operation operation) throws IOException { GetFeatureRequest req = GetFeatureRequest.adapt(operation.getParameters()[0]); String outputFormat = req.getOutputFormat(); // locate the transformation, and make sure it's the same for all feature types Set<FeatureType> featureTypes = getFeatureTypes(collections); TransformInfo result = null; FeatureType reference = null; for (FeatureType ft : featureTypes) { TransformInfo curr = locateTransform(outputFormat, ft); if (curr == null) { throw new WFSException("Could not find a XSLT transformation generating " + outputFormat + " for feature type " + ft.getName(), ServiceException.INVALID_PARAMETER_VALUE, "typeName"); } else if (result == null) { reference = ft; result = curr; } else if (!result.equals(curr)) { throw new WFSException( "Multiple feature types are mapped to different XLST transformations, cannot proceed: " + result.getXslt() + ", " + curr.getXslt(), ServiceException.INVALID_PARAMETER_VALUE, "typeName"); } } return result; } private TransformInfo locateTransform(String outputFormat, FeatureType ft) throws IOException { // first lookup the type specific transforms FeatureTypeInfo info = gs.getCatalog().getFeatureTypeByName(ft.getName()); TransformInfo result = null; if (info != null) { List<TransformInfo> transforms = repository.getTypeTransforms(info); result = filterByOutputFormat(outputFormat, transforms); } // we don't have a type specific one, look for a global one instead if (result == null) { List<TransformInfo> transforms = repository.getGlobalTransforms(); result = filterByOutputFormat(outputFormat, transforms); } return result; } private TransformInfo filterByOutputFormat(String outputFormat, List<TransformInfo> transforms) { for (TransformInfo tx : transforms) { if (outputFormat.equals(tx.getOutputFormat())) { return tx; } } return null; } private Set<FeatureType> getFeatureTypes(FeatureCollectionResponse collections) { Set<FeatureType> result = new HashSet<FeatureType>(); for (FeatureCollection<FeatureType, Feature> fc : collections.getFeatures()) { result.add(fc.getSchema()); } return result; } @Override public void destroy() throws Exception { // get rid of the execution service if (executor != null) { executor.shutdown(); executor.awaitTermination(10, TimeUnit.SECONDS); executor = null; } } @Override public List<String> getCapabilitiesElementNames() { return getAllCapabilitiesElementNames(); } }