001 package de.deepamehta.plugins.topicmaps; 002 003 import de.deepamehta.plugins.topicmaps.model.TopicmapViewmodel; 004 import de.deepamehta.plugins.topicmaps.service.TopicmapsService; 005 006 import de.deepamehta.core.Association; 007 import de.deepamehta.core.Topic; 008 import de.deepamehta.core.model.AssociationModel; 009 import de.deepamehta.core.model.AssociationRoleModel; 010 import de.deepamehta.core.model.CompositeValueModel; 011 import de.deepamehta.core.model.TopicModel; 012 import de.deepamehta.core.model.TopicRoleModel; 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.storage.spi.DeepaMehtaTransaction; 017 018 import javax.ws.rs.GET; 019 import javax.ws.rs.PUT; 020 import javax.ws.rs.POST; 021 import javax.ws.rs.DELETE; 022 import javax.ws.rs.HeaderParam; 023 import javax.ws.rs.Path; 024 import javax.ws.rs.PathParam; 025 import javax.ws.rs.Produces; 026 import javax.ws.rs.Consumes; 027 028 import java.io.InputStream; 029 import java.util.ArrayList; 030 import java.util.HashMap; 031 import java.util.List; 032 import java.util.Map; 033 import java.util.logging.Logger; 034 035 036 037 @Path("/topicmap") 038 @Consumes("application/json") 039 @Produces("application/json") 040 public class TopicmapsPlugin extends PluginActivator implements TopicmapsService { 041 042 // ------------------------------------------------------------------------------------------------------- Constants 043 044 private static final String DEFAULT_TOPICMAP_NAME = "untitled"; 045 private static final String DEFAULT_TOPICMAP_URI = "dm4.topicmaps.default_topicmap"; 046 private static final String DEFAULT_TOPICMAP_RENDERER = "dm4.webclient.default_topicmap_renderer"; 047 048 // association type semantics ### TODO: to be dropped. Model-driven manipulators required. 049 private static final String TOPIC_MAPCONTEXT = "dm4.topicmaps.topic_mapcontext"; 050 private static final String ASSOCIATION_MAPCONTEXT = "dm4.topicmaps.association_mapcontext"; 051 private static final String ROLE_TYPE_TOPICMAP = "dm4.core.default"; 052 private static final String ROLE_TYPE_TOPIC = "dm4.topicmaps.topicmap_topic"; 053 private static final String ROLE_TYPE_ASSOCIATION = "dm4.topicmaps.topicmap_association"; 054 055 // ---------------------------------------------------------------------------------------------- Instance Variables 056 057 private Map<String, TopicmapRenderer> topicmapRenderers = new HashMap(); 058 private List<ViewmodelCustomizer> viewmodelCustomizers = new ArrayList(); 059 060 private Logger logger = Logger.getLogger(getClass().getName()); 061 062 // -------------------------------------------------------------------------------------------------- Public Methods 063 064 065 066 public TopicmapsPlugin() { 067 // Note: registering the default renderer in the InitializePluginListener would be too late. 068 // The renderer is already needed in the PostInstallPluginListener. 069 registerTopicmapRenderer(new DefaultTopicmapRenderer()); 070 } 071 072 073 074 // *************************************** 075 // *** TopicmapsService Implementation *** 076 // *************************************** 077 078 079 080 @GET 081 @Path("/{id}") 082 @Override 083 public TopicmapViewmodel getTopicmap(@PathParam("id") long topicmapId) { 084 try { 085 return new TopicmapViewmodel(topicmapId, dms, viewmodelCustomizers); 086 } catch (Exception e) { 087 throw new RuntimeException("Fetching topicmap " + topicmapId + " failed", e); 088 } 089 } 090 091 // --- 092 093 @POST 094 @Path("/{name}/{topicmap_renderer_uri}") 095 @Override 096 public Topic createTopicmap(@PathParam("name") String name, 097 @PathParam("topicmap_renderer_uri") String topicmapRendererUri, 098 @HeaderParam("Cookie") ClientState clientState) { 099 return createTopicmap(name, null, topicmapRendererUri, clientState); 100 } 101 102 @Override 103 public Topic createTopicmap(String name, String uri, String topicmapRendererUri, ClientState clientState) { 104 CompositeValueModel topicmapState = getTopicmapRenderer(topicmapRendererUri).initialTopicmapState(); 105 return dms.createTopic(new TopicModel(uri, "dm4.topicmaps.topicmap", new CompositeValueModel() 106 .put("dm4.topicmaps.name", name) 107 .put("dm4.topicmaps.topicmap_renderer_uri", topicmapRendererUri) 108 .put("dm4.topicmaps.state", topicmapState)), clientState); 109 } 110 111 // --- 112 113 @POST 114 @Path("/{id}/topic/{topic_id}") 115 @Override 116 public void addTopicToTopicmap(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 117 CompositeValueModel viewProps) { 118 DeepaMehtaTransaction tx = dms.beginTx(); 119 try { 120 dms.createAssociation(new AssociationModel(TOPIC_MAPCONTEXT, 121 new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 122 new TopicRoleModel(topicId, ROLE_TYPE_TOPIC), viewProps), null); // FIXME: clientState=null 123 storeCustomViewProperties(topicmapId, topicId, viewProps); 124 // 125 tx.success(); 126 } catch (Exception e) { 127 throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " + 128 "(viewProps=" + viewProps + ")", e); 129 } finally { 130 tx.finish(); 131 } 132 } 133 134 @Override 135 public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) { 136 addTopicToTopicmap(topicmapId, topicId, new StandardViewProperties(x, y, visibility)); 137 } 138 139 @POST 140 @Path("/{id}/association/{assoc_id}") 141 @Override 142 public void addAssociationToTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 143 dms.createAssociation(new AssociationModel(ASSOCIATION_MAPCONTEXT, 144 new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 145 new AssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION)), null); // FIXME: clientState=null 146 } 147 148 // --- 149 150 @Override 151 public boolean isTopicInTopicmap(long topicmapId, long topicId) { 152 return fetchTopicRefAssociation(topicmapId, topicId) != null; 153 } 154 155 // --- 156 157 @PUT 158 @Path("/{id}/topic/{topic_id}") 159 @Override 160 public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 161 CompositeValueModel viewProps) { 162 DeepaMehtaTransaction tx = dms.beginTx(); 163 try { 164 storeStandardViewProperties(topicmapId, topicId, viewProps); 165 storeCustomViewProperties(topicmapId, topicId, viewProps); 166 // 167 tx.success(); 168 } catch (Exception e) { 169 throw new RuntimeException("Storing view properties of topic " + topicId + " failed " + 170 "(viewProps=" + viewProps + ")", e); 171 } finally { 172 tx.finish(); 173 } 174 } 175 176 177 @PUT 178 @Path("/{id}/topic/{topic_id}/{x}/{y}") 179 @Override 180 public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 181 @PathParam("x") int x, @PathParam("y") int y) { 182 storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(x, y)); 183 } 184 185 @PUT 186 @Path("/{id}/topic/{topic_id}/{visibility}") 187 @Override 188 public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 189 @PathParam("visibility") boolean visibility) { 190 storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(visibility)); 191 } 192 193 @DELETE 194 @Path("/{id}/association/{assoc_id}") 195 @Override 196 public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 197 fetchAssociationRefAssociation(topicmapId, assocId).delete(new Directives()); 198 } 199 200 // --- 201 202 @PUT 203 @Path("/{id}") 204 @Override 205 public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) { 206 for (ClusterCoords.Entry entry : coords) { 207 setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y); 208 } 209 } 210 211 @PUT 212 @Path("/{id}/translation/{x}/{y}") 213 @Override 214 public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX, 215 @PathParam("y") int transY) { 216 try { 217 CompositeValueModel topicmapState = new CompositeValueModel() 218 .put("dm4.topicmaps.state", new CompositeValueModel() 219 .put("dm4.topicmaps.translation", new CompositeValueModel() 220 .put("dm4.topicmaps.translation_x", transX) 221 .put("dm4.topicmaps.translation_y", transY))); 222 TopicModel model = new TopicModel(topicmapId, topicmapState); 223 // workaround the "lost URI" problem (see #311) ### FIXME 224 model.setUri(dms.getTopic(topicmapId, false).getUri()); 225 // 226 dms.updateTopic(model, null); 227 } catch (Exception e) { 228 throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" + 229 transX + ", transY=" + transY + ")", e); 230 } 231 } 232 233 // --- 234 235 @Override 236 public void registerTopicmapRenderer(TopicmapRenderer renderer) { 237 logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\""); 238 topicmapRenderers.put(renderer.getUri(), renderer); 239 } 240 241 // --- 242 243 @Override 244 public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) { 245 logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 246 viewmodelCustomizers.add(customizer); 247 } 248 249 @Override 250 public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) { 251 logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 252 if (!viewmodelCustomizers.remove(customizer)) { 253 throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")"); 254 } 255 } 256 257 // --- 258 259 // Note: not part of topicmaps service 260 @GET 261 @Path("/{id}") 262 @Produces("text/html") 263 public InputStream getTopicmapInWebclient() { 264 // Note: the path parameter is evaluated at client-side 265 return invokeWebclient(); 266 } 267 268 // Note: not part of topicmaps service 269 @GET 270 @Path("/{id}/topic/{topic_id}") 271 @Produces("text/html") 272 public InputStream getTopicmapAndTopicInWebclient() { 273 // Note: the path parameters are evaluated at client-side 274 return invokeWebclient(); 275 } 276 277 278 279 // **************************** 280 // *** Hook Implementations *** 281 // **************************** 282 283 284 285 @Override 286 public void postInstall() { 287 createTopicmap(DEFAULT_TOPICMAP_NAME, DEFAULT_TOPICMAP_URI, DEFAULT_TOPICMAP_RENDERER, null); 288 // Note: null is passed as clientState. On post-install we have no clientState. 289 // The workspace assignment is made by the Access Control plugin on all-plugins-active. 290 } 291 292 293 294 // ------------------------------------------------------------------------------------------------- Private Methods 295 296 private void storeStandardViewProperties(long topicmapId, long topicId, CompositeValueModel viewProps) { 297 fetchTopicRefAssociation(topicmapId, topicId).setCompositeValue(viewProps, null, new Directives()); 298 } // clientState=null 299 300 // ### Note: the topicmapId parameter is not used. Per-topicmap custom view properties not yet supported. 301 private void storeCustomViewProperties(long topicmapId, long topicId, CompositeValueModel viewProps) { 302 invokeViewmodelCustomizers(topicId, viewProps); 303 } 304 305 // --- 306 307 private Association fetchTopicRefAssociation(long topicmapId, long topicId) { 308 return dms.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, 309 ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC, false); // fetchComposite=false 310 } 311 312 private Association fetchAssociationRefAssociation(long topicmapId, long assocId) { 313 return dms.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId, 314 ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION, false); // fetchComposite=false 315 } 316 317 // --- 318 319 // ### There is a copy in TopicmapViewmodel 320 private void invokeViewmodelCustomizers(long topicId, CompositeValueModel viewProps) { 321 Topic topic = dms.getTopic(topicId, false); // fetchComposite=false 322 for (ViewmodelCustomizer customizer : viewmodelCustomizers) { 323 invokeViewmodelCustomizer(customizer, topic, viewProps); 324 } 325 } 326 327 // ### There is a principal copy in TopicmapViewmodel 328 private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, Topic topic, CompositeValueModel viewProps) { 329 try { 330 customizer.storeViewProperties(topic, viewProps); 331 } catch (Exception e) { 332 throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " + 333 "(customizer=\"" + customizer.getClass().getName() + "\", method=\"storeViewProperties\")", e); 334 } 335 } 336 337 // --- 338 339 private TopicmapRenderer getTopicmapRenderer(String rendererUri) { 340 TopicmapRenderer renderer = topicmapRenderers.get(rendererUri); 341 // 342 if (renderer == null) { 343 throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer"); 344 } 345 // 346 return renderer; 347 } 348 349 // --- 350 351 private InputStream invokeWebclient() { 352 try { 353 return dms.getPlugin("de.deepamehta.webclient").getResourceAsStream("web/index.html"); 354 } catch (Exception e) { 355 throw new RuntimeException("Invoking the webclient failed", e); 356 } 357 } 358 359 // --------------------------------------------------------------------------------------------- Private Inner Class 360 361 private class StandardViewProperties extends CompositeValueModel { 362 363 private StandardViewProperties(int x, int y, boolean visibility) { 364 put(x, y); 365 put(visibility); 366 } 367 368 private StandardViewProperties(int x, int y) { 369 put(x, y); 370 } 371 372 373 private StandardViewProperties(boolean visibility) { 374 put(visibility); 375 } 376 377 // --- 378 379 private void put(int x, int y) { 380 put("dm4.topicmaps.x", x); 381 put("dm4.topicmaps.y", y); 382 } 383 384 private void put(boolean visibility) { 385 put("dm4.topicmaps.visibility", visibility); 386 } 387 } 388 }