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