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