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