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.JSONObject;
007    
008    import java.util.ArrayList;
009    import java.util.HashMap;
010    import java.util.Iterator;
011    import java.util.List;
012    import java.util.Map;
013    import java.util.logging.Logger;
014    
015    
016    
017    /**
018     * A recursive composite of key/value pairs. ### FIXDOC
019     * <p>
020     * Keys are strings, values are non-null atomic (string, int, long, double, boolean)
021     * or again a <code>CompositeValueModel</code>. ### FIXDOC
022     */
023    public class CompositeValueModel implements Iterable<String> {
024    
025        // ------------------------------------------------------------------------------------------------------- Constants
026    
027        private static final String REF_ID_PREFIX = "ref_id:";
028        private static final String REF_URI_PREFIX = "ref_uri:";
029        private static final String DEL_PREFIX = "del_id:";
030    
031        // ---------------------------------------------------------------------------------------------- Instance Variables
032    
033        /**
034         * Internal representation.
035         * Key: String, value: TopicModel or List<TopicModel>
036         */
037        private Map<String, Object> childTopics = new HashMap();
038        // Note: it must be List<TopicModel>, not Set<TopicModel> (like before).
039        // There may be several TopicModels with the same ID. That occurrs if the webclient user adds several new topics
040        // at once (by the means of an "Add" button). In this case the ID is -1. TopicModel equality is defined solely as
041        // ID equality (see DeepaMehtaObjectModel.equals()).
042    
043        private Logger logger = Logger.getLogger(getClass().getName());
044    
045        // ---------------------------------------------------------------------------------------------------- Constructors
046    
047        public CompositeValueModel() {
048        }
049    
050        public CompositeValueModel(JSONObject values) {
051            try {
052                Iterator<String> i = values.keys();
053                while (i.hasNext()) {
054                    String childTypeUri = i.next();
055                    Object value = values.get(childTypeUri);
056                    if (value instanceof JSONArray) {
057                        JSONArray valueArray = (JSONArray) value;
058                        for (int j = 0; j < valueArray.length(); j++) {
059                            add(childTypeUri, createTopicModel(childTypeUri, valueArray.get(j)));
060                        }
061                    } else {
062                        put(childTypeUri, createTopicModel(childTypeUri, value));
063                    }
064                }
065            } catch (Exception e) {
066                throw new RuntimeException("Parsing CompositeValueModel failed (JSONObject=" + values + ")", e);
067            }
068        }
069    
070        // -------------------------------------------------------------------------------------------------- Public Methods
071    
072    
073    
074        // === Accessors ===
075    
076        /**
077         * Accesses a single-valued child.
078         * Throws if there is no such child.
079         */
080        public TopicModel getTopic(String childTypeUri) {
081            TopicModel topic = (TopicModel) get(childTypeUri);
082            // error check
083            if (topic == null) {
084                throw new RuntimeException("Invalid access to CompositeValueModel entry \"" + childTypeUri +
085                    "\": no such entry in\n" + this);
086            }
087            //
088            return topic;
089        }
090    
091        /**
092         * Accesses a single-valued child.
093         * Returns a default value if there is no such child.
094         */
095        public TopicModel getTopic(String childTypeUri, TopicModel defaultValue) {
096            TopicModel topic = (TopicModel) get(childTypeUri);
097            return topic != null ? topic : defaultValue;
098        }
099    
100        // ---
101    
102        /**
103         * Accesses a multiple-valued child.
104         * Throws if there is no such child.
105         */
106        public List<TopicModel> getTopics(String childTypeUri) {
107            try {
108                List<TopicModel> topics = (List<TopicModel>) get(childTypeUri);
109                // error check
110                if (topics == null) {
111                    throw new RuntimeException("Invalid access to CompositeValueModel entry \"" + childTypeUri +
112                        "\": no such entry in\n" + this);
113                }
114                //
115                return topics;
116            } catch (ClassCastException e) {
117                throwInvalidAccess(childTypeUri, e);
118                return null;    // never reached
119            }
120        }
121    
122        /**
123         * Accesses a multiple-valued child.
124         * Returns a default value if there is no such child.
125         */
126        public List<TopicModel> getTopics(String childTypeUri, List<TopicModel> defaultValue) {
127            try {
128                List<TopicModel> topics = (List<TopicModel>) get(childTypeUri);
129                return topics != null ? topics : defaultValue;
130            } catch (ClassCastException e) {
131                throwInvalidAccess(childTypeUri, e);
132                return null;    // never reached
133            }
134        }
135    
136        // ---
137    
138        /**
139         * Accesses a child generically, regardless of single-valued or multiple-valued.
140         * Returns null if there is no such child.
141         *
142         * @return  A TopicModel or List<TopicModel>, or null if there is no such child.
143         */
144        public Object get(String childTypeUri) {
145            return childTopics.get(childTypeUri);
146        }
147    
148        /**
149         * Checks if a child is directly contained in this composite value.
150         * ### TODO: could be renamed to "contains()"
151         */
152        public boolean has(String childTypeUri) {
153            return childTopics.containsKey(childTypeUri);
154        }
155    
156        /**
157         * Returns the number of childs directly contained in this composite value.
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 CompositeValueModel getCompositeValueModel(String childTypeUri) {
287            return getTopic(childTypeUri).getCompositeValueModel();
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 CompositeValueModel getCompositeValueModel(String childTypeUri, CompositeValueModel defaultValue) {
295            TopicModel topic = getTopic(childTypeUri, null);
296            return topic != null ? topic.getCompositeValueModel() : defaultValue;
297        }
298    
299        // Note: there are no convenience accessors for a multiple-valued child.
300    
301    
302    
303        // === Manipulators ===
304    
305        /**
306         * Puts a value in a single-valued child.
307         * An existing value is overwritten.
308         */
309        public CompositeValueModel put(String childTypeUri, TopicModel value) {
310            try {
311                // check argument
312                if (value == null) {
313                    throw new IllegalArgumentException("Tried to put null in a CompositeValueModel");
314                }
315                //
316                childTopics.put(childTypeUri, value);
317                return this;
318            } catch (Exception e) {
319                throw new RuntimeException("Putting a value in a CompositeValueModel failed (childTypeUri=\"" +
320                    childTypeUri + "\", value=" + value + ", composite=" + this + ")", e);
321            }
322        }
323    
324        // ---
325    
326        /**
327         * Convenience method to put a *simple* value in a single-valued child.
328         * An existing value is overwritten.
329         *
330         * @param   value   a String, Integer, Long, Double, or a Boolean.
331         *
332         * @return  this CompositeValueModel.
333         */
334        public CompositeValueModel put(String childTypeUri, Object value) {
335            try {
336                childTopics.put(childTypeUri, new TopicModel(childTypeUri, new SimpleValue(value)));
337                return this;
338            } catch (Exception e) {
339                throw new RuntimeException("Putting a value in a CompositeValueModel failed (childTypeUri=\"" +
340                    childTypeUri + "\", value=" + value + ", composite=" + this + ")", e);
341            }
342        }
343    
344        /**
345         * Convenience method to put a *composite* value in a single-valued child.
346         * An existing value is overwritten.
347         *
348         * @return  this CompositeValueModel.
349         */
350        public CompositeValueModel put(String childTypeUri, CompositeValueModel value) {
351            childTopics.put(childTypeUri, new TopicModel(childTypeUri, value));
352            return this;
353        }
354    
355        // ---
356    
357        /**
358         * Puts a by-ID topic reference for a single-valued child.
359         * An existing reference is overwritten.
360         * <p>
361         * Used to maintain the assigment of an *aggregated* child.
362         * Not applicable for a *compositioned* child.
363         */
364        public CompositeValueModel putRef(String childTypeUri, long refTopicId) {
365            put(childTypeUri, new TopicReferenceModel(refTopicId));
366            return this;
367        }
368    
369        /**
370         * Puts a by-URI topic reference for a single-valued child.
371         * An existing reference is overwritten.
372         * <p>
373         * Used to maintain the assigment of an *aggregated* child.
374         * Not applicable for a *compositioned* child.
375         */
376        public CompositeValueModel putRef(String childTypeUri, String refTopicUri) {
377            put(childTypeUri, new TopicReferenceModel(refTopicUri));
378            return this;
379        }
380    
381        // ---
382    
383        /**
384         * Adds a value to a multiple-valued child.
385         */
386        public CompositeValueModel add(String childTypeUri, TopicModel value) {
387            List<TopicModel> topics = getTopics(childTypeUri, null);     // defaultValue=null
388            // Note: topics just created have no child topics yet
389            if (topics == null) {
390                topics = new ArrayList();
391                childTopics.put(childTypeUri, topics);
392            }
393            //
394            topics.add(value);
395            //
396            return this;
397        }
398    
399        /**
400         * Sets the values of a multiple-valued child.
401         * Existing values are overwritten.
402         */
403        public CompositeValueModel put(String childTypeUri, List<TopicModel> values) {
404            childTopics.put(childTypeUri, values);
405            return this;
406        }
407    
408        /**
409         * Removes a value from a multiple-valued child.
410         */
411        public CompositeValueModel remove(String childTypeUri, TopicModel value) {
412            List<TopicModel> topics = getTopics(childTypeUri, null);     // defaultValue=null
413            if (topics != null) {
414                topics.remove(value);
415            }
416            return this;
417        }
418    
419        // ---
420    
421        /**
422         * Adds a by-ID topic reference to a multiple-valued child.
423         *
424         * Used to maintain the assigments of *aggregated* childs.
425         * Not applicable for *compositioned* childs.
426         */
427        public CompositeValueModel addRef(String childTypeUri, long refTopicId) {
428            add(childTypeUri, new TopicReferenceModel(refTopicId));
429            return this;
430        }
431    
432        /**
433         * Adds a by-URI topic reference to a multiple-valued child.
434         *
435         * Used to maintain the assigments of *aggregated* childs.
436         * Not applicable for *compositioned* childs.
437         */
438        public CompositeValueModel addRef(String childTypeUri, String refTopicUri) {
439            add(childTypeUri, new TopicReferenceModel(refTopicUri));
440            return this;
441        }
442    
443        // ---
444    
445        /**
446         * Adds a by-ID topic deletion reference to a multiple-valued child.
447         *
448         * Used to maintain the assigments of *aggregated* childs.
449         * Not applicable for *compositioned* childs.
450         */
451        public CompositeValueModel addDeletionRef(String childTypeUri, long refTopicId) {
452            add(childTypeUri, new TopicDeletionModel(refTopicId));
453            return this;
454        }
455    
456    
457    
458        // === Iterable Implementation ===
459    
460        /**
461         * Returns an interator which iterates this composite value's child type URIs.
462         */
463        @Override
464        public Iterator<String> iterator() {
465            return childTopics.keySet().iterator();
466        }
467    
468    
469    
470        // ===
471    
472        public JSONObject toJSON() {
473            try {
474                JSONObject json = new JSONObject();
475                for (String childTypeUri : this) {
476                    Object value = get(childTypeUri);
477                    if (value instanceof TopicModel) {
478                        json.put(childTypeUri, ((TopicModel) value).toJSON());
479                    } else if (value instanceof List) {
480                        json.put(childTypeUri, DeepaMehtaUtils.objectsToJSON((List<TopicModel>) value));
481                    } else {
482                        throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value);
483                    }
484                }
485                return json;
486            } catch (Exception e) {
487                throw new RuntimeException("Serialization of a CompositeValueModel failed (" + this + ")", e);
488            }
489        }
490    
491    
492    
493        // ****************
494        // *** Java API ***
495        // ****************
496    
497    
498    
499        @Override
500        public CompositeValueModel clone() {
501            CompositeValueModel clone = new CompositeValueModel();
502            for (String childTypeUri : this) {
503                Object value = get(childTypeUri);
504                if (value instanceof TopicModel) {
505                    TopicModel model = (TopicModel) value;
506                    clone.put(childTypeUri, model.clone());
507                } else if (value instanceof List) {
508                    for (TopicModel model : (List<TopicModel>) value) {
509                        clone.add(childTypeUri, model.clone());
510                    }
511                } else {
512                    throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value);
513                }
514            }
515            return clone;
516        }
517    
518        @Override
519        public String toString() {
520            return childTopics.toString();
521        }
522    
523    
524    
525        // ------------------------------------------------------------------------------------------------- Private Methods
526    
527        /**
528         * Creates a topic model from a JSON value.
529         *
530         * Both topic serialization formats are supported:
531         * 1) canonic format -- contains entire topic models.
532         * 2) compact format -- contains the topic value only (simple or composite).
533         */
534        private TopicModel createTopicModel(String childTypeUri, Object value) {
535            if (value instanceof JSONObject) {
536                JSONObject val = (JSONObject) value;
537                // we detect the canonic format by checking for a mandatory topic property
538                // ### TODO: "type_uri" should not be regarded mandatory. It would simplify update requests.
539                // ### Can we use another heuristic for detection: "value" exists OR "composite" exists?
540                if (val.has("type_uri")) {
541                    // canonic format
542                    return new TopicModel(val);
543                } else {
544                    // compact format (composite topic)
545                    return new TopicModel(childTypeUri, new CompositeValueModel(val));
546                }
547            } else {
548                // compact format (simple topic or topic reference)
549                if (value instanceof String) {
550                    String val = (String) value;
551                    if (val.startsWith(REF_ID_PREFIX)) {
552                        return new TopicReferenceModel(refTopicId(val));    // topic reference by-ID
553                    } else if (val.startsWith(REF_URI_PREFIX)) {
554                        return new TopicReferenceModel(refTopicUri(val));   // topic reference by-URI
555                    } else if (val.startsWith(DEL_PREFIX)) {
556                        return new TopicDeletionModel(delTopicId(val));     // topic deletion reference
557                    }
558                }
559                // compact format (simple topic)
560                return new TopicModel(childTypeUri, new SimpleValue(value));
561            }
562        }
563    
564        // ---
565    
566        private long refTopicId(String val) {
567            return Long.parseLong(val.substring(REF_ID_PREFIX.length()));
568        }
569    
570        private String refTopicUri(String val) {
571            return val.substring(REF_URI_PREFIX.length());
572        }
573    
574        private long delTopicId(String val) {
575            return Long.parseLong(val.substring(DEL_PREFIX.length()));
576        }
577    
578        // ---
579    
580        /**
581         * ### TODO: should not be public. Specify interfaces also for model classes?
582         */
583        public void throwInvalidAccess(String childTypeUri, ClassCastException e) {
584            if (e.getMessage().endsWith("cannot be cast to java.util.List")) {
585                throw new RuntimeException("Invalid access to CompositeValueModel entry \"" + childTypeUri +
586                    "\": the caller assumes it to be multiple-value but it is single-value in\n" + this, e);
587            } else {
588                throw new RuntimeException("Invalid access to CompositeValueModel entry \"" + childTypeUri +
589                    "\" in\n" + this, e);
590            }
591        }
592    }