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 }