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 dms.updateTopic(new TopicModel(topicmapId, topicmapState), null);
223 } catch (Exception e) {
224 throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" +
225 transX + ", transY=" + transY + ")", e);
226 }
227 }
228
229 // ---
230
231 @Override
232 public void registerTopicmapRenderer(TopicmapRenderer renderer) {
233 logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\"");
234 topicmapRenderers.put(renderer.getUri(), renderer);
235 }
236
237 // ---
238
239 @Override
240 public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) {
241 logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
242 viewmodelCustomizers.add(customizer);
243 }
244
245 @Override
246 public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) {
247 logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
248 if (!viewmodelCustomizers.remove(customizer)) {
249 throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")");
250 }
251 }
252
253 // ---
254
255 // Note: not part of topicmaps service
256 @GET
257 @Path("/{id}")
258 @Produces("text/html")
259 public InputStream getTopicmapInWebclient() {
260 // Note: the path parameter is evaluated at client-side
261 return invokeWebclient();
262 }
263
264 // Note: not part of topicmaps service
265 @GET
266 @Path("/{id}/topic/{topic_id}")
267 @Produces("text/html")
268 public InputStream getTopicmapAndTopicInWebclient() {
269 // Note: the path parameters are evaluated at client-side
270 return invokeWebclient();
271 }
272
273
274
275 // ****************************
276 // *** Hook Implementations ***
277 // ****************************
278
279
280
281 @Override
282 public void postInstall() {
283 createTopicmap(DEFAULT_TOPICMAP_NAME, DEFAULT_TOPICMAP_URI, DEFAULT_TOPICMAP_RENDERER, null);
284 // Note: null is passed as clientState. On post-install we have no clientState.
285 // The workspace assignment is made by the Access Control plugin on all-plugins-active.
286 }
287
288
289
290 // ------------------------------------------------------------------------------------------------- Private Methods
291
292 private void storeStandardViewProperties(long topicmapId, long topicId, CompositeValueModel viewProps) {
293 fetchTopicRefAssociation(topicmapId, topicId).setCompositeValue(viewProps, null, new Directives());
294 } // clientState=null
295
296 // ### Note: the topicmapId parameter is not used. Per-topicmap custom view properties not yet supported.
297 private void storeCustomViewProperties(long topicmapId, long topicId, CompositeValueModel viewProps) {
298 invokeViewmodelCustomizers(topicId, viewProps);
299 }
300
301 // ---
302
303 private Association fetchTopicRefAssociation(long topicmapId, long topicId) {
304 return dms.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId,
305 ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC, false); // fetchComposite=false
306 }
307
308 private Association fetchAssociationRefAssociation(long topicmapId, long assocId) {
309 return dms.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId,
310 ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION, false); // fetchComposite=false
311 }
312
313 // ---
314
315 // ### There is a copy in TopicmapViewmodel
316 private void invokeViewmodelCustomizers(long topicId, CompositeValueModel viewProps) {
317 Topic topic = dms.getTopic(topicId, false); // fetchComposite=false
318 for (ViewmodelCustomizer customizer : viewmodelCustomizers) {
319 invokeViewmodelCustomizer(customizer, topic, viewProps);
320 }
321 }
322
323 // ### There is a principal copy in TopicmapViewmodel
324 private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, Topic topic, CompositeValueModel viewProps) {
325 try {
326 customizer.storeViewProperties(topic, viewProps);
327 } catch (Exception e) {
328 throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " +
329 "(customizer=\"" + customizer.getClass().getName() + "\", method=\"storeViewProperties\")", e);
330 }
331 }
332
333 // ---
334
335 private TopicmapRenderer getTopicmapRenderer(String rendererUri) {
336 TopicmapRenderer renderer = topicmapRenderers.get(rendererUri);
337 //
338 if (renderer == null) {
339 throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer");
340 }
341 //
342 return renderer;
343 }
344
345 // ---
346
347 private InputStream invokeWebclient() {
348 try {
349 return dms.getPlugin("de.deepamehta.webclient").getResourceAsStream("web/index.html");
350 } catch (Exception e) {
351 throw new RuntimeException("Invoking the webclient failed", e);
352 }
353 }
354
355 // --------------------------------------------------------------------------------------------- Private Inner Class
356
357 private class StandardViewProperties extends CompositeValueModel {
358
359 private StandardViewProperties(int x, int y, boolean visibility) {
360 put(x, y);
361 put(visibility);
362 }
363
364 private StandardViewProperties(int x, int y) {
365 put(x, y);
366 }
367
368
369 private StandardViewProperties(boolean visibility) {
370 put(visibility);
371 }
372
373 // ---
374
375 private void put(int x, int y) {
376 put("dm4.topicmaps.x", x);
377 put("dm4.topicmaps.y", y);
378 }
379
380 private void put(boolean visibility) {
381 put("dm4.topicmaps.visibility", visibility);
382 }
383 }
384 }