Package org.vfny.geoserver.wms.responses

Source Code of org.vfny.geoserver.wms.responses.GetMapResponse

/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org.  All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package org.vfny.geoserver.wms.responses;

import java.io.IOException;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.geoserver.config.GeoServer;
import org.geoserver.config.ServiceInfo;
import org.geoserver.data.util.CoverageUtils;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.MapLayerInfo;
import org.geoserver.wms.WMSExtensions;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.data.DefaultQuery;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.data.QueryCapabilities;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.function.EnvFunction;
import org.geotools.map.DefaultMapLayer;
import org.geotools.map.FeatureSourceMapLayer;
import org.geotools.map.MapLayer;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.resources.coverage.FeatureUtilities;
import org.geotools.styling.FeatureTypeConstraint;
import org.geotools.styling.Style;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.vfny.geoserver.Request;
import org.vfny.geoserver.Response;
import org.vfny.geoserver.wms.GetMapProducer;
import org.vfny.geoserver.wms.RasterMapProducer;
import org.vfny.geoserver.wms.WMSMapContext;
import org.vfny.geoserver.wms.WmsException;
import org.vfny.geoserver.wms.requests.GetMapRequest;
import org.vfny.geoserver.wms.responses.map.metatile.MetatileMapProducer;

import com.vividsolutions.jts.geom.Envelope;

/**
* A GetMapResponse object is responsible of generating a map based on a GetMap
* request. The way the map is generated is independent of this class, wich will
* use a delegate object based on the output format requested
*
* @author Gabriel Roldan, Axios Engineering
* @author Simone Giannecchini - GeoSolutions SAS
* @version $Id: GetMapResponse.java 14049 2010-02-24 20:09:49Z aaime $
*/
public class GetMapResponse implements Response {
    /** DOCUMENT ME! */
    static final Logger LOGGER = org.geotools.util.logging.Logging
            .getLogger(GetMapResponse.class.getPackage().getName());

    private static FilterFactory filterFac = CommonFactoryFinder.getFilterFactory(null);

    /**
     * The map producer that will be used for the production of a map in the
     * requested format.
     */
    private GetMapProducer delegate;

    /**
     * The map context
     */
    private WMSMapContext map;

    /**
     * custom response headers
     */
    private HashMap responseHeaders;

    String headerContentDisposition;

    private Collection<GetMapProducer> availableProducers;

    /**
     * Creates a new GetMapResponse object.
     *
     * @param availableProducers
     *                the list of available map producers where to get one to handle the request
     *                format at {@link #execute(Request)}
     */
    public GetMapResponse(Collection<GetMapProducer> availableProducers) {
        if(availableProducers == null){
            throw new NullPointerException("availableProducers");
        }
        if(availableProducers.size() == 0){
            throw new IllegalArgumentException("No available map producers provided");
        }
        this.availableProducers = new ArrayList<GetMapProducer>(availableProducers);
        responseHeaders = null;
    }

    /**
     * Returns any extra headers that this service might want to set in the HTTP
     * response object.
     *
     */
    public HashMap getResponseHeaders() {
        return responseHeaders == null? null : new HashMap(responseHeaders);
    }

