001package de.deepamehta.core.impl; 002 003import de.deepamehta.core.model.AssociationDefinitionModel; 004import de.deepamehta.core.model.AssociationModel; 005import de.deepamehta.core.model.ChildTopicsModel; 006import de.deepamehta.core.model.DeepaMehtaObjectModel; 007import de.deepamehta.core.model.RelatedTopicModel; 008import de.deepamehta.core.model.SimpleValue; 009import de.deepamehta.core.model.TopicModel; 010import de.deepamehta.core.model.TopicReferenceModel; 011import de.deepamehta.core.model.TypeModel; 012import de.deepamehta.core.service.Directives; 013 014import java.util.ArrayList; 015import java.util.Iterator; 016import java.util.List; 017import java.util.logging.Logger; 018 019 020 021/** 022 * Helper for storing/fetching simple values and composite value models. 023 */ 024class ValueStorage { 025 026 // ------------------------------------------------------------------------------------------------------- Constants 027 028 private static final String LABEL_CHILD_SEPARATOR = " "; 029 private static final String LABEL_TOPIC_SEPARATOR = ", "; 030 031 // ---------------------------------------------------------------------------------------------- Instance Variables 032 033 private PersistenceLayer pl; 034 035 private Logger logger = Logger.getLogger(getClass().getName()); 036 037 // ---------------------------------------------------------------------------------------------------- Constructors 038 039 ValueStorage(PersistenceLayer pl) { 040 this.pl = pl; 041 } 042 043 // ----------------------------------------------------------------------------------------- Package Private Methods 044 045 /** 046 * Fetches the child topic models (recursively) of the given parent object model and updates it in-place. 047 * ### TODO: recursion is required in some cases (e.g. when fetching a topic through REST API) but is possibly 048 * overhead in others (e.g. when updating composite structures). 049 */ 050 void fetchChildTopics(DeepaMehtaObjectModel parent) { 051 for (AssociationDefinitionModel assocDef : ((DeepaMehtaObjectModelImpl) parent).getType().getAssocDefs()) { 052 fetchChildTopics(parent, assocDef); 053 } 054 } 055 056 /** 057 * Fetches the child topic models (recursively) of the given parent object model and updates it in-place. 058 * ### TODO: recursion is required in some cases (e.g. when fetching a topic through REST API) but is possibly 059 * overhead in others (e.g. when updating composite structures). 060 * <p> 061 * Works for both, "one" and "many" association definitions. 062 * 063 * @param assocDef The child topic models according to this association definition are fetched. 064 */ 065 void fetchChildTopics(DeepaMehtaObjectModel parent, AssociationDefinitionModel assocDef) { 066 try { 067 ChildTopicsModel childTopics = parent.getChildTopicsModel(); 068 String cardinalityUri = assocDef.getChildCardinalityUri(); 069 String assocDefUri = assocDef.getAssocDefUri(); 070 if (cardinalityUri.equals("dm4.core.one")) { 071 RelatedTopicModel childTopic = fetchChildTopic(parent.getId(), assocDef); 072 // Note: topics just created have no child topics yet 073 if (childTopic != null) { 074 childTopics.put(assocDefUri, childTopic); 075 fetchChildTopics(childTopic); // recursion 076 } 077 } else if (cardinalityUri.equals("dm4.core.many")) { 078 for (RelatedTopicModel childTopic : fetchChildTopics(parent.getId(), assocDef)) { 079 childTopics.add(assocDefUri, childTopic); 080 fetchChildTopics(childTopic); // recursion 081 } 082 } else { 083 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 084 } 085 } catch (Exception e) { 086 throw new RuntimeException("Fetching the \"" + assocDef.getAssocDefUri() + "\" child topics of object " + 087 parent.getId() + " failed", e); 088 } 089 } 090 091 // --- 092 093 /** 094 * Stores and indexes the specified model's value, either a simple value or a composite value (child topics). 095 * Depending on the model type's data type dispatches either to storeSimpleValue() or to storeChildTopics(). 096 * <p> 097 * Called to store the initial value of a newly created topic/association. 098 */ 099 void storeValue(DeepaMehtaObjectModelImpl model) { 100 if (model.getType().getDataTypeUri().equals("dm4.core.composite")) { 101 storeChildTopics(model); 102 recalculateLabel(model); 103 } else { 104 model.storeSimpleValue(); 105 } 106 } 107 108 /** 109 * Recalculates the label of the given parent object model and updates it in-place. 110 * Note: no child topics are loaded from the DB. The given parent object model is expected to contain all the 111 * child topic models required for the label calculation. 112 * 113 * @param parent The object model the label is calculated for. This is expected to be a composite model. 114 */ 115 void recalculateLabel(DeepaMehtaObjectModelImpl parent) { 116 try { 117 String label = calculateLabel(parent); 118 parent.updateSimpleValue(new SimpleValue(label)); 119 } catch (Exception e) { 120 throw new RuntimeException("Recalculating label of object " + parent.getId() + " failed (" + parent + ")", 121 e); 122 } 123 } 124 125 // ------------------------------------------------------------------------------------------------- Private Methods 126 127 /** 128 * Stores the composite value (child topics) of the specified topic or association model. 129 * Called to store the initial value of a newly created topic/association. 130 * <p> 131 * Note: the given model can contain childs not defined in the type definition. 132 * Only the childs defined in the type definition are stored. 133 */ 134 private void storeChildTopics(DeepaMehtaObjectModelImpl parent) { 135 ChildTopicsModel model = null; 136 try { 137 model = parent.getChildTopicsModel(); 138 for (AssociationDefinitionModel assocDef : parent.getType().getAssocDefs()) { 139 String assocDefUri = assocDef.getAssocDefUri(); 140 String cardinalityUri = assocDef.getChildCardinalityUri(); 141 if (cardinalityUri.equals("dm4.core.one")) { 142 RelatedTopicModel childTopic = model.getTopicOrNull(assocDefUri); 143 if (childTopic != null) { // skip if not contained in create request 144 storeChildTopic(childTopic, parent, assocDef); 145 } 146 } else if (cardinalityUri.equals("dm4.core.many")) { 147 List<? extends RelatedTopicModel> childTopics = model.getTopicsOrNull(assocDefUri); 148 if (childTopics != null) { // skip if not contained in create request 149 for (RelatedTopicModel childTopic : childTopics) { 150 storeChildTopic(childTopic, parent, assocDef); 151 } 152 } 153 } else { 154 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 155 } 156 } 157 } catch (Exception e) { 158 throw new RuntimeException("Storing the child topics of object " + parent.getId() + " failed (" + 159 model + ")", e); 160 } 161 } 162 163 private void storeChildTopic(RelatedTopicModel childTopic, DeepaMehtaObjectModel parent, 164 AssociationDefinitionModel assocDef) { 165 if (childTopic instanceof TopicReferenceModel) { 166 resolveReference((TopicReferenceModel) childTopic); 167 } else { 168 pl.createTopic(childTopic); 169 } 170 associateChildTopic(parent, childTopic, assocDef); 171 } 172 173 // --- 174 175 /** 176 * Replaces a reference with the real thing. 177 */ 178 void resolveReference(TopicReferenceModel topicRef) { 179 topicRef.set(fetchReferencedTopic(topicRef)); 180 } 181 182 private TopicModel fetchReferencedTopic(TopicReferenceModel topicRef) { 183 // Note: the resolved topic must be fetched including its composite value. 184 // It might be required at client-side. ### TODO 185 if (topicRef.isReferenceById()) { 186 return pl.fetchTopic(topicRef.getId()); // ### FIXME: had fetchComposite=true 187 } else if (topicRef.isReferenceByUri()) { 188 TopicModel topic = pl.fetchTopic("uri", new SimpleValue(topicRef.getUri())); // ### FIXME: had 189 if (topic == null) { // fetchComposite=true 190 throw new RuntimeException("Topic with URI \"" + topicRef.getUri() + "\" not found"); 191 } 192 return topic; 193 } else { 194 throw new RuntimeException("Invalid topic reference (" + topicRef + ")"); 195 } 196 } 197 198 // --- 199 200 /** 201 * Creates an association between the given parent object ("Parent" role) and the child topic ("Child" role). 202 * The association type is taken from the given association definition. 203 */ 204 void associateChildTopic(DeepaMehtaObjectModel parent, RelatedTopicModel childTopic, 205 AssociationDefinitionModel assocDef) { 206 AssociationModel assoc = childTopic.getRelatingAssociation(); 207 assoc.setTypeUri(assocDef.getInstanceLevelAssocTypeUri()); 208 assoc.setRoleModel1(parent.createRoleModel("dm4.core.parent")); 209 assoc.setRoleModel2(childTopic.createRoleModel("dm4.core.child")); 210 pl.createAssociation((AssociationModelImpl) assoc); 211 } 212 213 214 215 // === Label === 216 217 private String calculateLabel(DeepaMehtaObjectModelImpl model) { 218 TypeModel type = model.getType(); 219 if (type.getDataTypeUri().equals("dm4.core.composite")) { 220 StringBuilder label = new StringBuilder(); 221 for (String assocDefUri : getLabelAssocDefUris(model)) { 222 appendLabel(buildChildLabel(model, assocDefUri), label, LABEL_CHILD_SEPARATOR); 223 } 224 return label.toString(); 225 } else { 226 return model.getSimpleValue().toString(); 227 } 228 } 229 230 /** 231 * Prerequisite: parent is a composite model. 232 */ 233 List<String> getLabelAssocDefUris(DeepaMehtaObjectModel parent) { 234 TypeModel type = ((DeepaMehtaObjectModelImpl) parent).getType(); 235 List<String> labelConfig = type.getLabelConfig(); 236 if (labelConfig.size() > 0) { 237 return labelConfig; 238 } else { 239 List<String> assocDefUris = new ArrayList(); 240 Iterator<? extends AssociationDefinitionModel> i = type.getAssocDefs().iterator(); 241 // Note: types just created might have no child types yet 242 if (i.hasNext()) { 243 assocDefUris.add(i.next().getAssocDefUri()); 244 } 245 return assocDefUris; 246 } 247 } 248 249 private String buildChildLabel(DeepaMehtaObjectModel parent, String assocDefUri) { 250 Object value = parent.getChildTopicsModel().get(assocDefUri); 251 // Note: topics just created have no child topics yet 252 if (value == null) { 253 return ""; 254 } 255 // 256 if (value instanceof TopicModel) { 257 TopicModelImpl childTopic = (TopicModelImpl) value; 258 return calculateLabel(childTopic); // recursion 259 } else if (value instanceof List) { 260 StringBuilder label = new StringBuilder(); 261 for (TopicModel childTopic : (List<TopicModel>) value) { 262 appendLabel(calculateLabel((TopicModelImpl) childTopic), label, LABEL_TOPIC_SEPARATOR); // recursion 263 } 264 return label.toString(); 265 } else { 266 throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value); 267 } 268 } 269 270 private void appendLabel(String label, StringBuilder builder, String separator) { 271 // add separator 272 if (builder.length() > 0 && label.length() > 0) { 273 builder.append(separator); 274 } 275 // 276 builder.append(label); 277 } 278 279 280 281 // === Helper === 282 283 /** 284 * Fetches and returns a child topic or <code>null</code> if no such topic extists. 285 */ 286 private RelatedTopicModel fetchChildTopic(long parentId, AssociationDefinitionModel assocDef) { 287 return pl.fetchRelatedTopic( 288 parentId, 289 assocDef.getInstanceLevelAssocTypeUri(), 290 "dm4.core.parent", "dm4.core.child", 291 assocDef.getChildTypeUri() 292 ); 293 } 294 295 private List<RelatedTopicModelImpl> fetchChildTopics(long parentId, AssociationDefinitionModel assocDef) { 296 return pl.fetchRelatedTopics( 297 parentId, 298 assocDef.getInstanceLevelAssocTypeUri(), 299 "dm4.core.parent", "dm4.core.child", 300 assocDef.getChildTypeUri() 301 ); 302 } 303}