001 package de.deepamehta.core.impl; 002 003 import de.deepamehta.core.AssociationDefinition; 004 import de.deepamehta.core.Topic; 005 import de.deepamehta.core.Type; 006 import de.deepamehta.core.model.AssociationModel; 007 import de.deepamehta.core.model.CompositeValueModel; 008 import de.deepamehta.core.model.DeepaMehtaObjectModel; 009 import de.deepamehta.core.model.IndexMode; 010 import de.deepamehta.core.model.RelatedTopicModel; 011 import de.deepamehta.core.model.SimpleValue; 012 import de.deepamehta.core.model.TopicModel; 013 import de.deepamehta.core.model.TopicReferenceModel; 014 import de.deepamehta.core.model.TopicRoleModel; 015 import de.deepamehta.core.service.ClientState; 016 import de.deepamehta.core.service.Directives; 017 import de.deepamehta.core.service.ResultList; 018 import de.deepamehta.core.util.JavaUtils; 019 020 import java.util.Iterator; 021 import java.util.List; 022 import java.util.logging.Logger; 023 024 025 026 /** 027 * Helper for storing/fetching simple values and composite value models. 028 */ 029 class ValueStorage { 030 031 // ------------------------------------------------------------------------------------------------------- Constants 032 033 private static final String LABEL_CHILD_SEPARATOR = " "; 034 private static final String LABEL_TOPIC_SEPARATOR = ", "; 035 036 // ---------------------------------------------------------------------------------------------- Instance Variables 037 038 private EmbeddedService dms; 039 040 private Logger logger = Logger.getLogger(getClass().getName()); 041 042 // ---------------------------------------------------------------------------------------------------- Constructors 043 044 ValueStorage(EmbeddedService dms) { 045 this.dms = dms; 046 } 047 048 // ----------------------------------------------------------------------------------------- Package Private Methods 049 050 /** 051 * Recursively fetches the composite value (child topic models) of the given parent object model and updates it 052 * in-place. 053 */ 054 void fetchCompositeValue(DeepaMehtaObjectModel parent) { 055 try { 056 Type type = getType(parent); 057 if (!type.getDataTypeUri().equals("dm4.core.composite")) { 058 return; 059 } 060 // 061 for (AssociationDefinition assocDef : type.getAssocDefs()) { 062 fetchChildTopics(parent, assocDef); 063 } 064 } catch (Exception e) { 065 throw new RuntimeException("Fetching composite value of object " + parent.getId() + " failed (" + 066 parent + ")", e); 067 } 068 } 069 070 /** 071 * Recursively fetches the child topic models of the given parent object model and updates it in-place. 072 * <p> 073 * Works for both, "one" and "many" association definitions. 074 * 075 * @param assocDef The child topic models according to this association definition are fetched. 076 */ 077 void fetchChildTopics(DeepaMehtaObjectModel parent, AssociationDefinition assocDef) { 078 CompositeValueModel comp = parent.getCompositeValueModel(); 079 String cardinalityUri = assocDef.getChildCardinalityUri(); 080 String childTypeUri = assocDef.getChildTypeUri(); 081 if (cardinalityUri.equals("dm4.core.one")) { 082 RelatedTopicModel childTopic = fetchChildTopic(parent.getId(), assocDef); 083 // Note: topics just created have no child topics yet 084 if (childTopic != null) { 085 comp.put(childTypeUri, childTopic); 086 fetchCompositeValue(childTopic); // recursion 087 } 088 } else if (cardinalityUri.equals("dm4.core.many")) { 089 for (RelatedTopicModel childTopic : fetchChildTopics(parent.getId(), assocDef)) { 090 comp.add(childTypeUri, childTopic); 091 fetchCompositeValue(childTopic); // recursion 092 } 093 } else { 094 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 095 } 096 } 097 098 // --- 099 100 /** 101 * Stores and indexes the specified model's value, either a simple value or a composite value (child topics). 102 * Depending on the model type's data type dispatches either to storeSimpleValue() or to storeCompositeValue(). 103 * <p> 104 * Called to store the initial value of a newly created topic/association. 105 */ 106 void storeValue(DeepaMehtaObjectModel model, ClientState clientState, Directives directives) { 107 if (getType(model).getDataTypeUri().equals("dm4.core.composite")) { 108 storeCompositeValue(model, clientState, directives); 109 refreshLabel(model); 110 } else { 111 storeSimpleValue(model); 112 } 113 } 114 115 /** 116 * Indexes the simple value of the given object model according to the given index mode. 117 * <p> 118 * Called to index existing topics/associations once an index mode has been added to a type definition. 119 */ 120 void indexSimpleValue(DeepaMehtaObjectModel model, IndexMode indexMode) { 121 if (model instanceof TopicModel) { 122 dms.storageDecorator.indexTopicValue( 123 model.getId(), 124 indexMode, 125 model.getTypeUri(), 126 getIndexValue(model) 127 ); 128 } else if (model instanceof AssociationModel) { 129 dms.storageDecorator.indexAssociationValue( 130 model.getId(), 131 indexMode, 132 model.getTypeUri(), 133 getIndexValue(model) 134 ); 135 } 136 } 137 138 // --- 139 140 /** 141 * Prerequisite: this is a composite object. 142 */ 143 void refreshLabel(DeepaMehtaObjectModel model) { 144 try { 145 String label = buildLabel(model); 146 setSimpleValue(model, new SimpleValue(label)); 147 } catch (Exception e) { 148 throw new RuntimeException("Refreshing label of object " + model.getId() + " failed (" + model + ")", e); 149 } 150 } 151 152 void setSimpleValue(DeepaMehtaObjectModel model, SimpleValue value) { 153 if (value == null) { 154 throw new IllegalArgumentException("Tried to set a null SimpleValue (" + this + ")"); 155 } 156 // update memory 157 model.setSimpleValue(value); 158 // update DB 159 storeSimpleValue(model); 160 } 161 162 163 164 // === Helper === 165 166 /** 167 * Creates an association between the given parent object ("Parent" role) and the referenced topic ("Child" role). 168 * The association type is taken from the given association definition. 169 * 170 * @return the resolved child topic, including its composite values. 171 */ 172 Topic associateReferencedChildTopic(DeepaMehtaObjectModel parent, TopicReferenceModel childTopicRef, 173 AssociationDefinition assocDef, ClientState clientState) { 174 if (childTopicRef.isReferenceById()) { 175 long childTopicId = childTopicRef.getId(); 176 associateChildTopic(parent, childTopicId, assocDef, clientState); 177 // Note: the resolved topic must be fetched including its composite value. 178 // It might be required at client-side. 179 return dms.getTopic(childTopicId, true); // fetchComposite=true 180 } else if (childTopicRef.isReferenceByUri()) { 181 String childTopicUri = childTopicRef.getUri(); 182 associateChildTopic(parent, childTopicUri, assocDef, clientState); 183 // Note: the resolved topic must be fetched including its composite value. 184 // It might be required at client-side. 185 return dms.getTopic("uri", new SimpleValue(childTopicUri), true); // fetchComposite=true 186 } else { 187 throw new RuntimeException("Invalid topic reference (" + childTopicRef + ")"); 188 } 189 } 190 191 void associateChildTopic(DeepaMehtaObjectModel parent, long childTopicId, AssociationDefinition assocDef, 192 ClientState clientState) { 193 associateChildTopic(parent, new TopicRoleModel(childTopicId, "dm4.core.child"), assocDef, clientState); 194 } 195 196 void associateChildTopic(DeepaMehtaObjectModel parent, String childTopicUri, AssociationDefinition assocDef, 197 ClientState clientState) { 198 associateChildTopic(parent, new TopicRoleModel(childTopicUri, "dm4.core.child"), assocDef, clientState); 199 } 200 201 // --- 202 203 /** 204 * Convenience method to get the (attached) type of a DeepaMehta object model. 205 * The type is obtained from the core service's type cache. 206 */ 207 Type getType(DeepaMehtaObjectModel model) { 208 if (model instanceof TopicModel) { 209 return dms.getTopicType(model.getTypeUri()); 210 } else if (model instanceof AssociationModel) { 211 return dms.getAssociationType(model.getTypeUri()); 212 } 213 throw new RuntimeException("Unexpected model: " + model); 214 } 215 216 217 218 // ------------------------------------------------------------------------------------------------- Private Methods 219 220 /** 221 * Stores and indexes the simple value of the specified topic or association model. 222 * Determines the index key and index modes. 223 */ 224 private void storeSimpleValue(DeepaMehtaObjectModel model) { 225 Type type = getType(model); 226 if (model instanceof TopicModel) { 227 dms.storageDecorator.storeTopicValue( 228 model.getId(), 229 model.getSimpleValue(), 230 type.getIndexModes(), 231 type.getUri(), 232 getIndexValue(model) 233 ); 234 } else if (model instanceof AssociationModel) { 235 dms.storageDecorator.storeAssociationValue( 236 model.getId(), 237 model.getSimpleValue(), 238 type.getIndexModes(), 239 type.getUri(), 240 getIndexValue(model) 241 ); 242 } 243 } 244 245 /** 246 * Called to store the initial value of a newly created topic/association. 247 * Just prepares the arguments and calls storeChildTopics() repetitively. 248 * <p> 249 * Note: the given model can contain childs not defined in the type definition. 250 * Only the childs defined in the type definition are stored. 251 */ 252 private void storeCompositeValue(DeepaMehtaObjectModel parent, ClientState clientState, Directives directives) { 253 CompositeValueModel model = null; 254 try { 255 model = parent.getCompositeValueModel(); 256 for (AssociationDefinition assocDef : getType(parent).getAssocDefs()) { 257 String childTypeUri = assocDef.getChildTypeUri(); 258 String cardinalityUri = assocDef.getChildCardinalityUri(); 259 TopicModel childTopic = null; // only used for "one" 260 List<TopicModel> childTopics = null; // only used for "many" 261 if (cardinalityUri.equals("dm4.core.one")) { 262 childTopic = model.getTopic(childTypeUri, null); // defaultValue=null 263 // skip if not contained in create request 264 if (childTopic == null) { 265 continue; 266 } 267 } else if (cardinalityUri.equals("dm4.core.many")) { 268 childTopics = model.getTopics(childTypeUri, null); // defaultValue=null 269 // skip if not contained in create request 270 if (childTopics == null) { 271 continue; 272 } 273 } else { 274 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 275 } 276 // 277 storeChildTopics(childTopic, childTopics, parent, assocDef, clientState, directives); 278 } 279 } catch (Exception e) { 280 throw new RuntimeException("Storing composite value of object " + parent.getId() + " failed (" + 281 model + ")", e); 282 } 283 } 284 285 // --- 286 287 private void storeChildTopics(TopicModel childTopic, List<TopicModel> childTopics, DeepaMehtaObjectModel parent, 288 AssociationDefinition assocDef, ClientState clientState, Directives directives) { 289 String assocTypeUri = assocDef.getTypeUri(); 290 boolean one = childTopic != null; 291 if (assocTypeUri.equals("dm4.core.composition_def")) { 292 if (one) { 293 storeCompositionOne(childTopic, parent, assocDef, clientState, directives); 294 } else { 295 storeCompositionMany(childTopics, parent, assocDef, clientState, directives); 296 } 297 } else if (assocTypeUri.equals("dm4.core.aggregation_def")) { 298 if (one) { 299 storeAggregationOne(childTopic, parent, assocDef, clientState, directives); 300 } else { 301 storeAggregationMany(childTopics, parent, assocDef, clientState, directives); 302 } 303 } else { 304 throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported"); 305 } 306 } 307 308 // --- Composition --- 309 310 private void storeCompositionOne(TopicModel model, DeepaMehtaObjectModel parent, 311 AssociationDefinition assocDef, ClientState clientState, Directives directives) { 312 // == create child == 313 // update DB 314 Topic childTopic = dms.createTopic(model, clientState); 315 associateChildTopic(parent, childTopic.getId(), assocDef, clientState); 316 // Note: memory is already up-to-date. The child topic ID is updated in-place. 317 } 318 319 private void storeCompositionMany(List<TopicModel> models, DeepaMehtaObjectModel parent, 320 AssociationDefinition assocDef, ClientState clientState, Directives directives) { 321 for (TopicModel model : models) { 322 // == create child == 323 // update DB 324 Topic childTopic = dms.createTopic(model, clientState); 325 associateChildTopic(parent, childTopic.getId(), assocDef, clientState); 326 // Note: memory is already up-to-date. The child topic ID is updated in-place. 327 } 328 } 329 330 // --- Aggregation --- 331 332 private void storeAggregationOne(TopicModel model, DeepaMehtaObjectModel parent, 333 AssociationDefinition assocDef, ClientState clientState, Directives directives) { 334 if (model instanceof TopicReferenceModel) { 335 // == create assignment == 336 // update DB 337 Topic childTopic = associateReferencedChildTopic(parent, (TopicReferenceModel) model, assocDef, 338 clientState); 339 // update memory 340 putInCompositeValue(parent, childTopic, assocDef); 341 } else { 342 // == create child == 343 // update DB 344 Topic childTopic = dms.createTopic(model, clientState); 345 associateChildTopic(parent, childTopic.getId(), assocDef, clientState); 346 // Note: memory is already up-to-date. The child topic ID is updated in-place. 347 } 348 } 349 350 private void storeAggregationMany(List<TopicModel> models, DeepaMehtaObjectModel parent, 351 AssociationDefinition assocDef, ClientState clientState, Directives directives) { 352 for (TopicModel model : models) { 353 if (model instanceof TopicReferenceModel) { 354 // == create assignment == 355 // update DB 356 Topic childTopic = associateReferencedChildTopic(parent, (TopicReferenceModel) model, assocDef, 357 clientState); 358 // update memory 359 replaceReference(model, childTopic); 360 } else { 361 // == create child == 362 // update DB 363 Topic childTopic = dms.createTopic(model, clientState); 364 associateChildTopic(parent, childTopic.getId(), assocDef, clientState); 365 // Note: memory is already up-to-date. The child topic ID is updated in-place. 366 } 367 } 368 } 369 370 // --- 371 372 /** 373 * For single-valued childs 374 */ 375 private void putInCompositeValue(DeepaMehtaObjectModel parent, Topic childTopic, AssociationDefinition assocDef) { 376 parent.getCompositeValueModel().put(assocDef.getChildTypeUri(), childTopic.getModel()); 377 } 378 379 /** 380 * Replaces a topic reference with the resolved topic. 381 * 382 * Used for multiple-valued childs. 383 */ 384 private void replaceReference(TopicModel topicRef, Topic topic) { 385 // Note: we must update the topic reference in-place. 386 // Replacing the entire topic in the list of child topics would cause ConcurrentModificationException. 387 topicRef.set(topic.getModel()); 388 } 389 390 391 392 // === Label === 393 394 private String buildLabel(DeepaMehtaObjectModel model) { 395 Type type = getType(model); 396 if (type.getDataTypeUri().equals("dm4.core.composite")) { 397 List<String> labelConfig = type.getLabelConfig(); 398 if (labelConfig.size() > 0) { 399 return buildLabelFromConfig(model, labelConfig); 400 } else { 401 return buildDefaultLabel(model); 402 } 403 } else { 404 return model.getSimpleValue().toString(); 405 } 406 } 407 408 /** 409 * Builds the specified object model's label according to a label configuration. 410 */ 411 private String buildLabelFromConfig(DeepaMehtaObjectModel model, List<String> labelConfig) { 412 StringBuilder builder = new StringBuilder(); 413 for (String childTypeUri : labelConfig) { 414 appendLabel(buildChildLabel(model, childTypeUri), builder, LABEL_CHILD_SEPARATOR); 415 } 416 return builder.toString(); 417 } 418 419 private String buildDefaultLabel(DeepaMehtaObjectModel model) { 420 Iterator<AssociationDefinition> i = getType(model).getAssocDefs().iterator(); 421 // Note: types just created might have no child types yet 422 if (!i.hasNext()) { 423 return ""; 424 } 425 // 426 String childTypeUri = i.next().getChildTypeUri(); 427 return buildChildLabel(model, childTypeUri); 428 } 429 430 // --- 431 432 private String buildChildLabel(DeepaMehtaObjectModel parent, String childTypeUri) { 433 Object value = parent.getCompositeValueModel().get(childTypeUri); 434 // Note: topics just created have no child topics yet 435 if (value == null) { 436 return ""; 437 } 438 // 439 if (value instanceof TopicModel) { 440 TopicModel childTopic = (TopicModel) value; 441 return buildLabel(childTopic); // recursion 442 } else if (value instanceof List) { 443 StringBuilder builder = new StringBuilder(); 444 for (TopicModel childTopic : (List<TopicModel>) value) { 445 appendLabel(buildLabel(childTopic), builder, LABEL_TOPIC_SEPARATOR); // recursion 446 } 447 return builder.toString(); 448 } else { 449 throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value); 450 } 451 } 452 453 private void appendLabel(String label, StringBuilder builder, String separator) { 454 // add separator 455 if (builder.length() > 0 && label.length() > 0) { 456 builder.append(separator); 457 } 458 // 459 builder.append(label); 460 } 461 462 463 464 // === Helper === 465 466 /** 467 * Fetches and returns a child topic or <code>null</code> if no such topic extists. 468 */ 469 private RelatedTopicModel fetchChildTopic(long parentId, AssociationDefinition assocDef) { 470 return dms.storageDecorator.fetchRelatedTopic( 471 parentId, 472 assocDef.getInstanceLevelAssocTypeUri(), 473 "dm4.core.parent", "dm4.core.child", 474 assocDef.getChildTypeUri() 475 ); 476 } 477 478 private ResultList<RelatedTopicModel> fetchChildTopics(long parentId, AssociationDefinition assocDef) { 479 return dms.storageDecorator.fetchRelatedTopics( 480 parentId, 481 assocDef.getInstanceLevelAssocTypeUri(), 482 "dm4.core.parent", "dm4.core.child", 483 assocDef.getChildTypeUri() 484 ); 485 } 486 487 // --- 488 489 private void associateChildTopic(DeepaMehtaObjectModel parent, TopicRoleModel child, AssociationDefinition assocDef, 490 ClientState clientState) { 491 dms.createAssociation(assocDef.getInstanceLevelAssocTypeUri(), 492 parent.createRoleModel("dm4.core.parent"), child, clientState 493 ); 494 } 495 496 // --- 497 498 /** 499 * Calculates the simple value that is to be indexed for this object. 500 * 501 * HTML tags are stripped from HTML values. Non-HTML values are returned directly. 502 */ 503 private SimpleValue getIndexValue(DeepaMehtaObjectModel model) { 504 SimpleValue value = model.getSimpleValue(); 505 if (getType(model).getDataTypeUri().equals("dm4.core.html")) { 506 return new SimpleValue(JavaUtils.stripHTML(value.toString())); 507 } else { 508 return value; 509 } 510 } 511 }