nl.b3p.viewer.stripes.AttributesActionBean.java Source code

Java tutorial

Introduction

Here is the source code for nl.b3p.viewer.stripes.AttributesActionBean.java

Source

/*
 * Copyright (C) 2012-2013 B3Partners B.V.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package nl.b3p.viewer.stripes;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.servlet.http.HttpSession;
import net.sourceforge.stripes.action.*;
import net.sourceforge.stripes.controller.LifecycleStage;
import net.sourceforge.stripes.validation.Validate;
import nl.b3p.geotools.filter.visitor.RemoveDistanceUnit;
import nl.b3p.viewer.config.app.Application;
import nl.b3p.viewer.config.app.ApplicationLayer;
import nl.b3p.viewer.config.app.ConfiguredAttribute;
import nl.b3p.viewer.config.security.Authorizations;
import nl.b3p.viewer.config.services.*;
import nl.b3p.viewer.util.ChangeMatchCase;
import nl.b3p.viewer.util.FeatureToJson;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
//import org.codehaus.httpcache4j.cache.HTTPCache;
//import org.codehaus.httpcache4j.cache.MemoryCacheStorage;
//import org.codehaus.httpcache4j.client.HTTPClientResponseResolver;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.data.wfs.WFSDataStoreFactory;
import org.geotools.filter.text.cql2.CQL;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.opengis.filter.Filter;

/**
 *
 * @author Matthijs Laan
 */
@UrlBinding("/action/attributes")
@StrictBinding
public class AttributesActionBean implements ActionBean {
    private static final Log log = LogFactory.getLog(AttributesActionBean.class);

    private ActionBeanContext context;

    @Validate
    private Application application;

    @Validate
    private ApplicationLayer appLayer;

    @Validate
    private SimpleFeatureType featureType;

    private Layer layer = null;

    @Validate
    private int limit;
    @Validate
    private int page;
    @Validate
    private int start;
    @Validate
    private String dir;
    @Validate
    private String sort;
    @Validate
    private boolean arrays;
    @Validate
    private boolean edit = false;
    @Validate
    private String filter;

    @Validate
    private boolean debug;
    @Validate
    private boolean noCache;

    private boolean unauthorized;

    @Validate
    private List<Long> attributesToInclude = new ArrayList();

    @Validate
    private boolean graph = false;

    //<editor-fold defaultstate="collapsed" desc="getters en setters">
    public ActionBeanContext getContext() {
        return context;
    }

    public void setContext(ActionBeanContext context) {
        this.context = context;
    }

    public Application getApplication() {
        return application;
    }

    public void setApplication(Application application) {
        this.application = application;
    }

    public ApplicationLayer getAppLayer() {
        return appLayer;
    }

    public void setAppLayer(ApplicationLayer appLayer) {
        this.appLayer = appLayer;
    }

    public int getLimit() {
        return limit;
    }

    public void setLimit(int limit) {
        this.limit = limit;
    }

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getStart() {
        return start;
    }

    public void setStart(int start) {
        this.start = start;
    }

