org.geoserver.wfs.xslt.XSLTOutputFormat.java Source code

Java tutorial

Introduction

Here is the source code for org.geoserver.wfs.xslt.XSLTOutputFormat.java

Source

/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved.
 * 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()) {
            System.out.println("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()))) {
            System.out.println("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();
    }
}