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