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 }