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