001 package de.deepamehta.core.impl; 002 003 import de.deepamehta.core.Topic; 004 import de.deepamehta.core.model.AssociationDefinitionModel; 005 import de.deepamehta.core.model.AssociationModel; 006 import de.deepamehta.core.model.ChildTopicsModel; 007 import de.deepamehta.core.model.DeepaMehtaObjectModel; 008 import de.deepamehta.core.model.IndexMode; 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.TypeModel; 014 import de.deepamehta.core.service.ResultList; 015 import de.deepamehta.core.util.JavaUtils; 016 017 import java.util.ArrayList; 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 * Fetches the child topic models (recursively) of the given parent object model and updates it in-place. 050 * ### TODO: recursion is required in some cases (e.g. when fetching a topic through REST API) but is possibly 051 * overhead in others (e.g. when updating composite structures). 052 */ 053 private void fetchChildTopics(DeepaMehtaObjectModel parent) { 054 for (AssociationDefinitionModel assocDef : getType(parent).getAssocDefs()) { 055 fetchChildTopics(parent, assocDef); 056 } 057 } 058 059 /** 060 * Fetches the child topic models (recursively) of the given parent object model and updates it in-place. 061 * ### TODO: recursion is required in some cases (e.g. when fetching a topic through REST API) but is possibly 062 * overhead in others (e.g. when updating composite structures). 063 * <p> 064 * Works for both, "one" and "many" association definitions. 065 * 066 * @param assocDef The child topic models according to this association definition are fetched. 067 */ 068 void fetchChildTopics(DeepaMehtaObjectModel parent, AssociationDefinitionModel assocDef) { 069 try { 070 ChildTopicsModel childTopics = parent.getChildTopicsModel(); 071 String cardinalityUri = assocDef.getChildCardinalityUri(); 072 String childTypeUri = assocDef.getChildTypeUri(); 073 if (cardinalityUri.equals("dm4.core.one")) { 074 RelatedTopicModel childTopic = fetchChildTopic(parent.getId(), assocDef); 075 // Note: topics just created have no child topics yet 076 if (childTopic != null) { 077 childTopics.put(childTypeUri, childTopic); 078 fetchChildTopics(childTopic); // recursion 079 } 080 } else if (cardinalityUri.equals("dm4.core.many")) { 081 for (RelatedTopicModel childTopic : fetchChildTopics(parent.getId(), assocDef)) { 082 childTopics.add(childTypeUri, childTopic); 083 fetchChildTopics(childTopic); // recursion 084 } 085 } else { 086 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 087 } 088 } catch (Exception e) { 089 throw new RuntimeException("Fetching the \"" + assocDef.getChildTypeUri() + "\" child topics of object " + 090 parent.getId() + " failed", e); 091 } 092 } 093 094 // --- 095 096 /** 097 * Stores and indexes the specified model's value, either a simple value or a composite value (child topics). 098 * Depending on the model type's data type dispatches either to storeSimpleValue() or to storeChildTopics(). 099 * <p> 100 * Called to store the initial value of a newly created topic/association. 101 */ 102 void storeValue(DeepaMehtaObjectModel model) { 103 if (getType(model).getDataTypeUri().equals("dm4.core.composite")) { 104 storeChildTopics(model); 105 refreshLabel(model); 106 } else { 107 storeSimpleValue(model); 108 } 109 } 110 111 /** 112 * Indexes the simple value of the given object model according to the given index mode. 113 * <p> 114 * Called to index existing topics/associations once an index mode has been added to a type definition. 115 */ 116 void indexSimpleValue(DeepaMehtaObjectModel model, IndexMode indexMode) { 117 if (model instanceof TopicModel) { 118 dms.storageDecorator.indexTopicValue( 119 model.getId(), 120 indexMode, 121 model.getTypeUri(), 122 getIndexValue(model) 123 ); 124 } else if (model instanceof AssociationModel) { 125 dms.storageDecorator.indexAssociationValue( 126 model.getId(), 127 indexMode, 128 model.getTypeUri(), 129 getIndexValue(model) 130 ); 131 } 132 } 133 134 // --- 135 136 /** 137 * Recalculates the label of the given parent object model and updates it in-place. 138 * Note: no child topics are loaded from the DB. The given parent object model is expected to contain all the 139 * child topic models required for the label calculation. 140 * 141 * @param parent The object model the label is calculated for. This is expected to be a composite model. 142 */ 143 void refreshLabel(DeepaMehtaObjectModel parent) { 144 try { 145 String label = buildLabel(parent); 146 setSimpleValue(parent, new SimpleValue(label)); 147 } catch (Exception e) { 148 throw new RuntimeException("Refreshing label of object " + parent.getId() + " failed (" + parent + ")", 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 // ------------------------------------------------------------------------------------------------- Private Methods 165 166 /** 167 * Stores and indexes the simple value of the specified topic or association model. 168 * Determines the index key and index modes. 169 */ 170 private void storeSimpleValue(DeepaMehtaObjectModel model) { 171 TypeModel type = getType(model); 172 if (model instanceof TopicModel) { 173 dms.storageDecorator.storeTopicValue( 174 model.getId(), 175 model.getSimpleValue(), 176 type.getIndexModes(), 177 type.getUri(), 178 getIndexValue(model) 179 ); 180 } else if (model instanceof AssociationModel) { 181 dms.storageDecorator.storeAssociationValue( 182 model.getId(), 183 model.getSimpleValue(), 184 type.getIndexModes(), 185 type.getUri(), 186 getIndexValue(model) 187 ); 188 } 189 } 190 191 // --- 192 193 /** 194 * Stores the composite value (child topics) of the specified topic or association model. 195 * Called to store the initial value of a newly created topic/association. 196 * <p> 197 * Note: the given model can contain childs not defined in the type definition. 198 * Only the childs defined in the type definition are stored. 199 */ 200 private void storeChildTopics(DeepaMehtaObjectModel parent) { 201 ChildTopicsModel model = null; 202 try { 203 model = parent.getChildTopicsModel(); 204 for (AssociationDefinitionModel assocDef : getType(parent).getAssocDefs()) { 205 String childTypeUri = assocDef.getChildTypeUri(); 206 String cardinalityUri = assocDef.getChildCardinalityUri(); 207 if (cardinalityUri.equals("dm4.core.one")) { 208 RelatedTopicModel childTopic = model.getTopic(childTypeUri, null); // defaultValue=null 209 if (childTopic != null) { // skip if not contained in create request 210 storeChildTopic(childTopic, parent, assocDef); 211 } 212 } else if (cardinalityUri.equals("dm4.core.many")) { 213 List<RelatedTopicModel> childTopics = model.getTopics(childTypeUri, null); // defaultValue=null 214 if (childTopics != null) { // skip if not contained in create request 215 for (RelatedTopicModel childTopic : childTopics) { 216 storeChildTopic(childTopic, parent, assocDef); 217 } 218 } 219 } else { 220 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 221 } 222 } 223 } catch (Exception e) { 224 throw new RuntimeException("Storing the child topics of object " + parent.getId() + " failed (" + 225 model + ")", e); 226 } 227 } 228 229 private void storeChildTopic(RelatedTopicModel childTopic, DeepaMehtaObjectModel parent, 230 AssociationDefinitionModel assocDef) { 231 if (childTopic instanceof TopicReferenceModel) { 232 resolveReference((TopicReferenceModel) childTopic); 233 } else { 234 dms.createTopic(childTopic); 235 } 236 associateChildTopic(parent, childTopic, assocDef); 237 } 238 239 // --- 240 241 /** 242 * Replaces a reference with the real thing. 243 */ 244 void resolveReference(TopicReferenceModel topicRef) { 245 topicRef.set(fetchReferencedTopic(topicRef).getModel()); 246 } 247 248 private Topic fetchReferencedTopic(TopicReferenceModel topicRef) { 249 // Note: the resolved topic must be fetched including its composite value. 250 // It might be required at client-side. ### TODO 251 if (topicRef.isReferenceById()) { 252 return dms.getTopic(topicRef.getId()); // ### FIXME: had fetchComposite=true 253 } else if (topicRef.isReferenceByUri()) { 254 return dms.getTopic("uri", new SimpleValue(topicRef.getUri())); // ### FIXME: had fetchComposite=true 255 } else { 256 throw new RuntimeException("Invalid topic reference (" + topicRef + ")"); 257 } 258 } 259 260 // --- 261 262 /** 263 * Creates an association between the given parent object ("Parent" role) and the child topic ("Child" role). 264 * The association type is taken from the given association definition. 265 */ 266 void associateChildTopic(DeepaMehtaObjectModel parent, RelatedTopicModel childTopic, 267 AssociationDefinitionModel assocDef) { 268 AssociationModel assoc = childTopic.getRelatingAssociation(); 269 assoc.setTypeUri(assocDef.getInstanceLevelAssocTypeUri()); 270 assoc.setRoleModel1(parent.createRoleModel("dm4.core.parent")); 271 assoc.setRoleModel2(childTopic.createRoleModel("dm4.core.child")); 272 dms.createAssociation(assoc); 273 } 274 275 276 277 // === Label === 278 279 private String buildLabel(DeepaMehtaObjectModel model) { 280 TypeModel type = getType(model); 281 if (type.getDataTypeUri().equals("dm4.core.composite")) { 282 StringBuilder label = new StringBuilder(); 283 for (String childTypeUri : getLabelChildTypeUris(model)) { 284 appendLabel(buildChildLabel(model, childTypeUri), label, LABEL_CHILD_SEPARATOR); 285 } 286 return label.toString(); 287 } else { 288 return model.getSimpleValue().toString(); 289 } 290 } 291 292 /** 293 * Prerequisite: parent is a composite model. 294 */ 295 List<String> getLabelChildTypeUris(DeepaMehtaObjectModel parent) { 296 TypeModel type = getType(parent); 297 List<String> labelConfig = type.getLabelConfig(); 298 if (labelConfig.size() > 0) { 299 return labelConfig; 300 } else { 301 List<String> childTypeUris = new ArrayList(); 302 Iterator<AssociationDefinitionModel> i = type.getAssocDefs().iterator(); 303 // Note: types just created might have no child types yet 304 if (i.hasNext()) { 305 childTypeUris.add(i.next().getChildTypeUri()); 306 } 307 return childTypeUris; 308 } 309 } 310 311 private String buildChildLabel(DeepaMehtaObjectModel parent, String childTypeUri) { 312 Object value = parent.getChildTopicsModel().get(childTypeUri); 313 // Note: topics just created have no child topics yet 314 if (value == null) { 315 return ""; 316 } 317 // 318 if (value instanceof TopicModel) { 319 TopicModel childTopic = (TopicModel) value; 320 return buildLabel(childTopic); // recursion 321 } else if (value instanceof List) { 322 StringBuilder label = new StringBuilder(); 323 for (TopicModel childTopic : (List<TopicModel>) value) { 324 appendLabel(buildLabel(childTopic), label, LABEL_TOPIC_SEPARATOR); // recursion 325 } 326 return label.toString(); 327 } else { 328 throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value); 329 } 330 } 331 332 private void appendLabel(String label, StringBuilder builder, String separator) { 333 // add separator 334 if (builder.length() > 0 && label.length() > 0) { 335 builder.append(separator); 336 } 337 // 338 builder.append(label); 339 } 340 341 342 343 // === Helper === 344 345 /** 346 * Fetches and returns a child topic or <code>null</code> if no such topic extists. 347 */ 348 private RelatedTopicModel fetchChildTopic(long parentId, AssociationDefinitionModel assocDef) { 349 return dms.storageDecorator.fetchRelatedTopic( 350 parentId, 351 assocDef.getInstanceLevelAssocTypeUri(), 352 "dm4.core.parent", "dm4.core.child", 353 assocDef.getChildTypeUri() 354 ); 355 } 356 357 private ResultList<RelatedTopicModel> fetchChildTopics(long parentId, AssociationDefinitionModel assocDef) { 358 return dms.storageDecorator.fetchRelatedTopics( 359 parentId, 360 assocDef.getInstanceLevelAssocTypeUri(), 361 "dm4.core.parent", "dm4.core.child", 362 assocDef.getChildTypeUri() 363 ); 364 } 365 366 // --- 367 368 /** 369 * Calculates the simple value that is to be indexed for this object. 370 * 371 * HTML tags are stripped from HTML values. Non-HTML values are returned directly. 372 */ 373 private SimpleValue getIndexValue(DeepaMehtaObjectModel model) { 374 SimpleValue value = model.getSimpleValue(); 375 if (getType(model).getDataTypeUri().equals("dm4.core.html")) { 376 return new SimpleValue(JavaUtils.stripHTML(value.toString())); 377 } else { 378 return value; 379 } 380 } 381 382 /** 383 * Returns the type model of a DeepaMehta object model. 384 * The type is obtained from the type storage. 385 */ 386 private TypeModel getType(DeepaMehtaObjectModel model) { 387 if (model instanceof TopicModel) { 388 return dms.typeStorage.getTopicType(model.getTypeUri()); 389 } else if (model instanceof AssociationModel) { 390 return dms.typeStorage.getAssociationType(model.getTypeUri()); 391 } 392 throw new RuntimeException("Unexpected model: " + model); 393 } 394 }