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 }