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