001    package de.deepamehta.core.model;
002    
003    import de.deepamehta.core.util.DeepaMehtaUtils;
004    
005    import org.codehaus.jettison.json.JSONArray;
006    import org.codehaus.jettison.json.JSONException;
007    import org.codehaus.jettison.json.JSONObject;
008    
009    import java.util.ArrayList;
010    import java.util.HashMap;
011    import java.util.Iterator;
012    import java.util.List;
013    import java.util.Map;
014    import 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     */
024    public 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.objectsToJSON((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    }