001    package de.deepamehta.plugins.workspaces;
002    
003    import de.deepamehta.plugins.workspaces.service.WorkspacesService;
004    import de.deepamehta.plugins.facets.model.FacetValue;
005    import de.deepamehta.plugins.facets.service.FacetsService;
006    import de.deepamehta.plugins.topicmaps.service.TopicmapsService;
007    
008    import de.deepamehta.core.Association;
009    import de.deepamehta.core.AssociationType;
010    import de.deepamehta.core.DeepaMehtaObject;
011    import de.deepamehta.core.RelatedTopic;
012    import de.deepamehta.core.Topic;
013    import de.deepamehta.core.TopicType;
014    import de.deepamehta.core.Type;
015    import de.deepamehta.core.model.ChildTopicsModel;
016    import de.deepamehta.core.model.SimpleValue;
017    import de.deepamehta.core.model.TopicModel;
018    import de.deepamehta.core.osgi.PluginActivator;
019    import de.deepamehta.core.service.Cookies;
020    import de.deepamehta.core.service.Directives;
021    import de.deepamehta.core.service.Inject;
022    import de.deepamehta.core.service.ResultList;
023    import de.deepamehta.core.service.Transactional;
024    import de.deepamehta.core.service.accesscontrol.SharingMode;
025    import de.deepamehta.core.service.event.IntroduceAssociationTypeListener;
026    import de.deepamehta.core.service.event.IntroduceTopicTypeListener;
027    import de.deepamehta.core.service.event.PostCreateAssociationListener;
028    import de.deepamehta.core.service.event.PostCreateTopicListener;
029    import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
030    
031    import javax.ws.rs.GET;
032    import javax.ws.rs.POST;
033    import javax.ws.rs.PUT;
034    import javax.ws.rs.Consumes;
035    import javax.ws.rs.Path;
036    import javax.ws.rs.PathParam;
037    import javax.ws.rs.Produces;
038    import javax.ws.rs.core.Context;
039    import javax.ws.rs.core.UriInfo;
040    
041    import java.util.Iterator;
042    import java.util.logging.Logger;
043    
044    
045    
046    @Path("/workspace")
047    @Consumes("application/json")
048    @Produces("application/json")
049    public class WorkspacesPlugin extends PluginActivator implements WorkspacesService, IntroduceTopicTypeListener,
050                                                                                        IntroduceAssociationTypeListener,
051                                                                                        PostCreateTopicListener,
052                                                                                        PostCreateAssociationListener {
053    
054        // ------------------------------------------------------------------------------------------------------- Constants
055    
056        // Property URIs
057        private static final String PROP_WORKSPACE_ID = "dm4.workspaces.workspace_id";
058    
059        // Query parameter
060        private static final String PARAM_NO_WORKSPACE_ASSIGNMENT = "no_workspace_assignment";
061    
062        // ---------------------------------------------------------------------------------------------- Instance Variables
063    
064        @Inject
065        private FacetsService facetsService;
066    
067        @Inject
068        private TopicmapsService topicmapsService;
069    
070        @Context
071        private UriInfo uriInfo;
072    
073        private Logger logger = Logger.getLogger(getClass().getName());
074    
075        // -------------------------------------------------------------------------------------------------- Public Methods
076    
077    
078    
079        // ****************************************
080        // *** WorkspacesService Implementation ***
081        // ****************************************
082    
083    
084    
085        @POST
086        @Path("/{name}/{uri:[^/]*?}/{sharing_mode_uri}")    // Note: default is [^/]+?     // +? is a "reluctant" quantifier
087        @Transactional
088        @Override
089        public Topic createWorkspace(@PathParam("name") String name, @PathParam("uri") String uri,
090                                     @PathParam("sharing_mode_uri") SharingMode sharingMode) {
091            logger.info("Creating workspace \"" + name + "\" (uri=\"" + uri + "\", sharingMode=" + sharingMode + ")");
092            // create workspace
093            Topic workspace = dms.createTopic(new TopicModel(uri, "dm4.workspaces.workspace", new ChildTopicsModel()
094                .put("dm4.workspaces.name", name)
095                .putRef("dm4.workspaces.sharing_mode", sharingMode.getUri())
096            ));
097            // create default topicmap and assign to workspace
098            Topic topicmap = topicmapsService.createTopicmap(TopicmapsService.DEFAULT_TOPICMAP_NAME,
099                TopicmapsService.DEFAULT_TOPICMAP_RENDERER);
100            assignToWorkspace(topicmap, workspace.getId());
101            //
102            return workspace;
103        }
104    
105        // ---
106    
107        // Note: the "include_childs" query paramter is handled by the core's JerseyResponseFilter
108        @GET
109        @Path("/{uri}")
110        @Override
111        public Topic getWorkspace(@PathParam("uri") String uri) {
112            Topic workspace = dms.getTopic("uri", new SimpleValue(uri));
113            if (workspace == null) {
114                throw new RuntimeException("Workspace \"" + uri + "\" does not exist");
115            }
116            return workspace;
117        }
118    
119        // Note: the "include_childs" query paramter is handled by the core's JerseyResponseFilter
120        @GET
121        @Path("/{id}/topics/{type_uri}")
122        @Override
123        public ResultList<RelatedTopic> getAssignedTopics(@PathParam("id") long workspaceId,
124                                                          @PathParam("type_uri") String topicTypeUri) {
125            ResultList<RelatedTopic> topics = dms.getTopics(topicTypeUri, 0);   // maxResultSize=0
126            applyWorkspaceFilter(topics.iterator(), workspaceId);
127            return topics;
128        }
129    
130        // Note: the "include_childs" query paramter is handled by the core's JerseyResponseFilter
131        @GET
132        @Path("/object/{id}")
133        @Override
134        public Topic getAssignedWorkspace(@PathParam("id") long objectId) {
135            long workspaceId = getAssignedWorkspaceId(objectId);
136            if (workspaceId == -1) {
137                return null;
138            }
139            return dms.getTopic(workspaceId);
140        }
141    
142        @Override
143        public boolean isAssignedToWorkspace(long objectId, long workspaceId) {
144            return getAssignedWorkspaceId(objectId) == workspaceId;
145        }
146    
147        // ---
148    
149        // Note: not part of workspaces service
150        @PUT
151        @Path("/{workspace_id}/object/{object_id}")
152        @Transactional
153        public Directives assignToWorkspace(@PathParam("object_id") long objectId,
154                                            @PathParam("workspace_id") long workspaceId) {
155            assignToWorkspace(dms.getObject(objectId), workspaceId);
156            return Directives.get();
157        }
158    
159        @Override
160        public void assignToWorkspace(DeepaMehtaObject object, long workspaceId) {
161            checkArgument(workspaceId);
162            //
163            _assignToWorkspace(object, workspaceId);
164        }
165    
166        @Override
167        public void assignTypeToWorkspace(Type type, long workspaceId) {
168            checkArgument(workspaceId);
169            //
170            _assignToWorkspace(type, workspaceId);
171            for (Topic configTopic : type.getViewConfig().getConfigTopics()) {
172                _assignToWorkspace(configTopic, workspaceId);
173            }
174        }
175    
176    
177    
178        // ********************************
179        // *** Listener Implementations ***
180        // ********************************
181    
182    
183    
184        @Override
185        public void introduceTopicType(TopicType topicType) {
186            long workspaceId = workspaceIdForType(topicType);
187            if (workspaceId == -1) {
188                return;
189            }
190            //
191            assignTypeToWorkspace(topicType, workspaceId);
192        }
193    
194        @Override
195        public void introduceAssociationType(AssociationType assocType) {
196            long workspaceId = workspaceIdForType(assocType);
197            if (workspaceId == -1) {
198                return;
199            }
200            //
201            assignTypeToWorkspace(assocType, workspaceId);
202        }
203    
204        // ---
205    
206        /**
207         * Assigns every created topic to the current workspace.
208         */
209        @Override
210        public void postCreateTopic(Topic topic) {
211            if (abortAssignment(topic)) {
212                return;
213            }
214            // Note: we must avoid a vicious circle that would occur when editing a workspace. A Description topic
215            // would be created (as no description is set when the workspace is created) and be assigned to the
216            // workspace itself. This would create an endless recursion while bubbling the modification timestamp.
217            if (isWorkspaceDescription(topic)) {
218                return;
219            }
220            //
221            long workspaceId = workspaceId();
222            // Note: when there is no current workspace (because no user is logged in) we do NOT fallback to assigning
223            // the default workspace. This would not help in gaining data consistency because the topics created so far
224            // (BEFORE the Workspaces plugin is activated) would still have no workspace assignment.
225            // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
226            // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
227            // the other hand we don't have such a mechanism (and don't want one either).
228            if (workspaceId == -1) {
229                return;
230            }
231            //
232            assignToWorkspace(topic, workspaceId);
233        }
234    
235        /**
236         * Assigns every created association to the current workspace.
237         */
238        @Override
239        public void postCreateAssociation(Association assoc) {
240            if (abortAssignment(assoc)) {
241                return;
242            }
243            // Note: we must avoid a vicious circle that would occur when the association is an workspace assignment.
244            if (isWorkspaceAssignment(assoc)) {
245                return;
246            }
247            //
248            long workspaceId = workspaceId();
249            // Note: when there is no current workspace (because no user is logged in) we do NOT fallback to assigning
250            // the default workspace. This would not help in gaining data consistency because the associations created
251            // so far (BEFORE the Workspaces plugin is activated) would still have no workspace assignment.
252            // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
253            // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
254            // the other hand we don't have such a mechanism (and don't want one either).
255            if (workspaceId == -1) {
256                return;
257            }
258            //
259            assignToWorkspace(assoc, workspaceId);
260        }
261    
262    
263    
264        // ------------------------------------------------------------------------------------------------- Private Methods
265    
266        private long workspaceId() {
267            Cookies cookies = Cookies.get();
268            if (!cookies.has("dm4_workspace_id")) {
269                return -1;
270            }
271            return cookies.getLong("dm4_workspace_id");
272        }
273    
274        private long workspaceIdForType(Type type) {
275            long workspaceId = workspaceId();
276            if (workspaceId != -1) {
277                return workspaceId;
278            } else {
279                // assign types of the DeepaMehta standard distribution to the default workspace
280                if (isDeepaMehtaStandardType(type)) {
281                    Topic defaultWorkspace = getDeepaMehtaWorkspace();
282                    // Note: the default workspace is NOT required to exist ### TODO: think about it
283                    if (defaultWorkspace != null) {
284                        return defaultWorkspace.getId();
285                    }
286                }
287            }
288            return -1;
289        }
290    
291        // ---
292    
293        // ### TODO: copy in AccessControlImpl.java
294        private long getAssignedWorkspaceId(long objectId) {
295            return dms.hasProperty(objectId, PROP_WORKSPACE_ID) ? (Long) dms.getProperty(objectId, PROP_WORKSPACE_ID) : -1;
296        }
297    
298        private void _assignToWorkspace(DeepaMehtaObject object, long workspaceId) {
299            try {
300                // 1) create assignment association
301                facetsService.updateFacet(object, "dm4.workspaces.workspace_facet",
302                    new FacetValue("dm4.workspaces.workspace").putRef(workspaceId));
303                // Note: we are refering to an existing workspace. So we must put a topic *reference* (using putRef()).
304                //
305                // 2) store assignment property
306                object.setProperty(PROP_WORKSPACE_ID, workspaceId, false);      // addToIndex=false
307            } catch (Exception e) {
308                throw new RuntimeException("Assigning " + info(object) + " to workspace " + workspaceId + " failed", e);
309            }
310        }
311    
312        // --- Helper ---
313    
314        private boolean isDeepaMehtaStandardType(Type type) {
315            return type.getUri().startsWith("dm4.");
316        }
317    
318        private boolean isWorkspaceDescription(Topic topic) {
319            return topic.getTypeUri().equals("dm4.workspaces.description");
320        }
321    
322        private boolean isWorkspaceAssignment(Association assoc) {
323            if (assoc.getTypeUri().equals("dm4.core.aggregation")) {
324                Topic topic = assoc.getTopic("dm4.core.child");
325                if (topic != null && topic.getTypeUri().equals("dm4.workspaces.workspace")) {
326                    return true;
327                }
328            }
329            return false;
330        }
331    
332        // ---
333    
334        private Topic getDeepaMehtaWorkspace() {
335            return getWorkspace(DEEPAMEHTA_WORKSPACE_URI);
336        }
337    
338        private void applyWorkspaceFilter(Iterator<? extends Topic> topics, long workspaceId) {
339            while (topics.hasNext()) {
340                Topic topic = topics.next();
341                if (!isAssignedToWorkspace(topic.getId(), workspaceId)) {
342                    topics.remove();
343                }
344            }
345        }
346    
347        /**
348         * Checks if the topic with the specified ID exists and is a Workspace. If not, an exception is thrown.
349         */
350        private void checkArgument(long topicId) {
351            String typeUri = dms.getTopic(topicId).getTypeUri();
352            if (!typeUri.equals("dm4.workspaces.workspace")) {
353                throw new IllegalArgumentException("Topic " + topicId + " is not a workspace (but of type \"" + typeUri +
354                    "\")");
355            }
356        }
357    
358        // ### TODO: abort topic and association assignments separately?
359        private boolean abortAssignment(DeepaMehtaObject object) {
360            try {
361                String value = uriInfo.getQueryParameters().getFirst(PARAM_NO_WORKSPACE_ASSIGNMENT);
362                if (value == null) {
363                    // no such parameter in request
364                    return false;
365                }
366                if (!value.equals("false") && !value.equals("true")) {
367                    throw new RuntimeException("\"" + value + "\" is an unexpected value for the \"" +
368                        PARAM_NO_WORKSPACE_ASSIGNMENT + "\" query parameter (expected are \"false\" or \"true\")");
369                }
370                boolean abort = value.equals("true");
371                if (abort) {
372                    logger.info("### Workspace assignment for " + info(object) + " ABORTED -- \"" +
373                        PARAM_NO_WORKSPACE_ASSIGNMENT + "\" query parameter detected");
374                }
375                return abort;
376            } catch (IllegalStateException e) {
377                // Note: this happens if a UriInfo method is called outside request scope
378                return false;
379            }
380        }
381    
382        // ---
383    
384        // ### FIXME: copied from Access Control
385        // ### TODO: add shortInfo() to DeepaMehtaObject interface
386        private String info(DeepaMehtaObject object) {
387            if (object instanceof TopicType) {
388                return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
389            } else if (object instanceof AssociationType) {
390                return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
391            } else if (object instanceof Topic) {
392                return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
393                    "\")";
394            } else if (object instanceof Association) {
395                return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
396            } else {
397                throw new RuntimeException("Unexpected object: " + object);
398            }
399        }
400    }