    public boolean isDebug() {
        return debug;
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    public String getDir() {
        return dir;
    }

    public void setDir(String dir) {
        this.dir = dir;
    }

    public String getSort() {
        return sort;
    }

    public void setSort(String sort) {
        this.sort = sort;
    }

    public boolean isArrays() {
        return arrays;
    }

    public void setArrays(boolean arrays) {
        this.arrays = arrays;
    }

    public String getFilter() {
        return filter;
    }

    public void setFilter(String filter) {
        this.filter = filter;
    }

    public boolean isNoCache() {
        return noCache;
    }

    public void setNoCache(boolean noCache) {
        this.noCache = noCache;
    }

    public SimpleFeatureType getFeatureType() {
        return this.featureType;
    }

    public void setFeatureType(SimpleFeatureType ft) {
        this.featureType = ft;
    }

    public boolean isEdit() {
        return edit;
    }

    public void setEdit(boolean edit) {
        this.edit = edit;
    }

    public List<Long> getAttributesToInclude() {
        return attributesToInclude;
    }

    public void setAttributesToInclude(List<Long> attributesToInclude) {
        this.attributesToInclude = attributesToInclude;
    }

    public boolean isGraph() {
        return graph;
    }

    public void setGraph(boolean graph) {
        this.graph = graph;
    }
    //</editor-fold>

    @After(stages = LifecycleStage.BindingAndValidation)
    public void loadLayer() {
        layer = appLayer.getService().getSingleLayer(appLayer.getLayerName());
    }

    @Before(stages = LifecycleStage.EventHandling)
    public void checkAuthorization() {

        if (application == null || appLayer == null
                || !Authorizations.isAppLayerReadAuthorized(application, appLayer, context.getRequest())) {
            unauthorized = true;
        }
    }

    public Resolution attributes() throws JSONException {
        JSONObject json = new JSONObject();

        json.put("success", Boolean.FALSE);
        String error = null;

        if (appLayer == null) {
            error = "Invalid parameters";
        } else if (unauthorized) {
            error = "Not authorized";
        } else {

            Map<String, AttributeDescriptor> featureTypeAttributes = new HashMap<String, AttributeDescriptor>();
            SimpleFeatureType ft = null;
            if (layer != null) {
                ft = layer.getFeatureType();
                if (ft != null) {
                    featureTypeAttributes = makeAttributeDescriptorList(ft);
                }
            }

            Integer geometryAttributeIndex = null;
            JSONArray attributes = new JSONArray();
            List<ConfiguredAttribute> confAttributes;
            confAttributes = appLayer.getAttributes();

            for (ConfiguredAttribute ca : confAttributes) {
                JSONObject j = ca.toJSONObject();
                AttributeDescriptor ad = featureTypeAttributes.get(ca.getFullName());
                if (ad != null) {
                    j.put("alias", ad.getAlias());
                    j.put("type", ad.getType());

                    if (ft != null && ca.getAttributeName().equals(ft.getGeometryAttribute())) {
                        geometryAttributeIndex = attributes.length();
                    }
                }
                attributes.put(j);
            }

            if (ft != null) {
                json.put("geometryAttribute", ft.getGeometryAttribute());
                json.put("relations", relationsToJSON(ft.getRelations()));
            }
            if (geometryAttributeIndex != null) {
                json.put("geometryAttributeIndex", geometryAttributeIndex);
            }
            json.put("attributes", attributes);
            json.put("success", Boolean.TRUE);
        }

        if (error != null) {
            json.put("error", error);
        }

        return new StreamingResolution("application/json", new StringReader(json.toString()));
    }

    private static final String CACHE_APPLAYER = "total_count_cache_applayer";
    private static final String CACHE_FILTER = "total_count_cache_filter";
    private static final String CACHE_TIME = "total_count_cache_time";
    private static final String CACHE_COUNT = "total_count_cache";

    private static final int CACHE_MAX_AGE = 600 * 1000;

    /**
     * Call this to clear the "total feature count" cached value when a new feature 
     * is added to a feature source. Only clears the cache for the current session.
     */
    public static void clearTotalCountCache(ActionBeanContext context) {
        HttpSession sess = context.getRequest().getSession();
        sess.removeAttribute(CACHE_APPLAYER);
        sess.removeAttribute(CACHE_FILTER);
        sess.removeAttribute(CACHE_TIME);
        sess.removeAttribute(CACHE_COUNT);
    }

    private int lookupTotalCountCache(Callable<Integer> countProducer) throws Exception {
        HttpSession session = context.getRequest().getSession();

        Integer total = null;
        Long age = null;
        Long cacheAppLayerId = (Long) session.getAttribute(CACHE_APPLAYER);
        if (appLayer.getId().equals(cacheAppLayerId)) {
            if ((filter == null && session.getAttribute(CACHE_FILTER) == null)
                    || (filter != null && filter.equals(session.getAttribute(CACHE_FILTER)))) {
                Long time = (Long) session.getAttribute(CACHE_TIME);
                if (time != null) {
                    age = System.currentTimeMillis() - time;
                    if (age <= CACHE_MAX_AGE) {
                        total = (Integer) session.getAttribute(CACHE_COUNT);
                    }
                }
            }
        }

        if (total != null) {
            log.debug(String.format(
                    "Returning cached total count value %d which was cached %s ms ago for app layer id %d", total,
                    age, appLayer.getId()));
            return total;
        } else {
            long startTime = System.currentTimeMillis();
            total = countProducer.call();
            log.debug(String.format("Caching total count value %d which took %d ms to get for app layer id %d",
                    total, System.currentTimeMillis() - startTime, appLayer.getId()));

            // Maybe only cache if getting total took longer than threshold?

            // Now a new feature is only counted for all users after CACHE_MAX_AGE 
            // If clearTotalCountCache() is called then the new feature will be 
            // counted for the current user/session).

            session.setAttribute(CACHE_APPLAYER, appLayer.getId());
            session.setAttribute(CACHE_FILTER, filter);
            session.setAttribute(CACHE_TIME, System.currentTimeMillis());
            session.setAttribute(CACHE_COUNT, total);

            return total;
        }
    }

    private void setFilter(Query q, SimpleFeatureType ft) throws Exception {
        if (filter != null && filter.trim().length() > 0) {
            Filter f = CQL.toFilter(filter);
            f = (Filter) f.accept(new RemoveDistanceUnit(), null);
            f = (Filter) f.accept(new ChangeMatchCase(false), null);
            f = FeatureToJson.reformatFilter(f, ft);
            q.setFilter(f);
        }
    }

    private static final int MAX_CACHE_SIZE = 50;

    /*    
        private static HTTPCache cache;
        private static synchronized HTTPCache getHTTPCache() {
        
    if(cache != null) {
        if(cache.getStorage().size() > MAX_CACHE_SIZE) {
            log.debug("Clearing HTTP cache after reaching max size of " + MAX_CACHE_SIZE);
            // XXX No way to remove items according to strategy?
            cache.clear();
        } else {
            if(log.isDebugEnabled()) {
                log.debug(String.format("Using HTTP cache; size=%d hits=%d misses=%d hit ratio=%f",
                        cache.getStorage().size(),
                        cache.getStatistics().getHits(),
                        cache.getStatistics().getMisses(),
                        cache.getStatistics().getHitRatio())
                );
            }
        }
        return cache;
    }
           
    log.debug("Creating new HTTP cache");
    cache = new HTTPCache(
        new MemoryCacheStorage(), // XXX unchangeable capacity of 1000 is way too high
                                  // should cache based on body size...
                                  // So clear cache if size exceeds MAX_CACHE_SIZE
        HTTPClientResponseResolver.createMultithreadedInstance()
    );                    
    return cache;
        }
    */
    public Resolution store() throws JSONException, Exception {
        JSONObject json = new JSONObject();

        if (unauthorized) {
            json.put("success", false);
            json.put("message", "Not authorized");
            return new StreamingResolution("application/json", new StringReader(json.toString(4)));
        }

        try {
            int total = 0;

            if (featureType != null || (layer != null && layer.getFeatureType() != null)) {
                FeatureSource fs;
                SimpleFeatureType ft = featureType;
                if (ft == null) {
                    ft = layer.getFeatureType();
                }
                if (isDebug() && ft.getFeatureSource() instanceof WFSFeatureSource) {
                    Map extraDataStoreParams = new HashMap();
                    extraDataStoreParams.put(WFSDataStoreFactory.TRY_GZIP.key, Boolean.FALSE);
                    fs = ((WFSFeatureSource) ft.getFeatureSource())
                            .openGeoToolsFeatureSource(layer.getFeatureType(), extraDataStoreParams);
                } /*else if(ft.getFeatureSource() instanceof ArcGISFeatureSource) {
                  Map extraDataStoreParams = new HashMap();
                  if(isDebug()) {
                      extraDataStoreParams.put(ArcGISDataStoreFactory.TRY_GZIP.key, Boolean.FALSE);
                  }
                  if(!isNoCache()) {
                      extraDataStoreParams.put(ArcGISDataStoreFactory.HTTP_CACHE.key, getHTTPCache());
                  }
                  fs = ((ArcGISFeatureSource)ft.getFeatureSource()).openGeoToolsFeatureSource(layer.getFeatureType(), extraDataStoreParams);
                  }*/ else {

                    fs = ft.openGeoToolsFeatureSource();
                }

                boolean startIndexSupported = fs.getQueryCapabilities().isOffsetSupported();

                final Query q = new Query(fs.getName().toString());
                //List<String> propertyNames = FeatureToJson.setPropertyNames(appLayer,q,ft,false);

                setFilter(q, ft);

                final FeatureSource fs2 = fs;
                total = lookupTotalCountCache(new Callable<Integer>() {
                    public Integer call() throws Exception {
                        return fs2.getCount(q);
                    }
                });

                if (total == -1) {
                    total = FeatureToJson.MAX_FEATURES;
                }

                q.setStartIndex(start);
                q.setMaxFeatures(Math.min(limit, FeatureToJson.MAX_FEATURES));

                FeatureToJson ftoj = new FeatureToJson(arrays, this.edit, graph, attributesToInclude);

                JSONArray features = ftoj.getJSONFeatures(appLayer, ft, fs, q, sort, dir);

                if (!startIndexSupported) {
                    if (features.length() < limit) {
                        //the end is reached..... Otherwise there would be a 'limit' number of features
                        total = start + features.length();
                    }
                }
                json.put("success", true);
                json.put("features", features);
            }
            json.put("total", total);
        } catch (Exception e) {
            log.error("Error loading features", e);

            json.put("success", false);

            String message = "Fout bij ophalen features: " + e.toString();
            Throwable cause = e.getCause();
            while (cause != null) {
                message += "; " + cause.toString();
                cause = cause.getCause();
            }
            json.put("message", message);
        }

        return new StreamingResolution("application/json", new StringReader(json.toString(4)));
    }

    /**
     * Makes a list of al the attributeDescriptors of the given FeatureType and
     * all the child FeatureTypes (related by join/relate)
     */
    private Map<String, AttributeDescriptor> makeAttributeDescriptorList(SimpleFeatureType ft) {
        Map<String, AttributeDescriptor> featureTypeAttributes = new HashMap<String, AttributeDescriptor>();
        for (AttributeDescriptor ad : ft.getAttributes()) {
            String name = ft.getId() + ":" + ad.getName();
            //stop when already added. Stop a infinite configurated loop
            if (featureTypeAttributes.containsKey(name)) {
                return featureTypeAttributes;
            }
            featureTypeAttributes.put(name, ad);
        }
        if (ft.getRelations() != null) {
            for (FeatureTypeRelation rel : ft.getRelations()) {
                featureTypeAttributes.putAll(makeAttributeDescriptorList(rel.getForeignFeatureType()));
            }
        }
        return featureTypeAttributes;
    }

    private JSONArray relationsToJSON(List<FeatureTypeRelation> relations) throws JSONException {
        JSONArray jRelations = new JSONArray();
        for (FeatureTypeRelation rel : relations) {
            JSONObject jRel = rel.toJSONObject();
            jRelations.put(jRel);
        }
        return jRelations;
    }
}