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> values = 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 values.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 values.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 values.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                values.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                values.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            values.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                values.put(childTypeUri, topics);
392            }
393            // Note 1: we must not add a topic twice.
394            // This would happen e.g. when updating multi-facets: the facet values are added while updating, and
395            // would be added again through the PRE_SEND event (Kiezatlas plugin).
396            //
397            // Note 2: we must not add a topic twice *unless* its ID is -1.
398            // This happens when adding a couple of new child topics at once e.g. by pressing the "Add" button
399            // serveral times in a webclient form. These new topic models have no ID yet (-1).
400            if (value.getId() == -1 || !topics.contains(value)) {
401                topics.add(value);
402            }
403            //
404            return this;
405        }
406    
407        /**
408         * Removes a value from a multiple-valued child.
409         */
410        public CompositeValueModel remove(String childTypeUri, TopicModel value) {
411            List<TopicModel> topics = getTopics(childTypeUri, null);     // defaultValue=null
412            if (topics != null) {
413                topics.remove(value);
414            }
415            return this;
416        }
417    
418        // ---
419    
420        /**
421         * Adds a by-ID topic reference to a multiple-valued child.
422         *
423         * Used to maintain the assigments of *aggregated* childs.
424         * Not applicable for *compositioned* childs.
425         */
426        public CompositeValueModel addRef(String childTypeUri, long refTopicId) {
427            add(childTypeUri, new TopicReferenceModel(refTopicId));
428            return this;
429        }
430    
431        /**
432         * Adds a by-URI topic reference to a multiple-valued child.
433         *
434         * Used to maintain the assigments of *aggregated* childs.
435         * Not applicable for *compositioned* childs.
436         */
437        public CompositeValueModel addRef(String childTypeUri, String refTopicUri) {
438            add(childTypeUri, new TopicReferenceModel(refTopicUri));
439            return this;
440        }
441    
442    
443    
444        // === Iterable Implementation ===
445    
446        /**
447         * Returns an interator which iterates this composite value's child type URIs.
448         */
449        @Override
450        public Iterator<String> iterator() {
451            return values.keySet().iterator();
452        }
453    
454    
455    
456        // ===
457    
458        public JSONObject toJSON() {
459            try {
460                JSONObject json = new JSONObject();
461                for (String childTypeUri : this) {
462                    Object value = get(childTypeUri);
463                    if (value instanceof TopicModel) {
464                        json.put(childTypeUri, ((TopicModel) value).toJSON());
465                    } else if (value instanceof List) {
466                        json.put(childTypeUri, DeepaMehtaUtils.objectsToJSON((List<TopicModel>) value));
467                    } else {
468                        throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value);
469                    }
470                }
471                return json;
472            } catch (Exception e) {
473                throw new RuntimeException("Serialization of a CompositeValueModel failed (" + this + ")", e);
474            }
475        }
476    
477    
478    
479        // ****************
480        // *** Java API ***
481        // ****************
482    
483    
484    
485        @Override
486        public CompositeValueModel clone() {
487            CompositeValueModel clone = new CompositeValueModel();
488            for (String childTypeUri : this) {
489                Object value = get(childTypeUri);
490                if (value instanceof TopicModel) {
491                    TopicModel model = (TopicModel) value;
492                    clone.put(childTypeUri, model.clone());
493                } else if (value instanceof List) {
494                    for (TopicModel model : (List<TopicModel>) value) {
495                        clone.add(childTypeUri, model.clone());
496                    }
497                } else {
498                    throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value);
499                }
500            }
501            return clone;
502        }
503    
504        @Override
505        public String toString() {
506            return values.toString();
507        }
508    
509    
510    
511        // ------------------------------------------------------------------------------------------------- Private Methods
512    
513        /**
514         * Creates a topic model from a JSON value.
515         *
516         * Both topic serialization formats are supported:
517         * 1) canonic format -- contains entire topic models.
518         * 2) compact format -- contains the topic value only (simple or composite).
519         */
520        private TopicModel createTopicModel(String childTypeUri, Object value) {
521            if (value instanceof JSONObject) {
522                JSONObject val = (JSONObject) value;
523                // we detect the canonic format by checking for a mandatory topic property
524                // ### TODO: "type_uri" should not be regarded mandatory. It would simplify update requests.
525                // ### Can we use another heuristic for detection: "value" exists OR "composite" exists?
526                if (val.has("type_uri")) {
527                    // canonic format
528                    return new TopicModel(val);
529                } else {
530                    // compact format (composite topic)
531                    return new TopicModel(childTypeUri, new CompositeValueModel(val));
532                }
533            } else {
534                // compact format (simple topic or topic reference)
535                if (value instanceof String) {
536                    String val = (String) value;
537                    if (val.startsWith(REF_ID_PREFIX)) {
538                        return new TopicReferenceModel(refTopicId(val));    // topic reference by-ID
539                    } else if (val.startsWith(REF_URI_PREFIX)) {
540                        return new TopicReferenceModel(refTopicUri(val));   // topic reference by-URI
541                    } else if (val.startsWith(DEL_PREFIX)) {
542                        return new TopicDeletionModel(delTopicId(val));     // topic deletion reference
543                    }
544                }
545                // compact format (simple topic)
546                return new TopicModel(childTypeUri, new SimpleValue(value));
547            }
548        }
549    
550        // ---
551    
552        private long refTopicId(String val) {
553            return Long.parseLong(val.substring(REF_ID_PREFIX.length()));
554        }
555    
556        private String refTopicUri(String val) {
557            return val.substring(REF_URI_PREFIX.length());
558        }
559    
560        private long delTopicId(String val) {
561            return Long.parseLong(val.substring(DEL_PREFIX.length()));
562        }
563    
564        // ---
565    
566        /**
567         * ### TODO: should not be public. Specify interfaces also for model classes?
568         */
569        public void throwInvalidAccess(String childTypeUri, ClassCastException e) {
570            if (e.getMessage().endsWith("cannot be cast to java.util.List")) {
571                throw new RuntimeException("Invalid access to CompositeValueModel entry \"" + childTypeUri +
572                    "\": the caller assumes it to be multiple-value but it is single-value in\n" + this, e);
573            } else {
574                throw new RuntimeException("Invalid access to CompositeValueModel entry \"" + childTypeUri +
575                    "\" in\n" + this, e);
576            }
577        }
578    }