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 }