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 }