    /**
     * Implements the map production logic for a WMS GetMap request, delegating
     * the encoding to the appropriate output format to a {@link GetMapProducer}
     * appropriate for the required format.
     *
     * <p>
     * Preconditions:
     * <ul>
     * <li>request.getLayers().length > 0
     * <li>request.getStyles().length == request.getLayers().length
     * </ul>
     * </p>
     *
     * @param req
     *            a {@link GetMapRequest}
     *
     * @throws ServiceException
     *             if an error occurs creating the map from the provided request
     *
     * TODO: This method have become a 300+ lines monster, refactore it to
     * private methods from which names one can inferr what's going on... but
     * get a decent test coverage on it first as to avoid regressions as much as
     * possible
     */
    @SuppressWarnings("unchecked")
    public void execute(Request req) throws ServiceException {
        final GetMapRequest request = (GetMapRequest) req;
        assertMandatory(request);

        final String outputFormat = request.getFormat();

        this.delegate = getDelegate(outputFormat);
        // JD:make instance variable in order to release resources later
        // final WMSMapContext map = new WMSMapContext();
        map = new WMSMapContext(request);
        this.delegate.setMapContext(map);

        final Envelope env = request.getBbox();

        // enable on the fly meta tiling if request looks like a tiled one
        if (MetatileMapProducer.isRequestTiled(request, delegate)) {
            if (LOGGER.isLoggable(Level.FINER)) {
                LOGGER.finer("Tiled request detected, activating on the fly meta tiler");
            }

            this.delegate = new MetatileMapProducer(request, (RasterMapProducer) delegate);
            this.delegate.setMapContext(map);
        }

        final MapLayerInfo[] layers = request.getLayers();
        final Style[] styles = request.getStyles().toArray(new Style[] {});
        final Filter[] filters = buildLayersFilters(request.getFilter(), layers);

        // DJB DONE: replace by setAreaOfInterest(Envelope,
        // CoordinateReferenceSystem)
        // with the user supplied SRS parameter

        // if there's a crs in the request, use that. If not, assume its 4326
        final CoordinateReferenceSystem mapcrs = request.getCrs();

        // DJB: added this to be nicer about the "NONE" srs.
        if (mapcrs != null) {
            map.setAreaOfInterest(env, mapcrs);
        } else {
            map.setAreaOfInterest(env, DefaultGeographicCRS.WGS84);
        }

        map.setMapWidth(request.getWidth());
        map.setMapHeight(request.getHeight());
        map.setAngle(request.getAngle());
        map.setBgColor(request.getBgColor());
        map.setTransparent(request.isTransparent());
        map.setBuffer(request.getBuffer());
        map.setPaletteInverter(request.getPalette());

        // //
        //
        // Check to see if we really have something to display. Sometimes width
        // or height or both are non positivie or the requested area is null.
        //
        // ///
        if ((request.getWidth() <= 0) || (request.getHeight() <= 0)
                || (map.getAreaOfInterest().getLength(0) <= 0)
                || (map.getAreaOfInterest().getLength(1) <= 0)) {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER
                        .fine("We are not going to render anything because either the area is null or the dimensions are not positive.");
            }

            return;
        }

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("setting up map");
        }

        try { // mapcontext can leak memory -- we make sure we done (see
            // finally block)

            // track the external caching strategy for any map layers
            boolean cachingPossible = "GET".equals(request.getHttpServletRequest().getMethod());
            final String featureVersion = request.getFeatureVersion();
            int maxAge = Integer.MAX_VALUE;
            for (int i = 0; i < layers.length; i++) {
                final Style layerStyle = styles[i];
                final Filter layerFilter = filters[i];

                final MapLayer layer;
                if (layers[i].getType() == MapLayerInfo.TYPE_REMOTE_VECTOR) {
                    cachingPossible = false;

                    final FeatureSource<SimpleFeatureType, SimpleFeature> source = layers[i]
                            .getRemoteFeatureSource();
                    layer = new DefaultMapLayer(source, layerStyle);
                    layer.setTitle(layers[i].getRemoteFeatureSource().getSchema().getTypeName());

                    final DefaultQuery definitionQuery = new DefaultQuery(source.getSchema()
                            .getTypeName());
                    definitionQuery.setFilter(layerFilter);
                    definitionQuery.setVersion(featureVersion);
                    int maxFeatures = request.getMaxFeatures() != null ? request.getMaxFeatures()
                            : Integer.MAX_VALUE;
                    definitionQuery.setMaxFeatures(maxFeatures);

                    layer.setQuery(definitionQuery);
                    map.addLayer(layer);
                } else if (layers[i].getType() == MapLayerInfo.TYPE_VECTOR) {
                    if (cachingPossible) {
                        if (layers[i].isCachingEnabled()) {
                            int nma = layers[i].getCacheMaxAge();

                            // suppose the map contains multiple cachable
                            // layers...we can only cache the combined map for
                            // the
                            // time specified by the shortest-cached layer.
                            if (nma < maxAge) {
                                maxAge = nma;
                            }
                        } else {
                            // if one layer isn't cachable, then we can't cache
                            // any of them. Disable caching.
                            cachingPossible = false;
                        }
                    }

                    FeatureSource<? extends FeatureType, ? extends Feature> source;
                    // /////////////////////////////////////////////////////////
                    //
                    // Adding a feature layer
                    //
                    // /////////////////////////////////////////////////////////
                    try {
                        source = layers[i].getFeatureSource(true);

                        // NOTE for the feature. Here there was some code that
                        // sounded like:
                        // * get the bounding box from feature source
                        // * eventually reproject it to the actual CRS used for
                        // map
                        // * if no intersection, don't bother adding the feature
                        // source to the map
                        // This is not an optimization, on the contrary,
                        // computing the bbox may be
                        // very expensive depending on the data size. Using
                        // sigma.openplans.org data
                        // and a tiled client like OpenLayers, it dragged the
                        // server to his knees
                        // and the client simply timed out
                    } catch (IOException exp) {
                        if (LOGGER.isLoggable(Level.SEVERE)) {
                            LOGGER.log(Level.SEVERE, new StringBuffer("Getting feature source: ")
                                    .append(exp.getMessage()).toString(), exp);
                        }

                        throw new WmsException("Internal error", "", exp);
                    }

                    layer = new FeatureSourceMapLayer(source, layerStyle);
                    layer.setTitle(layers[i].getFeature().getName());

                    final DefaultQuery definitionQuery = new DefaultQuery(source.getSchema()
                            .getName().getLocalPart());
                    definitionQuery.setVersion(featureVersion);
                    definitionQuery.setFilter(layerFilter);

                    // check for startIndex + offset
                    final Integer startIndex = request.getStartIndex();
                    if (startIndex != null) {
                        QueryCapabilities queryCapabilities = source.getQueryCapabilities();
                        if (queryCapabilities.isOffsetSupported()) {
                            // fsource is required to support
                            // SortBy.NATURAL_ORDER so we don't bother checking
                            definitionQuery.setStartIndex(startIndex);
                        } else {
                            // source = new PagingFeatureSource(source,
                            // request.getStartIndex(), limit);
                            throw new WmsException("startIndex is not supported for the "
                                    + layers[i].getName() + " layer");
                        }
                    }

                    int maxFeatures = request.getMaxFeatures() != null ? request.getMaxFeatures()
                            : Integer.MAX_VALUE;
                    definitionQuery.setMaxFeatures(maxFeatures);

                    layer.setQuery(definitionQuery);
                    map.addLayer(layer);
                } else if (layers[i].getType() == MapLayerInfo.TYPE_RASTER) {
                    // /////////////////////////////////////////////////////////
                    //
                    // Adding a coverage layer
                    //
                    // /////////////////////////////////////////////////////////
                    AbstractGridCoverage2DReader reader;
                    reader = (AbstractGridCoverage2DReader) layers[i].getCoverageReader();
                    if (reader != null) {
                        // /////////////////////////////////////////////////////////
                        //
                        // Setting coverage reading params.
                        //
                        // /////////////////////////////////////////////////////////

                        /*
                         * Test if the parameter "TIME" is present in the WMS
                         * request, and by the way in the reading parameters. If
                         * it is the case, one can adds it to the request. If an
                         * exception is thrown, we have nothing to do.
                         */
                        try {
                            ParameterValue time = reader.getFormat().getReadParameters().parameter(
                                    "TIME");
                            if (time != null && request.getTime() != null) {
                                time.setValue(request.getTime());
                            }
                        } catch (ParameterNotFoundException p) {
                        }

                        // uncomment when the DIM_RANGE vendor parameter will be
                        // enabled
                        // try {
                        // ParameterValue dimRange =
                        // reader.getFormat().getReadParameters()
                        // .parameter("DIM_RANGE");
                        // if (dimRange != null && request.getDimRange() !=
                        // null) {
                        // dimRange.setValue(request.getDimRange());
                        // }
                        // } catch (ParameterNotFoundException p) {
                        // }

                        try {
                            ParameterValue elevation = reader.getFormat().getReadParameters()
                                    .parameter("ELEVATION");
                            if (elevation != null && request.getElevation() != null) {
                                elevation.setValue(request.getElevation().intValue());
                            }
                        } catch (ParameterNotFoundException p) {
                            //ignore?
                        }

                        try {
                            final ParameterValueGroup params = reader.getFormat()
                                    .getReadParameters();

                            layer = new DefaultMapLayer(FeatureUtilities.wrapGridCoverageReader(
                                    reader, CoverageUtils.getParameters(params, layers[i]
                                            .getCoverage().getParameters())), layerStyle);

                            layer.setTitle(layers[i].getCoverage().getName());
                            layer.setQuery(Query.ALL);
                            map.addLayer(layer);
                        } catch (IllegalArgumentException e) {
                            if (LOGGER.isLoggable(Level.SEVERE)) {
                                LOGGER.log(Level.SEVERE, new StringBuffer(
                                        "Wrapping GC in feature source: ").append(
                                        e.getLocalizedMessage()).toString(), e);
                            }

                            throw new WmsException(
                                    null,
                                    new StringBuffer(
                                            "Internal error : unable to get reader for this coverage layer ")
                                            .append(layers[i].toString()).toString());
                        }
                    } else {
                        throw new WmsException(null, new StringBuffer(
                                "Internal error : unable to get reader for this coverage layer ")
                                .append(layers[i].toString()).toString());
                    }
                }
            }
           
            // setup the SLD variable substitution environment
            EnvFunction.setLocalValues(request.getEnv());

            // /////////////////////////////////////////////////////////
            //
            // Producing the map in the requested format.
            //
            // /////////////////////////////////////////////////////////
            this.delegate.produceMap();

            if (cachingPossible) {
                if(responseHeaders == null){
                    responseHeaders = new HashMap();
                }
                responseHeaders.put("Cache-Control", "max-age=" + maxAge + ", must-revalidate");
               
                Date expires = new Date();
                expires.setTime(expires.getTime() + maxAge * 1000);
                DateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
                format.setTimeZone(TimeZone.getTimeZone("GMT"));
                responseHeaders.put("Expires", format.format(expires));
            }

            final String contentDisposition = this.delegate.getContentDisposition();
            if (contentDisposition != null) {
                this.headerContentDisposition = contentDisposition;
            }
        } catch (Exception e) {
            clearMapContext();
            throw new WmsException(e, "Internal error ", "");
        } finally {
            EnvFunction.clearLocalValues();
        }
    }
   
    /**
     * Asserts the mandatory GetMap parameters have been provided.
     * <p>
     * With the exception of the SRS and STYLES parameters, for which default values are assigned.
     * </p>
     *
     * @param request
     * @throws ServiceException
     *             if any mandatory parameter has not been set on the request
     */
    private void assertMandatory(GetMapRequest request) throws ServiceException {
        if (0 >= request.getWidth() || 0 >= request.getHeight()) {
            throw new ServiceException("Missing or invalid requested map size. Parameters"
                    + " WIDTH and HEIGHT shall be present and be integers > 0. Got " + "WIDTH="
                    + request.getWidth() + ", HEIGHT=" + request.getHeight(),
                    "MissingOrInvalidParameter");
        }

        if (request.getLayers().length == 0) {
            throw new ServiceException("No layers have been requested", "LayerNotDefined");
        }

        if (request.getStyles().size() == 0) {
            throw new ServiceException("No styles have been requested", "StyleNotDefined");
        }

        if (request.getFormat() == null) {
            throw new ServiceException("No output map format requested", "InvalidFormat");
        }

        // DJB: the WMS spec says that the request must not be 0 area
        // if it is, throw a service exception!
        final Envelope env = request.getBbox();
        if (env == null) {
            throw new WmsException("GetMap requests must include a BBOX parameter.", "MissingBBox");
        }
        if (env.isNull() || (env.getWidth() <= 0) || (env.getHeight() <= 0)) {
            throw new WmsException(new StringBuffer("The request bounding box has zero area: ")
                    .append(env).toString(), "InvalidBBox");
        }
    }
   

    /**
     * Returns the list of filters resulting of comining the layers definition
     * filters with the per layer filters made by the user.
     * <p>
     * If <code>requestFilters != null</code>, it shall contain the same
     * number of elements than <code>layers</code>, as filters are requested
     * one per layer.
     * </p>
     *
     * @param requestFilters
     *            the list of filters sent by the user, or <code>null</code>
     * @param layers
     *            the layers requested in the GetMap request, where to get the
     *            per layer definition filters from.
     * @return a list of filters, one per layer, resulting of anding the user
     *         requested filter and the layer definition filter
     */
    private Filter[] buildLayersFilters(List<Filter> requestFilters, MapLayerInfo[] layers) {
        final int nLayers = layers.length;
        if (requestFilters == null || requestFilters.size() == 0) {
            requestFilters = Collections.nCopies(layers.length, (Filter) Filter.INCLUDE);
        } else if (requestFilters.size() != nLayers) {
            throw new IllegalArgumentException(
                    "requested filters and number of layers do not match");
        }
        Filter[] combinedList = new Filter[nLayers];
        Filter layerDefinitionFilter;
        Filter userRequestedFilter;
        Filter combined;

        MapLayerInfo layer;
        for (int i = 0; i < nLayers; i++) {
            layer = layers[i];
            userRequestedFilter = requestFilters.get(i);
            if (layer.getType() == MapLayerInfo.TYPE_REMOTE_VECTOR) {
                combinedList[i] = userRequestedFilter;
            } else if (layer.getType() != MapLayerInfo.TYPE_RASTER) {
                layerDefinitionFilter = layer.getFeature().getFilter();

                // heck, how I wish we use the null objects more
                if (layerDefinitionFilter == null) {
                    layerDefinitionFilter = Filter.INCLUDE;
                }
                combined = filterFac.and(layerDefinitionFilter, userRequestedFilter);
               
                FeatureTypeConstraint[] featureTypeConstraints = layer.getLayerFeatureConstraints();
                if (featureTypeConstraints != null) {
                  List<Filter> filters = new ArrayList<Filter>();
          for (int j=0;j<featureTypeConstraints.length; j++) {
            FeatureTypeConstraint featureTypeConstraint =  featureTypeConstraints[j];
            filters.add(featureTypeConstraint.getFilter());
          };
          combined = filterFac.and(combined, filterFac.and(filters));
                }
                combinedList[i] = combined;
            }       
        }
        return combinedList;
    }

    /**
     * asks the internal GetMapDelegate for the MIME type of the map that it
     * will generate or is ready to, and returns it
     *
     * @param gs
     *            DOCUMENT ME!
     *
     * @return the MIME type of the map generated or ready to generate
     *
     * @throws IllegalStateException
     *             if a GetMapDelegate is not setted yet
     * @see Response#getContentType(GeoServer)
     */
    public String getContentType(GeoServer gs) throws IllegalStateException {
        if (this.delegate == null) {
            throw new IllegalStateException("No request has been processed");
        }

        return this.delegate.getContentType();
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public String getContentEncoding() {
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.finer("returning content encoding null");
        }

        return null;
    }

    /**
     * if a GetMapDelegate is set, calls it's abort method. Elsewere do nothing.
     *
     * @see Response#abort(ServiceInfo)
     */
    public void abort(ServiceInfo gs) {
        if (this.delegate != null) {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("asking delegate for aborting the process");
            }

            this.delegate.abort();
        }
    }

    /**
     * delegates the writing and encoding of the results of the request to the
     * <code>GetMapDelegate</code> wich is actually processing it, and has
     * been obtained when <code>execute(Request)</code> was called
     *
     * @param out
     *            the output to where the map must be written
     *
     * @throws ServiceException
     *             if the delegate throws a ServiceException inside its
     *             <code>writeTo(OuptutStream)</code>, mostly due to
     * @throws IOException
     *             if the delegate throws an IOException inside its
     *             <code>writeTo(OuptutStream)</code>, mostly due to
     * @throws IllegalStateException
     *             if this method is called before <code>execute(Request)</code>
     *             has succeed
     */
    public void writeTo(OutputStream out) throws ServiceException, IOException {
        try { // mapcontext can leak memory -- we make sure we done (see
            // finally block)

            if (this.delegate == null) {
                throw new IllegalStateException(
                        "No GetMapDelegate is setted, make sure you have called execute and it has succeed");
            }

            if (LOGGER.isLoggable(Level.FINER)) {
                LOGGER.finer(new StringBuffer("asking delegate for write to ").append(out)
                        .toString());
            }

            this.delegate.writeTo(out);
        } finally {
            clearMapContext();
        }
    }

    /**
     * Clearing the map context is paramount, otherwise we end up with a memory
     * leak
     */
    void clearMapContext() {
        try {
            if (map != null && map.getLayerCount() > 0)
                map.clearLayerList();
        } catch (Exception e) // we dont want to propogate a new error
        {
            if (LOGGER.isLoggable(Level.SEVERE)) {
                LOGGER.log(Level.SEVERE, new StringBuffer("Getting feature source: ").append(
                        e.getMessage()).toString(), e);
            }
        }
    }

    /**
     * Finds out a {@link GetMapProducer} specialized in generating the
     * requested map format, registered in the spring context.
     *
     * @param outputFormat
     *            a request parameter object wich holds the processed request
     *            objects, such as layers, bbox, outpu format, etc.
     *
     * @return A specialization of <code>GetMapDelegate</code> wich can
     *         produce the requested output map format
     *
     * @throws WmsException
     *             if no specialization is configured for the output format
     *             specified in <code>request</code> or if it can't be
     *             instantiated
     */
    private GetMapProducer getDelegate(final String outputFormat) throws WmsException {
        final GetMapProducer producer = WMSExtensions.findMapProducer(outputFormat,
                availableProducers);
        if (producer == null) {
            WmsException e = new WmsException("There is no support for creating maps in "
                    + outputFormat + " format", "InvalidFormat");
            e.setCode("InvalidFormat");
            throw e;
        }
        // Tell the producer which of its output format names the
        // request
        // was made for, the producer may need it for its logic.
        // For example, result is different if requested image/png8
        // instead of image/png
        // I'm not so glad with this, but found a lot of producers were
        // made that way
        producer.setOutputFormat(outputFormat);
        return producer;
    }

    public String getContentDisposition() {
        return headerContentDisposition;
    }

    @Override
    protected void finalize() throws Throwable {
        clearMapContext();
    }

    /**
     * This is package visible only to allow getting to the delegate from inside
     * unit tests
     *
     * @return
     */
    GetMapProducer getDelegate() {
        return delegate;
    }
}
TOP

Related Classes of org.vfny.geoserver.wms.responses.GetMapResponse

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.