001package systems.dmx.core.impl;
002
003import systems.dmx.core.model.AssociationDefinitionModel;
004import systems.dmx.core.model.ChildTopicsModel;
005import systems.dmx.core.model.RelatedTopicModel;
006import systems.dmx.core.model.SimpleValue;
007import systems.dmx.core.model.TopicModel;
008import systems.dmx.core.service.ModelFactory;
009import systems.dmx.core.util.DMXUtils;
010
011import org.codehaus.jettison.json.JSONObject;
012
013import java.util.ArrayList;
014import java.util.Iterator;
015import java.util.List;
016import java.util.Map;
017import java.util.logging.Logger;
018
019
020
021class ChildTopicsModelImpl implements ChildTopicsModel {
022
023    // ---------------------------------------------------------------------------------------------- Instance Variables
024
025    /**
026     * Internal representation.
027     * Key: assoc def URI (String), value: RelatedTopicModel or List<RelatedTopicModel>
028     */
029    private Map<String, Object> childTopics;
030    // Note: it must be List<RelatedTopicModel>, not Set<RelatedTopicModel>.
031    // There may be several TopicModels with the same ID. That occurrs if the webclient user adds several new topics
032    // at once (by the means of an "Add" button). In this case the ID is -1. TopicModel equality is defined solely as
033    // ID equality (see DMXObjectModel.equals()).
034
035    private ModelFactory mf;
036
037    private Logger logger = Logger.getLogger(getClass().getName());
038
039    // ---------------------------------------------------------------------------------------------------- Constructors
040
041    ChildTopicsModelImpl(Map<String, Object> childTopics, ModelFactory mf) {
042        this.childTopics = childTopics;
043        this.mf = mf;
044    }
045
046    ChildTopicsModelImpl(ChildTopicsModelImpl childTopics) {
047        this(childTopics.childTopics, childTopics.mf);
048    }
049
050    // -------------------------------------------------------------------------------------------------- Public Methods
051
052
053
054    // === Accessors ===
055
056    @Override
057    public final RelatedTopicModelImpl getTopic(String assocDefUri) {
058        RelatedTopicModelImpl topic = getTopicOrNull(assocDefUri);
059        // error check
060        if (topic == null) {
061            throw new RuntimeException("Assoc Def URI \"" + assocDefUri + "\" not found in " + childTopics.keySet());
062        }
063        //
064        return topic;
065    }
066
067    @Override
068    public final RelatedTopicModelImpl getTopicOrNull(String assocDefUri) {
069        try {
070            return (RelatedTopicModelImpl) childTopics.get(assocDefUri);
071        } catch (ClassCastException e) {
072            throwInvalidSingleAccess(assocDefUri, e);
073            return null;    // never reached
074        }
075    }
076
077    // ---
078
079    @Override
080    public final List<RelatedTopicModelImpl> getTopics(String assocDefUri) {
081        List<RelatedTopicModelImpl> topics = getTopicsOrNull(assocDefUri);
082        // error check
083        if (topics == null) {
084            throw new RuntimeException("Assoc Def URI \"" + assocDefUri + "\" not found in " + childTopics.keySet());
085        }
086        //
087        return topics;
088    }
089
090    @Override
091    public final List<RelatedTopicModelImpl> getTopicsOrNull(String assocDefUri) {
092        try {
093            return (List<RelatedTopicModelImpl>) childTopics.get(assocDefUri);
094        } catch (ClassCastException e) {
095            throwInvalidMultiAccess(assocDefUri, e);
096            return null;    // never reached
097        }
098    }
099
100    // ---
101
102    /**
103     * Accesses a child generically, regardless of single-valued or multiple-valued.
104     * Returns null if there is no such child.
105     *
106     * @return  A RelatedTopicModel or List<RelatedTopicModel>, or null if there is no such child.
107     */
108    @Override
109    public final Object get(String assocDefUri) {
110        return childTopics.get(assocDefUri);
111    }
112
113
114
115    // === Convenience Accessors ===
116
117    /**
118     * Convenience accessor for the *simple* value of a single-valued child.
119     * Throws if the child doesn't exist.
120     */
121    @Override
122    public final String getString(String assocDefUri) {
123        return getTopic(assocDefUri).getSimpleValue().toString();
124    }
125
126    /**
127     * Convenience accessor for the *simple* value of a single-valued child.
128     * Returns a default value if the child doesn't exist.
129     */
130    @Override
131    public final String getString(String assocDefUri, String defaultValue) {
132        TopicModel topic = getTopicOrNull(assocDefUri);
133        return topic != null ? topic.getSimpleValue().toString() : defaultValue;
134    }
135
136    // ---
137
138    /**
139     * Convenience accessor for the *simple* value of a single-valued child.
140     * Throws if the child doesn't exist.
141     */
142    @Override
143    public final int getInt(String assocDefUri) {
144        return getTopic(assocDefUri).getSimpleValue().intValue();
145    }
146
147    /**
148     * Convenience accessor for the *simple* value of a single-valued child.
149     * Returns a default value if the child doesn't exist.
150     */
151    @Override
152    public final int getInt(String assocDefUri, int defaultValue) {
153        TopicModel topic = getTopicOrNull(assocDefUri);
154        return topic != null ? topic.getSimpleValue().intValue() : defaultValue;
155    }
156
157    // ---
158
159    /**
160     * Convenience accessor for the *simple* value of a single-valued child.
161     * Throws if the child doesn't exist.
162     */
163    @Override
164    public final long getLong(String assocDefUri) {
165        return getTopic(assocDefUri).getSimpleValue().longValue();
166    }
167
168    /**
169     * Convenience accessor for the *simple* value of a single-valued child.
170     * Returns a default value if the child doesn't exist.
171     */
172    @Override
173    public final long getLong(String assocDefUri, long defaultValue) {
174        TopicModel topic = getTopicOrNull(assocDefUri);
175        return topic != null ? topic.getSimpleValue().longValue() : defaultValue;
176    }
177
178    // ---
179
180    /**
181     * Convenience accessor for the *simple* value of a single-valued child.
182     * Throws if the child doesn't exist.
183     */
184    @Override
185    public final double getDouble(String assocDefUri) {
186        return getTopic(assocDefUri).getSimpleValue().doubleValue();
187    }
188
189    /**
190     * Convenience accessor for the *simple* value of a single-valued child.
191     * Returns a default value if the child doesn't exist.
192     */
193    @Override
194    public final double getDouble(String assocDefUri, double defaultValue) {
195        TopicModel topic = getTopicOrNull(assocDefUri);
196        return topic != null ? topic.getSimpleValue().doubleValue() : defaultValue;
197    }
198
199    // ---
200
201    /**
202     * Convenience accessor for the *simple* value of a single-valued child.
203     * Throws if the child doesn't exist.
204     */
205    @Override
206    public final boolean getBoolean(String assocDefUri) {
207        return getTopic(assocDefUri).getSimpleValue().booleanValue();
208    }
209
210    /**
211     * Convenience accessor for the *simple* value of a single-valued child.
212     * Returns a default value if the child doesn't exist.
213     */
214    @Override
215    public final boolean getBoolean(String assocDefUri, boolean defaultValue) {
216        TopicModel topic = getTopicOrNull(assocDefUri);
217        return topic != null ? topic.getSimpleValue().booleanValue() : defaultValue;
218    }
219
220    // ---
221
222    /**
223     * Convenience accessor for the *simple* value of a single-valued child.
224     * Throws if the child doesn't exist.
225     */
226    @Override
227    public final Object getObject(String assocDefUri) {
228        return getTopic(assocDefUri).getSimpleValue().value();
229    }
230
231    /**
232     * Convenience accessor for the *simple* value of a single-valued child.
233     * Returns a default value if the child doesn't exist.
234     */
235    @Override
236    public final Object getObject(String assocDefUri, Object defaultValue) {
237        TopicModel topic = getTopicOrNull(assocDefUri);
238        return topic != null ? topic.getSimpleValue().value() : defaultValue;
239    }
240
241    // ---
242
243    /**
244     * Convenience accessor for the *composite* value of a single-valued child.
245     * Throws if the child doesn't exist.
246     */
247    @Override
248    public final ChildTopicsModel getChildTopicsModel(String assocDefUri) {
249        return getTopic(assocDefUri).getChildTopicsModel();
250    }
251
252    /**
253     * Convenience accessor for the *composite* value of a single-valued child.
254     * Returns a default value if the child doesn't exist.
255     */
256    @Override
257    public final ChildTopicsModel getChildTopicsModel(String assocDefUri, ChildTopicsModel defaultValue) {
258        RelatedTopicModel topic = getTopicOrNull(assocDefUri);
259        return topic != null ? topic.getChildTopicsModel() : defaultValue;
260    }
261
262    // Note: there are no convenience accessors for a multiple-valued child.
263
264
265
266    // === Manipulators ===
267
268    // --- Single-valued Childs ---
269
270    /**
271     * Puts a value in a single-valued child.
272     * An existing value is overwritten.
273     */
274    @Override
275    public final ChildTopicsModel put(String assocDefUri, RelatedTopicModel value) {
276        try {
277            // check argument
278            if (value == null) {
279                throw new IllegalArgumentException("Tried to put null in a ChildTopicsModel");
280            }
281            //
282            childTopics.put(assocDefUri, value);
283            return this;
284        } catch (Exception e) {
285            throw new RuntimeException("Putting a value in a ChildTopicsModel failed (assocDefUri=\"" +
286                assocDefUri + "\", value=" + value + ")", e);
287        }
288    }
289
290    @Override
291    public final ChildTopicsModel put(String assocDefUri, TopicModel value) {
292        return put(assocDefUri, mf.newRelatedTopicModel(value));
293    }
294
295    // ---
296
297    /**
298     * Convenience method to put a *simple* value in a single-valued child.
299     * An existing value is overwritten.
300     *
301     * @param   value   a String, Integer, Long, Double, or a Boolean.
302     *
303     * @return  this ChildTopicsModel.
304     */
305    @Override
306    public final ChildTopicsModel put(String assocDefUri, Object value) {
307        try {
308            return put(assocDefUri, mf.newTopicModel(mf.childTypeUri(assocDefUri), new SimpleValue(value)));
309        } catch (Exception e) {
310            throw new RuntimeException("Putting a value in a ChildTopicsModel failed (assocDefUri=\"" +
311                assocDefUri + "\", value=" + value + ")", e);
312        }
313    }
314
315    /**
316     * Convenience method to put a *composite* value in a single-valued child.
317     * An existing value is overwritten.
318     *
319     * @return  this ChildTopicsModel.
320     */
321    @Override
322    public final ChildTopicsModel put(String assocDefUri, ChildTopicsModel value) {
323        return put(assocDefUri, mf.newTopicModel(mf.childTypeUri(assocDefUri), value));
324    }
325
326    // ---
327
328    /**
329     * Puts a by-ID topic reference in a single-valued child.
330     * An existing reference is overwritten.
331     */
332    @Override
333    public final ChildTopicsModel putRef(String assocDefUri, long refTopicId) {
334        put(assocDefUri, mf.newTopicReferenceModel(refTopicId));
335        return this;
336    }
337
338    /**
339     * Puts a by-URI topic reference in a single-valued child.
340     * An existing reference is overwritten.
341     */
342    @Override
343    public final ChildTopicsModel putRef(String assocDefUri, String refTopicUri) {
344        put(assocDefUri, mf.newTopicReferenceModel(refTopicUri));
345        return this;
346    }
347
348    // ---
349
350    /**
351     * Puts a by-ID topic deletion reference to a single-valued child.
352     * An existing value is overwritten.
353     */
354    @Override
355    public final ChildTopicsModel putDeletionRef(String assocDefUri, long refTopicId) {
356        put(assocDefUri, mf.newTopicDeletionModel(refTopicId));
357        return this;
358    }
359
360    /**
361     * Puts a by-URI topic deletion reference to a single-valued child.
362     * An existing value is overwritten.
363     */
364    @Override
365    public final ChildTopicsModel putDeletionRef(String assocDefUri, String refTopicUri) {
366        put(assocDefUri, mf.newTopicDeletionModel(refTopicUri));
367        return this;
368    }
369
370    // ---
371
372    /**
373     * Removes a single-valued child.
374     */
375    @Override
376    public final ChildTopicsModel remove(String assocDefUri) {
377        childTopics.remove(assocDefUri);    // ### TODO: throw if not in map?
378        return this;
379    }
380
381    // --- Multiple-valued Childs ---
382
383    /**
384     * Adds a value to a multiple-valued child.
385     */
386    @Override
387    public final ChildTopicsModel add(String assocDefUri, RelatedTopicModel value) {
388        List<RelatedTopicModelImpl> topics = getTopicsOrNull(assocDefUri);
389        // Note: topics just created have no child topics yet
390        if (topics == null) {
391            topics = new ArrayList();
392            childTopics.put(assocDefUri, topics);
393        }
394        //
395        topics.add((RelatedTopicModelImpl) value);
396        //
397        return this;
398    }
399
400    @Override
401    public final ChildTopicsModel add(String assocDefUri, TopicModel value) {
402        return add(assocDefUri, mf.newRelatedTopicModel(value));
403    }
404
405    /**
406     * Sets the values of a multiple-valued child.
407     * Existing values are overwritten.
408     */
409    @Override
410    public final ChildTopicsModel put(String assocDefUri, List<RelatedTopicModel> values) {
411        childTopics.put(assocDefUri, values);
412        return this;
413    }
414
415    /**
416     * Removes a value from a multiple-valued child.
417     */
418    @Override
419    public final ChildTopicsModel remove(String assocDefUri, TopicModel value) {
420        List<RelatedTopicModelImpl> topics = getTopicsOrNull(assocDefUri);
421        if (topics != null) {
422            topics.remove(value);
423        }
424        return this;
425    }
426
427    // ---
428
429    /**
430     * Adds a by-ID topic reference to a multiple-valued child.
431     */
432    @Override
433    public final ChildTopicsModel addRef(String assocDefUri, long refTopicId) {
434        add(assocDefUri, mf.newTopicReferenceModel(refTopicId));
435        return this;
436    }
437
438    /**
439     * Adds a by-URI topic reference to a multiple-valued child.
440     */
441    @Override
442    public final ChildTopicsModel addRef(String assocDefUri, String refTopicUri) {
443        add(assocDefUri, mf.newTopicReferenceModel(refTopicUri));
444        return this;
445    }
446
447    // ---
448
449    /**
450     * Adds a by-ID topic deletion reference to a multiple-valued child.
451     */
452    @Override
453    public final ChildTopicsModel addDeletionRef(String assocDefUri, long refTopicId) {
454        add(assocDefUri, mf.newTopicDeletionModel(refTopicId));
455        return this;
456    }
457
458    /**
459     * Adds a by-URI topic deletion reference to a multiple-valued child.
460     */
461    @Override
462    public final ChildTopicsModel addDeletionRef(String assocDefUri, String refTopicUri) {
463        add(assocDefUri, mf.newTopicDeletionModel(refTopicUri));
464        return this;
465    }
466
467
468
469    // === Iterable Implementation ===
470
471    /**
472     * Returns an interator which iterates this ChildTopicsModel's assoc def URIs.
473     */
474    @Override
475    public final Iterator<String> iterator() {
476        return childTopics.keySet().iterator();
477    }
478
479
480
481    // ===
482
483    @Override
484    public final JSONObject toJSON() {
485        try {
486            JSONObject json = new JSONObject();
487            for (String assocDefUri : this) {
488                Object value = get(assocDefUri);
489                if (value instanceof RelatedTopicModel) {
490                    json.put(assocDefUri, ((RelatedTopicModel) value).toJSON());
491                } else if (value instanceof List) {
492                    json.put(assocDefUri, DMXUtils.toJSONArray((List<RelatedTopicModel>) value));
493                } else {
494                    throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value);
495                }
496            }
497            return json;
498        } catch (Exception e) {
499            throw new RuntimeException("Serialization failed", e);
500        }
501    }
502
503
504
505    // ****************
506    // *** Java API ***
507    // ****************
508
509
510
511    @Override
512    public final ChildTopicsModel clone() {
513        ChildTopicsModel clone = mf.newChildTopicsModel();
514        for (String assocDefUri : this) {
515            Object value = get(assocDefUri);
516            if (value instanceof RelatedTopicModel) {
517                RelatedTopicModel model = (RelatedTopicModel) value;
518                clone.put(assocDefUri, model.clone());
519            } else if (value instanceof List) {
520                for (RelatedTopicModel model : (List<RelatedTopicModel>) value) {
521                    clone.add(assocDefUri, model.clone());
522                }
523            } else {
524                throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value);
525            }
526        }
527        return clone;
528    }
529
530    // ----------------------------------------------------------------------------------------- Package Private Methods
531
532
533
534    // === Mmemory Access ===
535
536    // --- Read ---
537
538    /**
539     * For multiple-valued childs: looks in the attached object cache for a child topic by ID. ### FIXDOC
540     */
541    RelatedTopicModelImpl findChildTopicById(long childTopicId, AssociationDefinitionModel assocDef) {
542        List<RelatedTopicModelImpl> childTopics = getTopicsOrNull(assocDef.getAssocDefUri());
543        if (childTopics != null) {
544            for (RelatedTopicModelImpl childTopic : childTopics) {
545                if (childTopic.getId() == childTopicId) {
546                    return childTopic;
547                }
548            }
549        }
550        return null;
551    }
552
553    /**
554     * For multiple-valued childs: looks in the attached object cache for the child topic the given reference refers to.
555     * ### FIXDOC
556     *
557     * @param   assocDef    the child topics according to this association definition are considered.
558     */
559    RelatedTopicModelImpl findChildTopicByRef(TopicReferenceModelImpl topicRef, AssociationDefinitionModel assocDef) {
560        List<? extends RelatedTopicModel> childTopics = getTopicsOrNull(assocDef.getAssocDefUri());
561        if (childTopics != null) {
562            return topicRef.findReferencedTopic(childTopics);
563        }
564        return null;
565    }
566
567    // ---
568
569    /**
570     * Checks if a child is contained in this ChildTopicsModel.
571     */
572    boolean has(String assocDefUri) {
573        return childTopics.containsKey(assocDefUri);
574    }
575
576    /**
577     * Returns the number of childs contained in this ChildTopicsModel.
578     * Multiple-valued childs count as one.
579     */
580    int size() {
581        return childTopics.size();
582    }
583
584    // --- Write Helper ---
585
586    /**
587     * For single-valued childs
588     */
589    void putInChildTopics(RelatedTopicModel childTopic, AssociationDefinitionModel assocDef) {
590        put(assocDef.getAssocDefUri(), childTopic);
591    }
592
593    /**
594     * For single-valued childs
595     */
596    void removeChildTopic(AssociationDefinitionModel assocDef) {
597        remove(assocDef.getAssocDefUri());
598    }
599
600    /**
601     * For multiple-valued childs
602     */
603    void addToChildTopics(RelatedTopicModel childTopic, AssociationDefinitionModel assocDef) {
604        add(assocDef.getAssocDefUri(), childTopic);
605    }
606
607    /**
608     * For multiple-valued childs
609     */
610    void removeFromChildTopics(RelatedTopicModel childTopic, AssociationDefinitionModel assocDef) {
611        remove(assocDef.getAssocDefUri(), childTopic);
612    }
613
614
615
616    // ------------------------------------------------------------------------------------------------- Private Methods
617
618    private void throwInvalidSingleAccess(String assocDefUri, ClassCastException e) {
619        if (e.getMessage().startsWith("java.util.ArrayList cannot be cast to")) {
620            throw new RuntimeException("\"" + assocDefUri + "\" is accessed as single but is defined as multi", e);
621        } else {
622            throw new RuntimeException("Accessing \"" + assocDefUri + "\" failed", e);
623        }
624    }
625
626    private void throwInvalidMultiAccess(String assocDefUri, ClassCastException e) {
627        if (e.getMessage().endsWith("cannot be cast to java.util.List")) {
628            throw new RuntimeException("\"" + assocDefUri + "\" is accessed as multi but is defined as single", e);
629        } else {
630            throw new RuntimeException("Accessing \"" + assocDefUri + " failed", e);
631        }
632    }
633}