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 }