001package systems.dmx.core.impl;
002
003import systems.dmx.core.DMXObject;
004import systems.dmx.core.model.AssociationDefinitionModel;
005import systems.dmx.core.model.AssociationModel;
006import systems.dmx.core.model.ChildTopicsModel;
007import systems.dmx.core.model.DMXObjectModel;
008import systems.dmx.core.model.IndexMode;
009import systems.dmx.core.model.RelatedTopicModel;
010import systems.dmx.core.model.RoleModel;
011import systems.dmx.core.model.SimpleValue;
012import systems.dmx.core.model.TopicModel;
013import systems.dmx.core.model.TopicDeletionModel;
014import systems.dmx.core.model.TopicReferenceModel;
015import systems.dmx.core.model.TypeModel;
016import systems.dmx.core.service.DMXEvent;
017import systems.dmx.core.service.Directive;
018import systems.dmx.core.service.Directives;
019import systems.dmx.core.util.JavaUtils;
020
021import org.codehaus.jettison.json.JSONObject;
022
023import java.util.ArrayList;
024import java.util.Iterator;
025import java.util.List;
026import java.util.logging.Logger;
027
028
029
030class DMXObjectModelImpl implements DMXObjectModel {
031
032    // ------------------------------------------------------------------------------------------------------- Constants
033
034    private static final String LABEL_CHILD_SEPARATOR = " ";
035    private static final String LABEL_TOPIC_SEPARATOR = ", ";
036
037    // ---------------------------------------------------------------------------------------------- Instance Variables
038
039    long id;                            // is -1 in models used for a create operation. ### FIXDOC
040                                        // is never -1 in models used for an update operation.
041    String uri;                         // is never null in models used for a create operation, may be empty. ### FIXDOC
042                                        // may be null in models used for an update operation.
043    String typeUri;                     // is never null in models used for a create operation. ### FIXDOC
044                                        // may be null in models used for an update operation.
045    SimpleValue value;                  // is never null in models used for a create operation, may be constructed
046                                        //                                                   on empty string. ### FIXDOC
047                                        // may be null in models used for an update operation.
048    ChildTopicsModelImpl childTopics;   // is never null, may be empty. ### FIXDOC
049
050    // ---
051
052    PersistenceLayer pl;
053    EventManager em;
054    ModelFactoryImpl mf;
055
056    Logger logger = Logger.getLogger(getClass().getName());
057
058    // ---------------------------------------------------------------------------------------------------- Constructors
059
060    DMXObjectModelImpl(long id, String uri, String typeUri, SimpleValue value, ChildTopicsModelImpl childTopics,
061                                                                                      PersistenceLayer pl) {
062        this.id          = id;
063        this.uri         = uri;
064        this.typeUri     = typeUri;
065        this.value       = value;
066        this.childTopics = childTopics != null ? childTopics : pl.mf.newChildTopicsModel();
067        //
068        this.pl          = pl;
069        this.em          = pl.em;
070        this.mf          = pl.mf;
071    }
072
073    DMXObjectModelImpl(DMXObjectModelImpl object) {
074        this(object.getId(), object.getUri(), object.getTypeUri(), object.getSimpleValue(),
075            object.getChildTopicsModel(), object.pl);
076    }
077
078    // -------------------------------------------------------------------------------------------------- Public Methods
079
080    // --- ID ---
081
082    @Override
083    public long getId() {
084        return id;
085    }
086
087    @Override
088    public void setId(long id) {
089        this.id = id;
090    }
091
092    // --- URI ---
093
094    @Override
095    public String getUri() {
096        return uri;
097    }
098
099    @Override
100    public void setUri(String uri) {
101        this.uri = uri;
102    }
103
104    // --- Type URI ---
105
106    @Override
107    public String getTypeUri() {
108        return typeUri;
109    }
110
111    @Override
112    public void setTypeUri(String typeUri) {
113        this.typeUri = typeUri;
114    }
115
116    // --- Simple Value ---
117
118    @Override
119    public SimpleValue getSimpleValue() {
120        return value;
121    }
122
123    // ---
124
125    @Override
126    public void setSimpleValue(String value) {
127        setSimpleValue(new SimpleValue(value));
128    }
129
130    @Override
131    public void setSimpleValue(int value) {
132        setSimpleValue(new SimpleValue(value));
133    }
134
135    @Override
136    public void setSimpleValue(long value) {
137        setSimpleValue(new SimpleValue(value));
138    }
139
140    @Override
141    public void setSimpleValue(boolean value) {
142        setSimpleValue(new SimpleValue(value));
143    }
144
145    @Override
146    public void setSimpleValue(SimpleValue value) {
147        this.value = value;
148    }
149
150    // --- Child Topics ---
151
152    @Override
153    public ChildTopicsModelImpl getChildTopicsModel() {
154        return childTopics;
155    }
156
157    @Override
158    public void setChildTopicsModel(ChildTopicsModel childTopics) {
159        this.childTopics = (ChildTopicsModelImpl) childTopics;
160    }
161
162    // --- misc ---
163
164    @Override
165    public void set(DMXObjectModel object) {
166        setId(object.getId());
167        setUri(object.getUri());
168        setTypeUri(object.getTypeUri());
169        setSimpleValue(object.getSimpleValue());
170        setChildTopicsModel(object.getChildTopicsModel());
171    }
172
173    // ---
174
175    @Override
176    public RoleModel createRoleModel(String roleTypeUri) {
177        throw new RuntimeException("Not implemented");  // only implemented in subclasses
178        // Note: technically this class is not abstract. It is instantiated by the ModelFactory.
179    }
180
181
182
183    // === Serialization ===
184
185    @Override
186    public JSONObject toJSON() {
187        try {
188            // Note: for models used for topic/association enrichment (e.g. timestamps, permissions)
189            // default values must be set in case they are not fully initialized.
190            setDefaults();
191            //
192            return new JSONObject()
193                .put("id", id)
194                .put("uri", uri)
195                .put("typeUri", typeUri)
196                .put("value", value.value())
197                .put("childs", childTopics.toJSON());
198        } catch (Exception e) {
199            throw new RuntimeException("Serialization failed", e);
200        }
201    }
202
203
204
205    // === Java API ===
206
207    @Override
208    public DMXObjectModel clone() {
209        try {
210            DMXObjectModel object = (DMXObjectModel) super.clone();
211            object.setChildTopicsModel(childTopics.clone());
212            return object;
213        } catch (Exception e) {
214            throw new RuntimeException("Cloning a DMXObjectModel failed", e);
215        }
216    }
217
218    @Override
219    public boolean equals(Object o) {
220        return ((DMXObjectModel) o).getId() == id;
221    }
222
223    @Override
224    public int hashCode() {
225        return ((Long) id).hashCode();
226    }
227
228    @Override
229    public String toString() {
230        try {
231            return getClass().getSimpleName() + " " + toJSON().toString(4);
232        } catch (Exception e) {
233            throw new RuntimeException("Prettyprinting failed", e);
234        }
235    }
236
237    // ----------------------------------------------------------------------------------------- Package Private Methods
238
239
240
241    // === Abstract Methods ===
242
243    // ### TODO: make this a real abstract class.
244    // Change the model factory in a way it never instantiates DMXObjectModels.
245
246    String className() {
247        throw new UnsupportedOperationException();
248    }
249
250    DMXObject instantiate() {
251        throw new UnsupportedOperationException();
252    }
253
254    DMXObjectModelImpl createModelWithChildTopics(ChildTopicsModel childTopics) {
255        throw new UnsupportedOperationException();
256    }
257
258    // ---
259
260    TypeModelImpl getType() {
261        throw new UnsupportedOperationException();
262    }
263
264    List<AssociationModelImpl> getAssociations() {
265        throw new UnsupportedOperationException();
266    }
267
268    // ---
269
270    RelatedTopicModelImpl getRelatedTopic(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
271                                                                                     String othersTopicTypeUri) {
272        throw new UnsupportedOperationException();
273    }
274
275    List<RelatedTopicModelImpl> getRelatedTopics(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
276                                                                                            String othersTopicTypeUri) {
277        throw new UnsupportedOperationException();
278    }
279
280    List<RelatedTopicModelImpl> getRelatedTopics(List assocTypeUris, String myRoleTypeUri, String othersRoleTypeUri,
281                                                                                           String othersTopicTypeUri) {
282        throw new UnsupportedOperationException();
283    }
284
285    // ---
286
287    void storeUri() {
288        throw new UnsupportedOperationException();
289    }
290
291    void storeTypeUri() {
292        throw new UnsupportedOperationException();
293    }
294
295    /**
296     * Stores and indexes the simple value of this object model.
297     * Determines the index key and index modes.
298     */
299    void storeSimpleValue() {
300        throw new UnsupportedOperationException();
301    }
302
303    /**
304     * Indexes the simple value of the given object model according to the given index mode.
305     * <p>
306     * Called to index existing topics/associations once an index mode has been added to a type definition.
307     */
308    void indexSimpleValue(IndexMode indexMode) {
309        throw new UnsupportedOperationException();
310    }
311
312    void storeProperty(String propUri, Object propValue, boolean addToIndex) {
313        throw new UnsupportedOperationException();
314    }
315
316    void removeProperty(String propUri) {
317        throw new UnsupportedOperationException();
318    }
319
320    // ---
321
322    void _delete() {
323        throw new UnsupportedOperationException();
324    }
325
326    // ---
327
328    /**
329     * @throws  AccessControlException
330     */
331    void checkReadAccess() {
332        throw new UnsupportedOperationException();
333    }
334
335    /**
336     * @throws  AccessControlException
337     */
338    void checkWriteAccess() {
339        throw new UnsupportedOperationException();
340    }
341
342    // ---
343
344    DMXEvent getPreUpdateEvent() {
345        throw new UnsupportedOperationException();
346    }
347
348    DMXEvent getPostUpdateEvent() {
349        throw new UnsupportedOperationException();
350    }
351
352    DMXEvent getPreDeleteEvent() {
353        throw new UnsupportedOperationException();
354    }
355
356    DMXEvent getPostDeleteEvent() {
357        throw new UnsupportedOperationException();
358    }
359
360    // ---
361
362    Directive getUpdateDirective() {
363        throw new UnsupportedOperationException();
364    }
365
366    Directive getDeleteDirective() {
367        throw new UnsupportedOperationException();
368    }
369
370
371
372    // === Core Internal Hooks ===
373
374    void preCreate() {
375    }
376
377    void postCreate() {
378    }
379
380    // ---
381
382    void preUpdate(DMXObjectModel updateModel) {
383    }
384
385    void postUpdate(DMXObjectModel updateModel, DMXObjectModel oldObject) {
386    }
387
388    // ---
389
390    void preDelete() {
391    }
392
393    void postDelete() {
394    }
395
396
397
398    // === Update (memory + DB) ===
399
400    final void updateChildTopics(ChildTopicsModel childTopics) {
401        update(createModelWithChildTopics(childTopics));
402    }
403
404    final void updateChildTopics(ChildTopicsModel updateModel, AssociationDefinitionModel assocDef) {
405        // ### TODO: think about: no directives are added, no events are fired, no core internal hooks are invoked.
406        // Possibly this is not wanted for facet updates. This method is solely used for facet updates.
407        // Compare to update() method.
408        new ValueIntegrator(pl).integrate(createModelWithChildTopics(updateModel), this, assocDef);
409    }
410
411    // ---
412
413    /**
414     * @param   updateModel    The data to update.
415     *              If the URI is <code>null</code> it is not updated.
416     *              If the type URI is <code>null</code> it is not updated.
417     *              If the simple value is <code>null</code> it is not updated.
418     */
419    final void update(DMXObjectModelImpl updateModel) {
420        try {
421            logger.info("Updating " + objectInfo() + " (typeUri=\"" + typeUri + "\")");
422            DMXObjectModel oldObject = clone();
423            em.fireEvent(getPreUpdateEvent(), instantiate(), updateModel);
424            //
425            preUpdate(updateModel);
426            //
427            _updateUri(updateModel.getUri());
428            _updateTypeUri(updateModel.getTypeUri());
429            new ValueIntegrator(pl).integrate(updateModel, this, null);   // TODO: handle return value
430            // TODO: rethink semantics of 1) events, 2) core internal hooks, and 3) directives in the face
431            // of DM5 update logic (= unification). Note that update() is not called recursively anmore.
432            //
433            postUpdate(updateModel, oldObject);
434            //
435            // Note: in case of a type topic the instantiate() call above creates a cloned model
436            // that doesn't reflect the update. Here we instantiate the now updated model.
437            DMXObject object = instantiate();
438            Directives.get().add(getUpdateDirective(), object);
439            em.fireEvent(getPostUpdateEvent(), object, updateModel, oldObject);
440        } catch (Exception e) {
441            throw new RuntimeException("Updating " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e);
442        }
443    }
444
445    // ---
446
447    final void updateUri(String uri) {
448        setUri(uri);            // update memory
449        storeUri();             // update DB, "abstract"
450    }
451
452    final void updateTypeUri(String typeUri) {
453        setTypeUri(typeUri);    // update memory
454        storeTypeUri();         // update DB, "abstract"
455    }
456
457    final void updateSimpleValue(SimpleValue value) {
458        if (value == null) {
459            throw new IllegalArgumentException("Tried to set a null SimpleValue (" + this + ")");
460        }
461        setSimpleValue(value);  // update memory
462        storeSimpleValue();     // update DB, "abstract"
463    }
464
465
466
467    // === Delete ===
468
469    /**
470     * Deletes this object's direct associations, and the object itself.
471     */
472    final void delete() {
473        try {
474            em.fireEvent(getPreDeleteEvent(), instantiate());
475            //
476            preDelete();
477            //
478            // delete direct associations
479            for (AssociationModelImpl assoc : getAssociations()) {
480                assoc.delete();
481            }
482            // delete object itself
483            logger.info("Deleting " + objectInfo() + " (typeUri=\"" + typeUri + "\")");
484            _delete();
485            //
486            postDelete();
487            //
488            Directives.get().add(getDeleteDirective(), this);
489            em.fireEvent(getPostDeleteEvent(), this);
490        } catch (IllegalStateException e) {
491            // Note: getAssociations() might throw IllegalStateException and is no problem.
492            // This can happen when this object is an association which is already deleted.
493            //
494            // Consider this particular situation: let A1 and A2 be associations of this object and let A2 point to A1.
495            // If A1 gets deleted first (the association set order is non-deterministic), A2 is implicitely deleted
496            // with it (because it is a direct association of A1 as well). Then when the loop comes to A2
497            // "IllegalStateException: Node[1327] has been deleted in this tx" is thrown because A2 has been deleted
498            // already. (The Node appearing in the exception is the middle node of A2.) If, on the other hand, A2
499            // gets deleted first no error would occur.
500            //
501            // This particular situation exists when e.g. a topicmap is deleted while one of its mapcontext
502            // associations is also a part of the topicmap itself. This originates e.g. when the user reveals
503            // a topicmap's mapcontext association and then deletes the topicmap.
504            //
505            // Compare to PersistenceLayer.deleteAssociation()
506            // TODO: introduce storage-vendor neutral DM exception.
507            //
508            if (e.getMessage().equals("Node[" + id + "] has been deleted in this tx")) {
509                logger.info("### Association " + id + " has already been deleted in this transaction. This can " +
510                    "happen while deleting a topic with associations A1 and A2 while A2 points to A1 (" + this + ")");
511            } else {
512                throw e;
513            }
514        } catch (Exception e) {
515            throw new RuntimeException("Deleting " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e);
516        }
517    }
518
519
520
521    // === Load Child Topics ===
522
523    // All 3 loadChildTopics() methods use this object itself as a cache.
524    // Child topics are fetched from DB only when not fetched already.
525    // Caching is done on a per assoc def basis.
526
527    /**
528     * Recursively loads this object's child topics which are not loaded already.
529     */
530    final DMXObjectModel loadChildTopics(boolean deep) {
531        for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) {
532            loadChildTopics(assocDef, deep);
533        }
534        return this;
535    }
536
537    /**
538     * Recursively loads this object's child topics for the given assoc def, provided they are not loaded already.
539     * If the child topics are loaded already nothing is performed.
540     * <p>
541     * Implemented on top of {@link #loadChildTopics(AssociationDefinitionModel, boolean)}.
542     * The assoc def is get from this object's type definition.
543     * As a consequence this method can <i>not</i> be used to load facet values.
544     * To load facet values use {@link #loadChildTopics(AssociationDefinitionModel, boolean)} and pass the facet type's
545     * assoc def.
546     */
547    final DMXObjectModel loadChildTopics(String assocDefUri, boolean deep) {
548        try {
549            return loadChildTopics(getAssocDef(assocDefUri), deep);
550        } catch (Exception e) {
551            throw new RuntimeException("Loading \"" + assocDefUri + "\" child topics of " + objectInfo() + " failed",
552                e);
553        }
554    }
555
556    /**
557     * Recursively loads this object's child topics for the given assoc def, provided they are not loaded already.
558     * If the child topics are loaded already nothing is performed.
559     * <p>
560     * Can be used to load facet values.
561     *
562     * @param   assocDef    the child topics according to this association definition are loaded.
563     *                      <p>
564     *                      Note: the association definition must not necessarily originate from this object's
565     *                      type definition. It may originate from a facet type as well.
566     */
567    final DMXObjectModel loadChildTopics(AssociationDefinitionModel assocDef, boolean deep) {
568        String assocDefUri = assocDef.getAssocDefUri();
569        if (!childTopics.has(assocDefUri)) {
570            logger.fine("### Loading \"" + assocDefUri + "\" child topics of " + objectInfo());
571            new ChildTopicsFetcher(pl).fetch(this, assocDef, deep);
572        }
573        return this;
574    }
575
576
577
578    // ===
579
580    /**
581     * Calculates the simple value that is to be indexed for this object.
582     *
583     * HTML tags are stripped from HTML values. Non-HTML values are returned directly. ### FIXDOC
584     */
585    SimpleValue getIndexValue() {
586        // TODO: rethink HTML indexing.
587        // DM5's value updater needs the exact index also for HTML values.
588        return value;
589        /* SimpleValue value = getSimpleValue();
590        if (getType().getDataTypeUri().equals("dmx.core.html")) {
591            return new SimpleValue(JavaUtils.stripHTML(value.toString()));
592        } else {
593            return value;
594        } */
595    }
596
597    boolean uriChange(String newUri, String compareUri) {
598        return newUri != null && !newUri.equals(compareUri);
599    }
600
601    boolean isSimple() {
602        // TODO: add isSimple() to type model
603        String dataTypeUri = getType().getDataTypeUri();
604        return dataTypeUri.equals("dmx.core.text")
605            || dataTypeUri.equals("dmx.core.html")
606            || dataTypeUri.equals("dmx.core.number")
607            || dataTypeUri.equals("dmx.core.boolean");
608    }
609
610
611
612    // ------------------------------------------------------------------------------------------------- Private Methods
613
614    // ### TODO: a principal copy exists in Neo4jStorage.
615    // Should this be package private? Should Neo4jStorage have access to the Core's impl package?
616    private void setDefaults() {
617        if (getUri() == null) {
618            setUri("");
619        }
620        if (getSimpleValue() == null) {
621            setSimpleValue("");
622        }
623    }
624
625
626
627    // === Update (memory + DB) ===
628
629    private void _updateUri(String newUri) {
630        if (uriChange(newUri, uri)) {                               // abort if no update is requested
631            logger.info("### Changing URI of " + objectInfo() + ": \"" + uri + "\" -> \"" + newUri + "\"");
632            updateUri(newUri);
633        }
634    }
635
636    private void _updateTypeUri(String newTypeUri) {
637        if (newTypeUri != null && !newTypeUri.equals(typeUri)) {    // abort if no update is requested
638            logger.info("### Changing type URI of " + objectInfo() + ": \"" + typeUri + "\" -> \"" + newTypeUri +
639                "\"");
640            updateTypeUri(newTypeUri);
641        }
642    }
643
644    final void _updateSimpleValue(SimpleValue newValue) {
645        if (newValue != null && !newValue.equals(value)) {          // abort if no update is requested
646            logger.info("### Changing simple value of " + objectInfo() + ": \"" + value + "\" -> \"" + newValue +
647                "\"");
648            updateSimpleValue(newValue);
649        }
650    }
651
652
653
654    // === Helper ===
655
656    // Note: doesn't work for facets
657    private AssociationDefinitionModel getAssocDef(String assocDefUri) {
658        return getType().getAssocDef(assocDefUri);
659    }
660
661    // ### TODO: drop it?
662    String objectInfo() {
663        return className() + " " + id;
664    }
665}