001    package de.kiezatlas;
002    
003    import de.kiezatlas.service.KiezatlasService;
004    
005    import de.deepamehta.plugins.geomaps.service.GeomapsService;
006    import de.deepamehta.plugins.facets.model.FacetValue;
007    import de.deepamehta.plugins.facets.service.FacetsService;
008    import de.deepamehta.core.AssociationDefinition;
009    import de.deepamehta.core.RelatedTopic;
010    import de.deepamehta.core.Topic;
011    import de.deepamehta.core.model.TopicModel;
012    import de.deepamehta.core.model.SimpleValue;
013    import de.deepamehta.core.osgi.PluginActivator;
014    import de.deepamehta.core.service.ClientState;
015    import de.deepamehta.core.service.Directives;
016    import de.deepamehta.core.service.PluginService;
017    import de.deepamehta.core.service.ResultList;
018    import de.deepamehta.core.service.annotation.ConsumesService;
019    import de.deepamehta.core.service.event.PostUpdateTopicListener;
020    import de.deepamehta.core.service.event.PreSendTopicListener;
021    import de.deepamehta.core.util.DeepaMehtaUtils;
022    
023    import javax.ws.rs.GET;
024    import javax.ws.rs.PUT;
025    import javax.ws.rs.POST;
026    import javax.ws.rs.DELETE;
027    import javax.ws.rs.HeaderParam;
028    import javax.ws.rs.Path;
029    import javax.ws.rs.PathParam;
030    import javax.ws.rs.QueryParam;
031    import javax.ws.rs.Produces;
032    import javax.ws.rs.Consumes;
033    
034    import java.io.InputStream;
035    import java.util.ArrayList;
036    import java.util.Iterator;
037    import java.util.List;
038    import java.util.logging.Logger;
039    
040    
041    
042    @Path("/site")
043    @Consumes("application/json")
044    @Produces("application/json")
045    public class KiezatlasPlugin extends PluginActivator implements KiezatlasService, PostUpdateTopicListener,
046                                                                                      PreSendTopicListener {
047    
048        // ------------------------------------------------------------------------------------------------------- Constants
049    
050        private static final String TYPE_URI_GEO_OBJECT      = "ka2.geo_object";
051        private static final String TYPE_URI_GEO_OBJECT_NAME = "ka2.geo_object.name";
052    
053        // Website-Geomap association
054        private static final String WEBSITE_GEOMAP = "dm4.core.association";
055        private static final String ROLE_TYPE_WEBSITE = "dm4.core.default";     // Note: used for both associations
056        private static final String ROLE_TYPE_GEOMAP = "dm4.core.default";
057        // Website-Facet Types association
058        private static final String WEBSITE_FACET_TYPES = "dm4.core.association";
059        // private static final String ROLE_TYPE_WEBSITE = "dm4.core.default";
060        private static final String ROLE_TYPE_FACET_TYPE = "dm4.core.default";
061    
062        // ---------------------------------------------------------------------------------------------- Instance Variables
063    
064        private GeomapsService geomapsService;
065        private FacetsService facetsService;
066    
067        private Logger logger = Logger.getLogger(getClass().getName());
068    
069        // -------------------------------------------------------------------------------------------------- Public Methods
070    
071    
072    
073    
074        // **********************
075        // *** Plugin Service ***
076        // **********************
077    
078    
079    
080        @GET
081        @Path("/{url}")
082        @Produces("text/html")
083        public InputStream launchWebclient() {
084            try {
085                // Note: the template parameters are evaluated at client-side
086                return dms.getPlugin("de.deepamehta.webclient").getResourceAsStream("web/index.html");
087            } catch (Exception e) {
088                throw new RuntimeException("Launching the webclient failed", e);
089            }
090        }
091    
092        @GET
093        @Path("/geomap/{geomap_id}")
094        @Override
095        public Topic getWebsite(@PathParam("geomap_id") long geomapId) {
096            try {
097                return dms.getTopic(geomapId, false).getRelatedTopic(WEBSITE_GEOMAP, ROLE_TYPE_WEBSITE,
098                    ROLE_TYPE_GEOMAP, "ka2.website", false, false);
099            } catch (Exception e) {
100                throw new RuntimeException("Finding the geomap's website topic failed (geomapId=" + geomapId + ")", e);
101            }
102        }
103    
104        @GET
105        @Path("/{website_id}/facets")
106        @Override
107        public ResultList<RelatedTopic> getFacetTypes(@PathParam("website_id") long websiteId) {
108            try {
109                return dms.getTopic(websiteId, false).getRelatedTopics(WEBSITE_FACET_TYPES, ROLE_TYPE_WEBSITE,
110                    ROLE_TYPE_FACET_TYPE, "dm4.core.topic_type", false, false, 0);
111            } catch (Exception e) {
112                throw new RuntimeException("Finding the website's facet types failed (websiteId=" + websiteId + ")", e);
113            }
114        }
115    
116        @GET
117        @Path("/geomap/{geomap_id}/objects")
118        @Override
119        public List<Topic> getGeoObjects(@PathParam("geomap_id") long geomapId) {
120            try {
121                return fetchGeoObjects(geomapId);
122            } catch (Exception e) {
123                throw new RuntimeException("Fetching the geomap's geo objects failed (geomapId=" + geomapId + ")", e);
124            }
125        }
126    
127        @GET
128        @Path("/category/{id}/objects")
129        @Override
130        public List<RelatedTopic> getGeoObjectsByCategory(@PathParam("id") long categoryId) {
131            return dms.getTopic(categoryId, false).getRelatedTopics("dm4.core.aggregation", "dm4.core.child",
132                "dm4.core.parent", TYPE_URI_GEO_OBJECT, false, false, 0).getItems();
133        }
134    
135        @GET
136        @Path("/geoobject")
137        @Override
138        public GeoObjects searchGeoObjects(@QueryParam("search") String searchTerm, @QueryParam("clock") long clock) {
139            GeoObjects result = new GeoObjects(clock);
140            for (Topic geoObjectName : dms.searchTopics("*" + searchTerm + "*", TYPE_URI_GEO_OBJECT_NAME)) {
141                result.add(getGeoObject(geoObjectName));
142            }
143            return result;
144        }
145    
146        @GET
147        @Path("/category/objects")
148        @Override
149        public GroupedGeoObjects searchCategories(@QueryParam("search") String searchTerm,
150                                                  @QueryParam("clock") long clock) {
151            GroupedGeoObjects result = new GroupedGeoObjects(clock);
152            for (Topic criteria : fetchAllCriteria()) {
153                for (Topic category : dms.searchTopics("*" + searchTerm + "*", criteria.getUri())) {
154                    List<RelatedTopic> geoObjects = getGeoObjectsByCategory(category.getId());
155                    result.add(criteria, category, geoObjects);
156                }
157            }
158            return result;
159        }
160    
161    
162    
163        // ****************************
164        // *** Hook Implementations ***
165        // ****************************
166    
167    
168    
169        /**
170         * Note: we *wait* for the Access Control service but we don't actually *consume* it.
171         * This ensures the Kiezatlas types are properly setup for Access Control.
172         */
173        @Override
174        @ConsumesService({
175            "de.deepamehta.plugins.geomaps.service.GeomapsService",
176            "de.deepamehta.plugins.facets.service.FacetsService",
177            "de.deepamehta.plugins.accesscontrol.service.AccessControlService"
178        })
179        public void serviceArrived(PluginService service) {
180            if (service instanceof GeomapsService) {
181                geomapsService = (GeomapsService) service;
182            } else if (service instanceof FacetsService) {
183                facetsService = (FacetsService) service;
184            }
185        }
186    
187        @Override
188        public void serviceGone(PluginService service) {
189            if (service == geomapsService) {
190                geomapsService = null;
191            } else if (service == facetsService) {
192                facetsService = null;
193            }
194        }
195    
196    
197    
198        // ********************************
199        // *** Listener Implementations ***
200        // ********************************
201    
202    
203    
204        @Override
205        public void preSendTopic(Topic topic, ClientState clientState) {
206            if (!topic.getTypeUri().equals(TYPE_URI_GEO_OBJECT)) {
207                return;
208            }
209            //
210            ResultList<RelatedTopic> facetTypes = getFacetTypes(clientState);
211            if (facetTypes == null) {
212                return;
213            }
214            //
215            enrichWithFacets(topic, facetTypes);
216        }
217    
218        @Override
219        public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel, ClientState clientState,
220                                                                                           Directives directives) {
221            if (!topic.getTypeUri().equals(TYPE_URI_GEO_OBJECT)) {
222                return;
223            }
224            //
225            ResultList<RelatedTopic> facetTypes = getFacetTypes(clientState);
226            if (facetTypes == null) {
227                return;
228            }
229            //
230            updateFacets(topic, facetTypes, newModel, clientState, directives);
231        }
232    
233        // ------------------------------------------------------------------------------------------------- Private Methods
234    
235    
236    
237    
238        // === Enrich with facets ===
239    
240        private void enrichWithFacets(Topic geoObject, ResultList<RelatedTopic> facetTypes) {
241            for (Topic facetType : facetTypes) {
242                String facetTypeUri = facetType.getUri();
243                if (!isMultiFacet(facetTypeUri)) {
244                    enrichWithSingleFacet(geoObject, facetTypeUri);
245                } else {
246                    enrichWithMultiFacet(geoObject, facetTypeUri);
247                }
248            }
249        }
250    
251        // ---
252    
253        private void enrichWithSingleFacet(Topic geoObject, String facetTypeUri) {
254            Topic facetValue = facetsService.getFacet(geoObject, facetTypeUri);
255            // Note: facetValue is null in 2 cases:
256            // 1) The geo object has just been created (no update yet)
257            // 2) The geo object has been created outside a geomap and then being revealed in a geomap.
258            if (facetValue == null) {
259                logger.info("### Enriching geo object " + geoObject.getId() + " with its \"" + facetTypeUri +
260                    "\" facet value ABORTED -- no such facet in DB");
261                return;
262            }
263            //
264            logger.info("### Enriching geo object " + geoObject.getId() + " with its \"" + facetTypeUri +
265                "\" facet value (" + facetValue + ")");
266            geoObject.getCompositeValue().getModel().put(facetValue.getTypeUri(), facetValue.getModel());
267        }
268    
269        private void enrichWithMultiFacet(Topic geoObject, String facetTypeUri) {
270            List<RelatedTopic> facetValues = facetsService.getFacets(geoObject, facetTypeUri);
271            logger.info("### Enriching geo object " + geoObject.getId() + " with its \"" + facetTypeUri +
272                "\" facet values (" + facetValues + ")");
273            String childTypeUri = getChildTypeUri(facetTypeUri);
274            // Note: we set the facet values at once (using put()) instead of iterating (and using add()) as after an geo
275            // object update request the facet values are already set. Using add() would result in having the values twice.
276            geoObject.getCompositeValue().getModel().put(childTypeUri, DeepaMehtaUtils.toTopicModels(facetValues));
277        }
278    
279    
280    
281        // === Update facets ===
282    
283        private void updateFacets(Topic geoObject, ResultList<RelatedTopic> facetTypes, TopicModel newModel,
284                                                                           ClientState clientState, Directives directives) {
285            for (Topic facetType : facetTypes) {
286                String facetTypeUri = facetType.getUri();
287                String childTypeUri = getChildTypeUri(facetTypeUri);
288                if (!isMultiFacet(facetTypeUri)) {
289                    TopicModel facetValue = newModel.getCompositeValueModel().getTopic(childTypeUri);
290                    logger.info("### Storing facet of type \"" + facetTypeUri + "\" for geo object " + geoObject.getId() +
291                        " (facetValue=" + facetValue + ")");
292                    FacetValue value = new FacetValue(childTypeUri).put(facetValue);
293                    facetsService.updateFacet(geoObject, facetTypeUri, value, clientState, directives);
294                } else {
295                    List<TopicModel> facetValues = newModel.getCompositeValueModel().getTopics(childTypeUri);
296                    logger.info("### Storing facets of type \"" + facetTypeUri + "\" for geo object " + geoObject.getId() +
297                        " (facetValues=" + facetValues + ")");
298                    FacetValue value = new FacetValue(childTypeUri).put(facetValues);
299                    facetsService.updateFacet(geoObject, facetTypeUri, value, clientState, directives);
300                }
301            }
302        }
303    
304    
305    
306        // === Helper ===
307    
308        /**
309         * Returns all Kiezatlas ctriteria. A Kiezatlas ctriteria is a topic type whose URI starts with
310         * <code>ka2.criteria.</code> but does not end with <code>.facet</code>.
311         */
312        private List<Topic> fetchAllCriteria() {
313            List<Topic> criteria = dms.getTopics("uri", new SimpleValue("ka2.criteria.*"), false);
314            // remove facet types
315            Iterator<Topic> i = criteria.iterator();
316            while (i.hasNext()) {
317                Topic crit = i.next();
318                if (crit.getUri().endsWith(".facet")) {
319                    i.remove();
320                }
321            }
322            //
323            return criteria;
324        }
325    
326        /**
327         * Returns the facet types for the current topicmap, or null if the facet types can't be determined.
328         * There can be several reasons for the latter:
329         *   a) there is no "current topicmap". This can be the case with 3rd-party clients.
330         *   b) the current topicmap is not a geomap.
331         *   c) the geomap is not part of a Kiezatlas Website.
332         *
333         * @return  The facet types (as a result set, may be empty), or <code>null</code>.
334         */
335        private ResultList<RelatedTopic> getFacetTypes(ClientState clientState) {
336            if (!clientState.has("dm4_topicmap_id")) {
337                logger.info("### Finding geo object facet types ABORTED -- topicmap is unknown (no \"dm4_topicmap_id\" " +
338                    "cookie was sent)");
339                return null;
340            }
341            //
342            long topicmapId = clientState.getLong("dm4_topicmap_id");
343            if (!isGeomap(topicmapId)) {
344                logger.info("### Finding geo object facet types for topicmap " + topicmapId + " ABORTED -- not a geomap");
345                return null;
346            }
347            //
348            Topic website = getWebsite(topicmapId);
349            if (website == null) {
350                logger.info("### Finding geo object facet types for geomap " + topicmapId + " ABORTED -- not part of a " +
351                    "Kiezatlas website");
352                return null;
353            }
354            //
355            logger.info("### Finding geo object facet types for geomap " + topicmapId);
356            return getFacetTypes(website.getId());
357        }
358    
359        private List<Topic> fetchGeoObjects(long geomapId) {
360            List<Topic> geoObjects = new ArrayList();
361            for (TopicModel geoCoord : geomapsService.getGeomap(geomapId)) {
362                Topic geoObject = geomapsService.getDomainTopic(geoCoord.getId());
363                geoObjects.add(geoObject);
364                // ### TODO: optimization. Include only name and address in returned geo objects.
365                // ### For the moment the entire objects are returned, including composite values and facets.
366            }
367            return geoObjects;
368        }
369    
370        // ---
371    
372        private Topic getGeoObject(Topic geoObjectName) {
373            return geoObjectName.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
374                TYPE_URI_GEO_OBJECT, false, false); // ### TODO: Core API should provide type-driven navigation
375        }
376    
377        private boolean isGeomap(long topicmapId) {
378            Topic topicmap = dms.getTopic(topicmapId, true);
379            String rendererUri = topicmap.getCompositeValue().getString("dm4.topicmaps.topicmap_renderer_uri");
380            return rendererUri.equals("dm4.geomaps.geomap_renderer");
381        }
382    
383        // ---
384    
385        // ### FIXME: there is a copy in FacetsPlugin.java
386        private boolean isMultiFacet(String facetTypeUri) {
387            return getAssocDef(facetTypeUri).getChildCardinalityUri().equals("dm4.core.many");
388        }
389    
390        // ### FIXME: there is a copy in FacetsPlugin.java
391        private String getChildTypeUri(String facetTypeUri) {
392            return getAssocDef(facetTypeUri).getChildTypeUri();
393        }
394    
395        // ### FIXME: there is a copy in FacetsPlugin.java
396        private AssociationDefinition getAssocDef(String facetTypeUri) {
397            // Note: a facet type has exactly *one* association definition
398            return dms.getTopicType(facetTypeUri).getAssocDefs().iterator().next();
399        }
400    }