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