View Javadoc
1   /*
2    * Copyright (C) 2015 B3Partners B.V.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16   */
17  package nl.b3p.viewer.stripes;
18  
19  import static nl.b3p.viewer.ibis.util.DateUtils.addMonth;
20  import static nl.b3p.viewer.ibis.util.DateUtils.differenceInMonths;
21  
22  import java.io.StringReader;
23  import java.math.BigDecimal;
24  import java.text.SimpleDateFormat;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.Comparator;
29  import java.util.Date;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.TimeZone;
36  import java.util.TreeMap;
37  import java.util.TreeSet;
38  import javax.persistence.EntityManager;
39  import net.sourceforge.stripes.action.ActionBean;
40  import net.sourceforge.stripes.action.ActionBeanContext;
41  import net.sourceforge.stripes.action.After;
42  import net.sourceforge.stripes.action.Before;
43  import net.sourceforge.stripes.action.DefaultHandler;
44  import net.sourceforge.stripes.action.Resolution;
45  import net.sourceforge.stripes.action.StreamingResolution;
46  import net.sourceforge.stripes.action.StrictBinding;
47  import net.sourceforge.stripes.action.UrlBinding;
48  import net.sourceforge.stripes.controller.LifecycleStage;
49  import net.sourceforge.stripes.validation.DateTypeConverter;
50  import net.sourceforge.stripes.validation.EnumeratedTypeConverter;
51  import net.sourceforge.stripes.validation.Validate;
52  import nl.b3p.viewer.config.app.Application;
53  import nl.b3p.viewer.config.app.ApplicationLayer;
54  import nl.b3p.viewer.config.security.Authorizations;
55  import nl.b3p.viewer.config.services.AttributeDescriptor;
56  import nl.b3p.viewer.config.services.FeatureTypeRelation;
57  import nl.b3p.viewer.config.services.Layer;
58  import nl.b3p.viewer.config.services.SimpleFeatureType;
59  import nl.b3p.viewer.ibis.util.IbisConstants;
60  import nl.b3p.viewer.util.FeatureToJson;
61  import org.apache.commons.logging.Log;
62  import org.apache.commons.logging.LogFactory;
63  import org.geotools.data.DataUtilities;
64  import org.geotools.data.Query;
65  import org.geotools.data.simple.SimpleFeatureCollection;
66  import org.geotools.data.simple.SimpleFeatureIterator;
67  import org.geotools.feature.simple.SimpleFeatureBuilder;
68  import org.geotools.data.simple.SimpleFeatureSource;
69  import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
70  import org.geotools.filter.text.ecql.ECQL;
71  import org.json.JSONArray;
72  import org.json.JSONException;
73  import org.json.JSONObject;
74  import org.opengis.feature.Property;
75  import org.opengis.feature.simple.SimpleFeature;
76  import org.opengis.filter.Filter;
77  import org.stripesstuff.stripersist.Stripersist;
78  
79  /**
80   * Attribute list backend for voor IBIS component IbisReport.
81   *
82   * @author Mark Prins
83   */
84  @UrlBinding("/action/ibisattributes")
85  @StrictBinding
86  public class IbisAttributeListActionBean implements ActionBean, IbisConstants {
87  
88      private static final Log log = LogFactory.getLog(IbisAttributeListActionBean.class);
89      private static final String JSON_METADATA = "metaData";
90      private ActionBeanContext context;
91  
92      /**
93       * Base64 form fData to echo back.
94       */
95      @Validate
96      private String data;
97      /**
98       * filename to echo back.
99       */
100     @Validate
101     private String filename;
102     /**
103      * mimetype to echo back.
104      */
105     @Validate
106     private String mimetype;
107 
108     @Validate
109     private Application application;
110 
111     @Validate
112     private ApplicationLayer appLayer;
113 
114     @Validate(converter = DateTypeConverter.class)
115     private Date fromDate;
116     @Validate(converter = DateTypeConverter.class)
117     private Date toDate;
118     @Validate
119     private String regio;
120     @Validate
121     private String gemeente;
122     @Validate
123     private String terrein;
124     @Validate
125     private List<String> attrNames;
126     /**
127      * report reportType
128      */
129     @Validate(converter = EnumeratedTypeConverter.class)
130     private ReportType reportType;
131     /**
132      * report type
133      */
134     @Validate(converter = EnumeratedTypeConverter.class)
135     private QueryArea aggregationLevel;
136     @Validate(converter = EnumeratedTypeConverter.class)
137     private AggregationLevelDate aggregationLevelDate;
138 
139     private Layer layer = null;
140     private boolean unauthorized;
141     private String gebiedsNaamQuery;
142     private QueryArea areaType;
143 
144     enum QueryArea {
145 
146         REGIO, GEMEENTE, TERREIN
147     }
148 
149     enum ReportType {
150 
151         INDIVIDUAL, AGGREGATED, ISSUE;
152     }
153 
154     enum AggregationLevelDate {
155 
156         NONE, MONTH
157     }
158 
159     /**
160      * Field in the datamodel (base uitgifte view). {@value }
161      */
162     private static final String TERREINID_FIELDNAME = "id";
163     /**
164      * Field in the datamodel (base uitgifte view). {@value }
165      */
166     private static final String GEMEENTE_FIELDNAME = "naam";
167     /**
168      * Field in the datamodel (base uitgifte view). {@value }
169      */
170     private static final String REGIO_FIELDNAME = "vvr_naam";
171     /**
172      * Field in the datamodel (base uitgifte view). {@value }
173      */
174     private static final String TERREIN_FIELDNAME = "a_plannaam";
175     /**
176      * Field in the datamodel. {@value }
177      */
178     private static final String STATUS_FIELDNAME = "status";
179     /**
180      * Field in the datamodel (related view). {@value }
181      */
182     private static final String KAVELID_RELATED_FIELDNAME = "kavelid";
183     /**
184      * Field in the datamodel (related view). {@value }
185      */
186     private static final String GEMEENTE_RELATED_FIELDNAME = "gemeentenaam";
187     /**
188      * Field in the datamodel (related view). {@value }
189      */
190     private static final String REGIO_RELATED_FIELDNAME = "regionaam";
191     /**
192      * Field in the datamodel (related view). {@value }
193      */
194     private static final String TERREIN_RELATED_FIELDNAME = "terreinnaam";
195 
196     /**
197      * Field in the datamodel (related view). {@value }
198      */
199     private static final String TERREINID_RELATED_FIELDNAME = "terreinid";
200 
201     /**
202      * Field in the datamodel (related view). {@value }
203      */
204     private static final String UITGIFTEDATUM_RELATED_FIELDNAME = "datumuitgifte";
205     /**
206      * Field in the datamodel (related view). {@value }
207      */
208     private static final String OPPERVLAKTE_GEOM_RELATED_FIELDNAME = "opp_geometrie";
209 
210     /**
211      * aggregate locality field name. {@value}
212      */
213     private static final String GEBIED_FIELDNAME = "gebied";
214 
215     /**
216      * name of the related feature type. {@code null}.
217      *
218      * @todo assuming there is only one relate
219      */
220     private static final String RELATED_FT_NAME = null;
221 
222     @After(stages = LifecycleStage.BindingAndValidation)
223     public void loadLayer() {
224         this.layer = appLayer.getService().getSingleLayer(appLayer.getLayerName(), Stripersist.getEntityManager());
225     }
226 
227     @Before(stages = LifecycleStage.EventHandling)
228     public void checkAuthorization() {
229         EntityManager em = Stripersist.getEntityManager();
230         if (application == null
231                 || appLayer == null
232                 || !Authorizations.isAppLayerReadAuthorized(application, appLayer, context.getRequest(), em)) {
233             unauthorized = true;
234         }
235     }
236 
237     /**
238      * Echo back the received base64 encoded form data. A fallback for IE and
239      * browsers that don't support client side downloads.
240      *
241      * @return excel download of the posted fData (posted data is not validated
242      * for 'excel-ness')
243      * @throws Exception if data is null or something goes wrong during IO
244      */
245     public Resolution download() throws Exception {
246         if (data == null) {
247             throw new IllegalArgumentException("Data cannot be null.");
248         } else if (unauthorized) {
249             throw new IllegalStateException("Not authorized.");
250         }
251         if (mimetype == null) {
252             mimetype = "application/vnd.ms-excel";
253         }
254         if (filename == null) {
255             filename = "ibisrapportage.xls";
256         }
257         log.debug("returning excel:" + data);
258         return new StreamingResolution(mimetype, new StringReader(data)).setFilename(filename).setAttachment(false);
259     }
260 
261     @DefaultHandler
262     public Resolution query() throws Exception {
263         JSONObject json = new JSONObject();
264         json.put("success", Boolean.FALSE);
265         // initial metadata
266         JSONObject metadata = new JSONObject()
267                 .put("root", "data").put("totalProperty", "total")
268                 .put("successProperty", "success")
269                 .put("messageProperty", "message")
270                 .put("idProperty", "rownum");
271         json.put(JSON_METADATA, metadata);
272 
273         String error = null;
274         if (appLayer == null) {
275             error = "Invalid parameters.";
276         } else if (unauthorized) {
277             error = "Not authorized.";
278         } else if (reportType == null) {
279             error = "Report type is required.";
280         } else {
281             try {
282                 // test either regio / gemeente / terrein must not be null
283                 if (terrein != null) {
284                     areaType = QueryArea.TERREIN;
285                     gebiedsNaamQuery = TERREIN_FIELDNAME + "='" + terrein + "'";;
286                 } else if (gemeente != null) {
287                     areaType = QueryArea.GEMEENTE;
288                     gebiedsNaamQuery = GEMEENTE_FIELDNAME + "='" + gemeente + "'";
289                 } else if (regio != null) {
290                     areaType = QueryArea.REGIO;
291                     gebiedsNaamQuery = REGIO_FIELDNAME + "='" + regio + "'";
292                 } else {
293                     throw new IllegalArgumentException("Geen gebied opgegeven voor rapport.");
294                 }
295 
296                 switch (reportType) {
297                     case ISSUE:
298                         reportIssued(json);
299                         break;
300                     case INDIVIDUAL:
301                         reportIndividualData(json);
302                         break;
303                     case AGGREGATED:
304                         reportAggregateData(json);
305                         break;
306                 }
307 
308                 json.put("message", "OK");
309                 json.put("success", Boolean.TRUE);
310 
311             } catch (Exception e) {
312                 log.error("Error generating report data.", e);
313                 error = e.getLocalizedMessage();
314             }
315         }
316 
317         if (error != null) {
318             json.put("success", Boolean.FALSE);
319             json.put("message", error);
320         }
321 
322         log.debug("returning json:" + json);
323         return new StreamingResolution("application/json", new StringReader(json.toString()));
324     }
325 
326     /**
327      * Uitgifte report.
328      *
329      * @param json that get the data added
330      * @throws Exception if any
331      */
332     private void reportIssued(JSONObject json) throws Exception {
333         if (fromDate == null || toDate == null) {
334             throw new IllegalArgumentException("Datum vanaf en datum tot zijn verplicht voor uitgifte.");
335         }
336         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
337         sdf.setTimeZone(TimeZone.getDefault());
338 
339         SimpleFeatureType ft = layer.getFeatureType();
340         SimpleFeatureType relFt = this.getRelatedSFT(ft, RELATED_FT_NAME);
341         SimpleFeatureSource fs = (SimpleFeatureSource) ft.openGeoToolsFeatureSource();
342         SimpleFeatureSource foreignFs = (SimpleFeatureSource) relFt.openGeoToolsFeatureSource();
343 
344         Filter filter = ECQL.toFilter(this.gebiedsNaamQuery);
345         List<String> tPropnames = Arrays.asList(
346                 TERREINID_FIELDNAME,
347                 TERREIN_FIELDNAME,
348                 GEMEENTE_FIELDNAME,
349                 REGIO_FIELDNAME);
350         Query q = new Query(fs.getName().toString());
351         q.setPropertyNames(tPropnames);
352         q.setFilter(filter);
353         q.setHandle("uitgifte-rapport");
354         q.setMaxFeatures(FeatureToJson.MAX_FEATURES);
355 
356         try {
357             // store terreinen in mem and get a list of the id's
358             SimpleFeatureCollection inMem = DataUtilities.collection(fs.getFeatures(q).features());
359             StringBuilder in = new StringBuilder();
360             SimpleFeatureIterator inMemFeats = inMem.features();
361             Set<String> terreinNames = new TreeSet<>(
362                     new Comparator<String>() {
363                         @Override
364                         public int compare(String a, String b) {
365                             return a.compareTo(b);
366                         }
367                     }
368             );
369             Set<String> regioNames = new TreeSet<>(new Comparator<String>() {
370                 @Override
371                 public int compare(String a, String b) {
372                     return a.compareTo(b);
373                 }
374             });
375             Set<String> gemeenteNames = new TreeSet<>(new Comparator<String>() {
376                 @Override
377                 public int compare(String a, String b) {
378                     return a.compareTo(b);
379                 }
380             });
381 
382             while (inMemFeats.hasNext()) {
383                 SimpleFeature f = inMemFeats.next();
384                 in.append(f.getAttribute(TERREINID_FIELDNAME)).append(",");
385                 terreinNames.add((String) f.getAttribute(TERREIN_FIELDNAME));
386                 regioNames.add((String) f.getAttribute(REGIO_FIELDNAME));
387                 gemeenteNames.add((String) f.getAttribute(GEMEENTE_FIELDNAME));
388             }
389             inMemFeats.close();
390 
391             // get related features (terreinen)
392             Query foreignQ = new Query(foreignFs.getName().toString());
393             foreignQ.setHandle("uitgifte-rapport-related");
394             List<String> propnames = Arrays.asList(
395                     KAVELID_RELATED_FIELDNAME,
396                     OPPERVLAKTE_GEOM_RELATED_FIELDNAME,
397                     UITGIFTEDATUM_RELATED_FIELDNAME,
398                     TERREIN_RELATED_FIELDNAME,
399                     REGIO_RELATED_FIELDNAME,
400                     GEMEENTE_RELATED_FIELDNAME);
401             foreignQ.setPropertyNames(propnames);
402             String query = STATUS_FIELDNAME + "= 'uitgegeven' AND "
403                     + TERREINID_RELATED_FIELDNAME + " IN (" + in.substring(0, in.length() - 1) + ") AND "
404                     + UITGIFTEDATUM_RELATED_FIELDNAME + " DURING " + sdf.format(fromDate) + "/" + sdf.format(toDate);
405             log.debug("uitgifte query: " + query);
406             foreignQ.setFilter(ECQL.toFilter(query));
407 
408             // kavels for selected terrein id's
409             SimpleFeatureCollection sfc = DataUtilities.collection(foreignFs.getFeatures(foreignQ).features());
410 
411             // create new aggregate featuretype
412             org.opengis.feature.simple.SimpleFeatureType type = DataUtilities.createType(
413                     "AGGREGATE",
414                     "id:String,*geom:MultiPolygon:28992,maand:String,oppervlakte:Double,gebied:String");
415 
416             // create flamingo attribute descriptors for AGGREGATE
417             List<AttributeDescriptor> relFeatureTypeAttributes = new ArrayList<>();
418             AttributeDescriptor maand = new AttributeDescriptor();
419             maand.setName("maand");
420             maand.setAlias("maand");
421             maand.setType(AttributeDescriptor.TYPE_DATE);
422             maand.setId(1L);
423             relFeatureTypeAttributes.add(maand);
424 
425             AttributeDescriptor opp = new AttributeDescriptor();
426             opp.setName("oppervlakte");
427             opp.setAlias("oppervlakte");
428             opp.setType(AttributeDescriptor.TYPE_DOUBLE);
429             opp.setId(2L);
430             relFeatureTypeAttributes.add(opp);
431 
432             AttributeDescriptor plan = new AttributeDescriptor();
433             plan.setName(GEBIED_FIELDNAME);
434             plan.setAlias("gebiedsnaam");
435             plan.setType(AttributeDescriptor.TYPE_STRING);
436             plan.setId(3L);
437             relFeatureTypeAttributes.add(plan);
438 
439             switch (aggregationLevel) {
440                 case REGIO:
441                     switch (aggregationLevelDate) {
442                         case MONTH:
443                             sfc = aggregateUitgifteByMonthAndArea(sfc, type, "oppervlakte",
444                                     regioNames, REGIO_RELATED_FIELDNAME);
445                             break;
446                         case NONE:
447                             sfc = aggregateUitgifteByArea(sfc, type, "oppervlakte",
448                                     regioNames, REGIO_RELATED_FIELDNAME);
449                             break;
450                     }
451                     break;
452                 case GEMEENTE:
453                     switch (aggregationLevelDate) {
454                         case MONTH:
455                             sfc = aggregateUitgifteByMonthAndArea(sfc, type, "oppervlakte",
456                                     gemeenteNames, GEMEENTE_RELATED_FIELDNAME);
457                             break;
458                         case NONE:
459                             sfc = aggregateUitgifteByArea(sfc, type, "oppervlakte",
460                                     gemeenteNames, GEMEENTE_RELATED_FIELDNAME);
461                             break;
462                     }
463                     break;
464                 case TERREIN:
465                     switch (aggregationLevelDate) {
466                         case MONTH:
467                             sfc = aggregateUitgifteByMonthAndArea(sfc, type, "oppervlakte",
468                                     terreinNames, TERREIN_RELATED_FIELDNAME);
469                             break;
470                         case NONE:
471                             sfc = aggregateUitgifteByArea(sfc, type, "oppervlakte",
472                                     terreinNames, TERREIN_RELATED_FIELDNAME);
473                             break;
474                     }
475                     break;
476             }
477             switch (aggregationLevelDate) {
478                 case MONTH:
479                     propnames = Arrays.asList("maand", "oppervlakte", GEBIED_FIELDNAME);
480                     break;
481                 case NONE:
482                     propnames = Arrays.asList("oppervlakte", GEBIED_FIELDNAME);
483             }
484             featuresToJson(sfc, json, relFeatureTypeAttributes, propnames);
485         } finally {
486             foreignFs.getDataStore().dispose();
487             fs.getDataStore().dispose();
488         }
489     }
490 
491     /**
492      * aggregate features by date and area into new collection with named
493      * features.
494      *
495      * @param sfc source of simple features to aggregate
496      * @param type aggregate feature type
497      * @param featNames names of the new features
498      * @param gebiedFieldName field name of the aggregation bucket (eg. gemeente
499      * or regio)
500      * @return aggregated features
501      */
502     private SimpleFeatureCollection aggregateUitgifteByMonthAndArea(SimpleFeatureCollection sfc,
503             org.opengis.feature.simple.SimpleFeatureType type, final String sfTypeAreaName,
504             Set<String> featNames, final String gebiedFieldName) {
505 
506         final int months = differenceInMonths(fromDate, toDate);
507         final SimpleDateFormat YYYYMM = new SimpleDateFormat("YYYY.MM");
508         Map<String, SimpleFeature> newfeats = new TreeMap<>();
509 
510         // create a feature for each month for each 'gebiedFieldName' with 0 area and null geom
511         for (String fName : featNames) {
512             Date newDate = fromDate;
513             for (int m = 0; m < months; m++) {
514                 String key = fName + YYYYMM.format(newDate);
515                 SimpleFeature month = DataUtilities.
516                         createFeature(type, key + "|null|" + YYYYMM.format(newDate) + "|0d|" + fName);
517                 newfeats.put(key, month);
518                 newDate = addMonth(newDate);
519             }
520         }
521 
522         // for each month add up opp_geometrie
523         SimpleFeatureIterator items = sfc.features();
524         while (items.hasNext()) {
525             SimpleFeature f = items.next();
526             Date d = (Date) f.getAttribute(UITGIFTEDATUM_RELATED_FIELDNAME);
527             SimpleFeature newFeat = newfeats.get(f.getAttribute(gebiedFieldName) + YYYYMM.format(d));
528             newFeat.setAttribute(sfTypeAreaName,
529                     ((Double) newFeat.getAttribute(sfTypeAreaName))
530                     + ((BigDecimal) f.getAttribute(OPPERVLAKTE_GEOM_RELATED_FIELDNAME)).doubleValue());
531 
532         }
533         items.close();
534 
535         ArrayList<SimpleFeature> feats = new ArrayList<>(newfeats.values());
536         return DataUtilities.collection(feats);
537     }
538 
539     /**
540      * aggregate features area into new collection with named features.
541      *
542      * @param sfc source of simple features to aggregate
543      * @param type aggregate featuretype
544      * @param featNames names of the new features
545      * @param gebiedFieldName field name of the aggregation bucket (eg. gemeente
546      * or regio)
547      * @return aggregated features
548      */
549     private SimpleFeatureCollection aggregateUitgifteByArea(SimpleFeatureCollection sfc,
550             org.opengis.feature.simple.SimpleFeatureType type, String sfTypeAreaName,
551             Set<String> featNames, final String gebiedFieldName) {
552 
553         // create a feature for each 'gebiedFieldName' with 0 area and null date and null geom
554         Map<String, SimpleFeature> newfeats = new TreeMap<>();
555         for (String fName : featNames) {
556             SimpleFeature newfeat = DataUtilities.
557                     createFeature(type, fName + "|null|null|0d|" + fName);
558             newfeats.put(fName, newfeat);
559         }
560 
561         // for each regio add up opp_geometrie
562         SimpleFeatureIterator items = sfc.features();
563         while (items.hasNext()) {
564             SimpleFeature f = items.next();
565             SimpleFeature newFeat = newfeats.get((String) f.getAttribute(gebiedFieldName));
566             newFeat.setAttribute(sfTypeAreaName,
567                     ((Double) newFeat.getAttribute(sfTypeAreaName))
568                     + ((BigDecimal) f.getAttribute(OPPERVLAKTE_GEOM_RELATED_FIELDNAME)).doubleValue());
569 
570         }
571         items.close();
572         ArrayList<SimpleFeature> feats = new ArrayList<SimpleFeature>(newfeats.values());
573         Collections.reverse(feats);
574         return DataUtilities.collection(feats);
575     }
576 
577     /**
578      * Convert a SimpleFeatureCollection to JSON with metadata.
579      *
580      * @param sfc collections of features
581      * @param json output/appendend to json structure
582      * @param featureTypeAttributes flamingo attribute descriptors for the
583      * features
584      * @param outputPropNames fieldnames to put in output
585      * @throws JSONException is any
586      */
587     private void featuresToJson(SimpleFeatureCollection sfc, JSONObject json,
588             List<AttributeDescriptor> featureTypeAttributes, List<String> outputPropNames) throws JSONException {
589 
590         // metadata for fData fields
591         JSONArray fields = new JSONArray();
592         // columns for grid
593         JSONArray columns = new JSONArray();
594         // fData payload
595         JSONArray datas = new JSONArray();
596 
597         SimpleFeatureIterator sfIter = sfc.features();
598 
599         boolean getMetadataFromFirstFeature = true;
600         while (sfIter.hasNext()) {
601             SimpleFeature feature = sfIter.next();
602             JSONObject fData = new JSONObject();
603 
604             for (AttributeDescriptor attr : featureTypeAttributes) {
605                 String name = attr.getName();
606                 if (getMetadataFromFirstFeature) {
607                     if (outputPropNames.contains(name)) {
608                         // only load metadata into json this for first feature
609                         JSONObject field = new JSONObject().put("name", name).put("type", attr.getExtJSType());
610                         if (reportType == ReportType.ISSUE && attr.getType().equals(AttributeDescriptor.TYPE_DATE)) {
611                             field.put("dateFormat", "Y-m");
612                         }
613                         fields.put(field);
614                         columns.put(new JSONObject().put("text", (attr.getAlias() != null ? attr.getAlias() : name)).put("dataIndex", name));
615                     }
616                 }
617                 fData.put(attr.getName(), feature.getAttribute(attr.getName()));
618             }
619             datas.put(fData);
620             getMetadataFromFirstFeature = false;
621         }
622 
623         json.getJSONObject(JSON_METADATA).put("fields", fields);
624         json.getJSONObject(JSON_METADATA).put("columns", columns);
625         json.put("data", datas);
626         json.put("total", datas.length());
627 
628         sfIter.close();
629     }
630 
631     /**
632      * look up the named related featuretype.
633      *
634      * @param ft main/parent feature type
635      * @param typeNameToGet the name of the related feature type or null
636      * @return the named feature type or the first feature type in case
637      * {@code typeNameToGet} is {
638      * @null} or {
639      * @null} when the/a related feature type does not exist
640      */
641     private SimpleFeatureType getRelatedSFT(SimpleFeatureType ft, String typeNameToGet) {
642         SimpleFeatureType relFt = null;
643 
644         for (FeatureTypeRelation rel : ft.getRelations()) {
645             if (rel.getType().equals(FeatureTypeRelation.RELATE)) {
646                 relFt = rel.getForeignFeatureType();
647                 if (relFt.getTypeName().equals(typeNameToGet) || typeNameToGet == null) {
648                     break;
649                 }
650             }
651         }
652         return relFt;
653     }
654 
655     private void reportIndividualData(JSONObject json) throws Exception {
656         List<String> tPropnames = new ArrayList(attrNames);
657 
658         SimpleFeatureType ft = layer.getFeatureType();
659         List<AttributeDescriptor> featureTypeAttributes = ft.getAttributes();
660         SimpleFeatureSource fs = (SimpleFeatureSource) ft.openGeoToolsFeatureSource();
661 
662         List<String> foreignAttrNames = new ArrayList<String>();
663         // find out which attribute names -if any- are from related features
664         for (String a : attrNames) {
665             if (fs.getSchema().getDescriptor(a) == null) {
666                 // if not from fs it must be foreign
667                 foreignAttrNames.add(a);
668                 tPropnames.remove(a);
669             }
670         }
671         foreignAttrNames.add(TERREINID_RELATED_FIELDNAME);
672         tPropnames.add(TERREINID_FIELDNAME);
673         Filter filter = ECQL.toFilter(this.gebiedsNaamQuery);
674         Query q = new Query(fs.getName().toString());
675         q.setPropertyNames(tPropnames);
676         q.setFilter(filter);
677         q.setHandle("individueel-rapport");
678         q.setMaxFeatures(FeatureToJson.MAX_FEATURES);
679 
680         SimpleFeatureCollection mainFSinMem;
681         try {
682             mainFSinMem = DataUtilities.collection(fs.getFeatures(q).features());
683         } finally {
684             fs.getDataStore().dispose();
685         }
686         if (!foreignAttrNames.isEmpty()) {
687             SimpleFeatureType relFt = this.getRelatedSFT(ft, RELATED_FT_NAME);
688             SimpleFeatureSource foreignFs = (SimpleFeatureSource) relFt.openGeoToolsFeatureSource();
689             // compose IN query criteria and store parent features in a map so we can easily get them later
690             StringBuilder in = new StringBuilder();
691             HashMap<Integer, SimpleFeature> parentFeatures = new HashMap<>();
692             SimpleFeatureIterator inMemFeats = mainFSinMem.features();
693             while (inMemFeats.hasNext()) {
694                 SimpleFeature f = inMemFeats.next();
695                 in.append(f.getAttribute(TERREINID_FIELDNAME)).append(",");
696                 parentFeatures.put((Integer) f.getAttribute(TERREINID_FIELDNAME), f);
697             }
698             inMemFeats.close();
699 
700             // get related features (children)
701             Query foreignQ = new Query(foreignFs.getName().toString());
702             foreignQ.setHandle("individueel-rapport-related");
703             foreignQ.setPropertyNames(foreignAttrNames);
704             String query = TERREINID_RELATED_FIELDNAME + " IN (" + in.substring(0, in.length() - 1) + ")";
705             log.debug("individueel-rapport-related: " + query);
706             foreignQ.setFilter(ECQL.toFilter(query));
707             try {
708                 // kavels/children for selected terrein id's
709                 SimpleFeatureCollection relatedFC = foreignFs.getFeatures(foreignQ);
710                 // create a new aggregate feature type 'COMPOSITE' that has attributes of both parent and child types
711                 SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
712                 tb.setName("COMPOSITE");
713                 for (String prop : attrNames) {
714                     org.opengis.feature.type.AttributeDescriptor attrDescName = fs.getSchema().getDescriptor(prop);
715                     if (attrDescName == null) {
716                         // if not from the parent it must be from the child
717                         attrDescName = foreignFs.getSchema().getDescriptor(prop);
718                     }
719                     tb.add(attrDescName);
720                 }
721                 tb.add(TERREINID_RELATED_FIELDNAME, Integer.class);
722                 org.opengis.feature.simple.SimpleFeatureType type = tb.buildFeatureType();
723                 //  merge main & related child flamingo attribute descriptors for COMPOSITE,
724                 featureTypeAttributes.addAll(relFt.getAttributes());
725 
726                 SimpleFeatureBuilder sfBuilder = new SimpleFeatureBuilder(type);
727                 SimpleFeatureIterator sfcIter = relatedFC.features();
728                 ArrayList<SimpleFeature> newfeats = new ArrayList<>();
729                 while (sfcIter.hasNext()) {
730                     // create as many new features as children
731                     SimpleFeature f = sfcIter.next();
732                     SimpleFeature n = SimpleFeatureBuilder.retype(f, sfBuilder);
733                     // copy main data to related children(n)
734                     SimpleFeature p = parentFeatures.get((Integer) f.getAttribute(TERREINID_RELATED_FIELDNAME));
735                     for (Property a : p.getProperties()) {
736                         if (attrNames.contains(a.getName().toString())) {
737                             n.setAttribute(a.getName(), a.getValue());
738                         }
739                     }
740                     newfeats.add(n);
741                 }
742                 sfcIter.close();
743                 mainFSinMem = DataUtilities.collection(newfeats);
744             } finally {
745                 foreignFs.getDataStore().dispose();
746             }
747         }
748         featuresToJson(mainFSinMem, json, featureTypeAttributes, attrNames);
749     }
750 
751     private void reportAggregateData(JSONObject json) throws Exception {
752         SimpleFeatureType ft = layer.getFeatureType();
753         List<AttributeDescriptor> featureTypeAttributes = ft.getAttributes();
754         SimpleFeatureSource fs = (SimpleFeatureSource) ft.openGeoToolsFeatureSource();
755 
756         List<String> tPropnames = new ArrayList(attrNames);
757         List<String> foreignAttrNames = new ArrayList<>();
758         // find out which attribute names -if any- are from related features
759         for (String a : attrNames) {
760             if (fs.getSchema().getDescriptor(a) == null) {
761                 // if not from fs it must be foreign
762                 foreignAttrNames.add(a);
763                 tPropnames.remove(a);
764             }
765         }
766         foreignAttrNames.add(TERREINID_RELATED_FIELDNAME);
767         tPropnames.add(TERREINID_FIELDNAME);
768         tPropnames.add(GEMEENTE_FIELDNAME);
769         tPropnames.add(REGIO_FIELDNAME);
770         tPropnames.add(TERREIN_FIELDNAME);
771 
772         Filter filter = ECQL.toFilter(this.gebiedsNaamQuery);
773         Query q = new Query(fs.getName().toString());
774         q.setPropertyNames(tPropnames);
775         q.setFilter(filter);
776         q.setHandle("aggregatie-rapport");
777         q.setMaxFeatures(FeatureToJson.MAX_FEATURES);
778         log.debug("aggregatie query:" + q);
779 
780         try {
781             // create the new aggregate featuretype
782             org.opengis.feature.simple.SimpleFeatureType type;
783             SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
784             tb.setName("AGGREGATE");
785             for (String prop : tPropnames) {
786                 org.opengis.feature.type.AttributeDescriptor attrDescName = fs.getSchema().getDescriptor(prop);
787                 if (attrDescName != null) {
788                     tb.add(attrDescName);
789                 }
790             }
791             tb.add(GEBIED_FIELDNAME, String.class);
792             // update flamingo attribute descriptors for AGGREGATE
793             AttributeDescriptor g = new AttributeDescriptor();
794             g.setName(GEBIED_FIELDNAME);
795             g.setAlias("gebiedsnaam");
796             g.setType(AttributeDescriptor.TYPE_STRING);
797             featureTypeAttributes.add(0, g);
798             // get parent features
799             SimpleFeatureCollection sfc = DataUtilities.collection(fs.getFeatures(q).features());
800 
801             if (!foreignAttrNames.isEmpty()) {
802                 SimpleFeatureType relFt = this.getRelatedSFT(ft, RELATED_FT_NAME);
803                 SimpleFeatureSource foreignFs = (SimpleFeatureSource) relFt.openGeoToolsFeatureSource();
804 
805                 // add related attributes to type
806                 for (String prop : foreignAttrNames) {
807                     org.opengis.feature.type.AttributeDescriptor attrDescName = foreignFs.getSchema().getDescriptor(prop);
808                     if (attrDescName != null) {
809                         tb.add(attrDescName);
810                     }
811                 }
812                 tb.add(TERREINID_RELATED_FIELDNAME, Integer.class);
813                 type = tb.buildFeatureType();
814 
815                 //  merge main & related child flamingo attribute descriptors for AGGREGATE,
816                 featureTypeAttributes.addAll(relFt.getAttributes());
817 
818                 // compose IN query criteria and store parent features in a map so we can easily get them later
819                 StringBuilder in = new StringBuilder();
820                 HashMap<Integer, SimpleFeature> parentFeatures = new HashMap<>();
821                 SimpleFeatureIterator inMemFeats = sfc.features();
822                 while (inMemFeats.hasNext()) {
823                     SimpleFeature f = inMemFeats.next();
824                     in.append(f.getAttribute(TERREINID_FIELDNAME)).append(",");
825                     parentFeatures.put((Integer) f.getAttribute(TERREINID_FIELDNAME), f);
826                 }
827                 inMemFeats.close();
828 
829                 // get related features (children)
830                 Query foreignQ = new Query(foreignFs.getName().toString());
831                 foreignQ.setHandle("aggregatie-rapport-related");
832                 foreignQ.setPropertyNames(foreignAttrNames);
833                 String query = TERREINID_RELATED_FIELDNAME + " IN (" + in.substring(0, in.length() - 1) + ")";
834                 log.debug("aggregatie-rapport-related: " + query);
835                 foreignQ.setFilter(ECQL.toFilter(query));
836 
837                 try {
838                     // kavels/children for selected terrein id's
839                     SimpleFeatureCollection relatedFC = foreignFs.getFeatures(foreignQ);
840                     SimpleFeatureBuilder sfBuilder = new SimpleFeatureBuilder(type);
841                     SimpleFeatureIterator sfcIter = relatedFC.features();
842                     ArrayList<SimpleFeature> newfeats = new ArrayList<>();
843 
844                     boolean firsttimeForP = true;
845                     Set<SimpleFeature> firsttimeForPSet = new HashSet<>();
846                     while (sfcIter.hasNext()) {
847                         // create as many new features as related/children
848                         SimpleFeature f = sfcIter.next();
849                         SimpleFeature n = SimpleFeatureBuilder.retype(f, sfBuilder);
850 
851                         // copy main data to related children
852                         //   but payload field only once to prevent aggregating those values later
853                         SimpleFeature p = parentFeatures.get((Integer) f.getAttribute(TERREINID_RELATED_FIELDNAME));
854                         for (Property a : p.getProperties()) {
855                             if (firsttimeForPSet.contains(p)) {
856                                 if (!attrNames.contains(a.getName().toString())) {
857                                     n.setAttribute(a.getName(), a.getValue());
858                                 }
859                             } else {
860                                 if (tPropnames.contains(a.getName().toString())) {
861                                     n.setAttribute(a.getName(), a.getValue());
862                                 }
863                             }
864                         }
865                         newfeats.add(n);
866                         firsttimeForPSet.add(p);
867                     }
868                     sfcIter.close();
869                     sfc = DataUtilities.collection(newfeats);
870                 } finally {
871                     foreignFs.getDataStore().dispose();
872                 }
873             } else {
874                 type = tb.buildFeatureType();
875             }
876 
877             // aggregation
878             Set<String> regions = new HashSet<>();
879             switch (aggregationLevel) {
880                 case REGIO:
881                     // max number for regio is 1
882                     regions.add(regio);
883                     sfc = aggregateFields(sfc, type, regions, attrNames, REGIO_FIELDNAME);
884                     break;
885                 case GEMEENTE:
886                     if (this.areaType == QueryArea.TERREIN) {
887                         regions.add(gemeente);
888                     } else {
889                         // create set of all gemeente from query result
890                         SimpleFeatureIterator iter = sfc.features();
891                         try {
892                             while (iter.hasNext()) {
893                                 SimpleFeature f = iter.next();
894                                 regions.add((String) f.getAttribute(GEMEENTE_FIELDNAME));
895                             }
896                         } finally {
897                             iter.close();
898                         }
899                     }
900                     sfc = aggregateFields(sfc, type, regions, attrNames, GEMEENTE_FIELDNAME);
901                     break;
902                 case TERREIN:
903                     if (this.areaType == QueryArea.TERREIN) {
904                         regions.add(terrein);
905                     } else {
906                         // create set of all terrein from query result
907                         SimpleFeatureIterator iter = sfc.features();
908                         try {
909                             while (iter.hasNext()) {
910                                 SimpleFeature f = iter.next();
911                                 regions.add((String) f.getAttribute(TERREIN_FIELDNAME));
912                             }
913                         } finally {
914                             iter.close();
915                         }
916                     }
917                     sfc = aggregateFields(sfc, type, regions, attrNames, TERREIN_FIELDNAME);
918                     break;
919             }
920 
921             attrNames.add(GEBIED_FIELDNAME);
922             featuresToJson(sfc, json, featureTypeAttributes, attrNames);
923         } finally {
924             fs.getDataStore().dispose();
925         }
926 
927     }
928 
929     /**
930      * Aggregate the values of the given feature collection into new features of
931      * {@code type} that are named.
932      *
933      * @param sfc source of simple features to aggregate
934      * @param type aggregate feature type
935      * @param featNames names of the new features
936      * @param gebiedFieldName field name of the aggregation bucket (eg. gemeente
937      * or regio)
938      * @param aggregateFieldNames
939      * @param gebiedFieldName
940      * @return aggregated features
941      *
942      */
943     private SimpleFeatureCollection aggregateFields(
944             SimpleFeatureCollection sfc,
945             org.opengis.feature.simple.SimpleFeatureType type,
946             Set<String> featNames,
947             List<String> aggregateFieldNames,
948             String gebiedFieldName) {
949 
950         // create a feature for each (regio|gemeente|terrein) name with 0 value
951         Map<String, SimpleFeature> newfeats = new TreeMap<>();
952         for (String fName : featNames) {
953             SimpleFeature newfeat = DataUtilities.template(type);
954             for (String aggrName : aggregateFieldNames) {
955                 newfeat.setAttribute(aggrName, 0);
956             }
957             newfeat.setAttribute(GEBIED_FIELDNAME, fName);
958             newfeats.put(fName, newfeat);
959         }
960 
961         // for each (regio|gemeente|terrein) name in relatedFC get the attribute to aggregate
962         //  and add up in new named feature
963         SimpleFeatureIterator items = sfc.features();
964         while (items.hasNext()) {
965             SimpleFeature f = items.next();
966             SimpleFeature newFeat = newfeats.get((String) f.getAttribute(gebiedFieldName));
967 
968             for (String aggrFieldName : aggregateFieldNames) {
969                 if (f.getAttribute(aggrFieldName) != null) {
970                     // add up if not null
971                     newFeat.setAttribute(aggrFieldName,
972                             ((Number) newFeat.getAttribute(aggrFieldName)).doubleValue()
973                             + ((Number) f.getAttribute(aggrFieldName)).doubleValue()
974                     );
975                 }
976             }
977         }
978         items.close();
979         ArrayList<SimpleFeature> feats = new ArrayList<>(newfeats.values());
980         return DataUtilities.collection(feats);
981     }
982 
983     //<editor-fold defaultstate="collapsed" desc="getters en setters">
984     public String getData() {
985         return data;
986     }
987 
988     public void setData(String data) {
989         this.data = data;
990     }
991 
992     public String getFilename() {
993         return filename;
994     }
995 
996     public void setFilename(String filename) {
997         this.filename = filename;
998     }
999 
1000     public String getMimetype() {
1001         return mimetype;
1002     }
1003 
1004     public void setMimetype(String mimetype) {
1005         this.mimetype = mimetype;
1006     }
1007 
1008     @Override
1009     public void setContext(ActionBeanContext context) {
1010         this.context = context;
1011     }
1012 
1013     @Override
1014     public ActionBeanContext getContext() {
1015         return context;
1016     }
1017 
1018     public Application getApplication() {
1019         return application;
1020     }
1021 
1022     public void setApplication(Application application) {
1023         this.application = application;
1024     }
1025 
1026     public ApplicationLayer getAppLayer() {
1027         return appLayer;
1028     }
1029 
1030     public void setAppLayer(ApplicationLayer appLayer) {
1031         this.appLayer = appLayer;
1032     }
1033 
1034     public String getRegio() {
1035         return regio;
1036     }
1037 
1038     public void setRegio(String regio) {
1039         this.regio = regio;
1040     }
1041 
1042     public String getGemeente() {
1043         return gemeente;
1044     }
1045 
1046     public void setGemeente(String gemeente) {
1047         this.gemeente = gemeente;
1048     }
1049 
1050     public String getTerrein() {
1051         return terrein;
1052     }
1053 
1054     public void setTerrein(String terrein) {
1055         this.terrein = terrein;
1056     }
1057 
1058     public ReportType getReportType() {
1059         return reportType;
1060     }
1061 
1062     public void setReportType(ReportType reportType) {
1063         this.reportType = reportType;
1064     }
1065 
1066     public QueryArea getAggregationLevel() {
1067         return aggregationLevel;
1068     }
1069 
1070     public void setAggregationLevel(QueryArea aggregationLevel) {
1071         this.aggregationLevel = aggregationLevel;
1072     }
1073 
1074     public AggregationLevelDate getAggregationLevelDate() {
1075         return aggregationLevelDate;
1076     }
1077 
1078     public void setAggregationLevelDate(AggregationLevelDate aggregationLevelDate) {
1079         this.aggregationLevelDate = aggregationLevelDate;
1080     }
1081 
1082     public Date getFromDate() {
1083         return fromDate;
1084     }
1085 
1086     public void setFromDate(Date fromDate) {
1087         this.fromDate = fromDate;
1088     }
1089 
1090     public Date getToDate() {
1091         return toDate;
1092     }
1093 
1094     public void setToDate(Date toDate) {
1095         this.toDate = toDate;
1096     }
1097 
1098     public List<String> getAttrNames() {
1099         return attrNames;
1100     }
1101 
1102     public void setAttrNames(List<String> attrNames) {
1103         this.attrNames = attrNames;
1104     }
1105 
1106     //</editor-fold>
1107 }