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 {
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         * Returns the type URIs of all childs directly contained in this composite value.
150         * ### TODO: could be renamed to "getChildTypeURIs()"
151         */
152        public Iterable<String> keys() {
153            return values.keySet();
154        }
155    
156        /**
157         * Checks if a child is directly contained in this composite value.
158         * ### TODO: could be renamed to "contains()"
159         */
160        public boolean has(String childTypeUri) {
161            return values.containsKey(childTypeUri);
162        }
163    
164        /**
165         * Returns the number of childs directly contained in this composite value.
166         * Multiple-valued childs count as one.
167         */
168        public int size() {
169            return values.size();
170        }
171    
172    
173    
174        // === Convenience Accessors ===
175    
176        /**
177         * Convenience accessor for the *simple* value of a single-valued child.
178         * Throws if the child doesn't exist.
179         */
180        public String getString(String childTypeUri) {
181            return getTopic(childTypeUri).getSimpleValue().toString();
182        }
183    
184        /**
185         * Convenience accessor for the *simple* value of a single-valued child.
186         * Returns a default value if the child doesn't exist.
187         */
188        public String getString(String childTypeUri, String defaultValue) {
189            TopicModel topic = getTopic(childTypeUri, null);
190            return topic != null ? topic.getSimpleValue().toString() : defaultValue;
191        }
192    
193        // ---
194    
195        /**
196         * Convenience accessor for the *simple* value of a single-valued child.
197         * Throws if the child doesn't exist.
198         */
199        public int getInt(String childTypeUri) {
200            return getTopic(childTypeUri).getSimpleValue().intValue();
201        }
202    
203        /**
204         * Convenience accessor for the *simple* value of a single-valued child.
205         * Returns a default value if the child doesn't exist.
206         */
207        public int getInt(String childTypeUri, int defaultValue) {
208            TopicModel topic = getTopic(childTypeUri, null);
209            return topic != null ? topic.getSimpleValue().intValue() : defaultValue;
210        }
211    
212        // ---
213    
214        /**
215         * Convenience accessor for the *simple* value of a single-valued child.
216         * Throws if the child doesn't exist.
217         */
218        public long getLong(String childTypeUri) {
219            return getTopic(childTypeUri).getSimpleValue().longValue();
220        }
221    
222        /**
223         * Convenience accessor for the *simple* value of a single-valued child.
224         * Returns a default value if the child doesn't exist.
225         */
226        public long getLong(String childTypeUri, long defaultValue) {
227            TopicModel topic = getTopic(childTypeUri, null);
228            return topic != null ? topic.getSimpleValue().longValue() : defaultValue;
229        }
230    
231        // ---
232    
233        /**
234         * Convenience accessor for the *simple* value of a single-valued child.
235         * Throws if the child doesn't exist.
236         */
237        public double getDouble(String childTypeUri) {
238            return getTopic(childTypeUri).getSimpleValue().doubleValue();
239        }
240    
241        /**
242         * Convenience accessor for the *simple* value of a single-valued child.
243         * Returns a default value if the child doesn't exist.
244         */
245        public double getDouble(String childTypeUri, double defaultValue) {
246            TopicModel topic = getTopic(childTypeUri, null);
247            return topic != null ? topic.getSimpleValue().doubleValue() : defaultValue;
248        }
249    
250        // ---
251    
252        /**
253         * Convenience accessor for the *simple* value of a single-valued child.
254         * Throws if the child doesn't exist.
255         */
256        public boolean getBoolean(String childTypeUri) {
257            return getTopic(childTypeUri).getSimpleValue().booleanValue();
258        }
259    
260        /**
261         * Convenience accessor for the *simple* value of a single-valued child.
262         * Returns a default value if the child doesn't exist.
263         */
264        public boolean getBoolean(String childTypeUri, boolean defaultValue) {
265            TopicModel topic = getTopic(childTypeUri, null);
266            return topic != null ? topic.getSimpleValue().booleanValue() : defaultValue;
267        }
268    
269        // ---
270    
271        /**
272         * Convenience accessor for the *simple* value of a single-valued child.
273         * Throws if the child doesn't exist.
274         */
275        public Object getObject(String childTypeUri) {
276            return getTopic(childTypeUri).getSimpleValue().value();
277        }
278    
279        /**
280         * Convenience accessor for the *simple* value of a single-valued child.
281         * Returns a default value if the child doesn't exist.
282         */
283        public Object getObject(String childTypeUri, Object defaultValue) {
284            TopicModel topic = getTopic(childTypeUri, null);
285            return topic != null ? topic.getSimpleValue().value() : defaultValue;
286        }
287    
288        // ---
289    
290        /**
291         * Convenience accessor for the *composite* value of a single-valued child.
292         * Throws if the child doesn't exist.
293         */
294        public CompositeValueModel getCompositeValueModel(String childTypeUri) {
295            return getTopic(childTypeUri).getCompositeValueModel();
296        }
297    
298        /**
299         * Convenience accessor for the *composite* value of a single-valued child.
300         * Returns a default value if the child doesn't exist.
301         */
302        public CompositeValueModel getCompositeValueModel(String childTypeUri, CompositeValueModel defaultValue) {
303            TopicModel topic = getTopic(childTypeUri, null);
304            return topic != null ? topic.getCompositeValueModel() : defaultValue;
305        }
306    
307        // Note: there are no convenience accessors for a multiple-valued child.
308    
309    
310    
311        // === Manipulators ===
312    
313        /**
314         * Puts a value in a single-valued child.
315         * An existing value is overwritten.
316         */
317        public CompositeValueModel put(String childTypeUri, TopicModel value) {
318            try {
319                // check argument
320                if (value == null) {
321                    throw new IllegalArgumentException("Tried to put null in a CompositeValueModel");
322                }
323                //
324                values.put(childTypeUri, value);
325                return this;
326            } catch (Exception e) {
327                throw new RuntimeException("Putting a value in a CompositeValueModel failed (childTypeUri=\"" +
328                    childTypeUri + "\", value=" + value + ", composite=" + this + ")", e);
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 CompositeValueModel.
339         */
340        public CompositeValueModel put(String childTypeUri, Object value) {
341            try {
342                values.put(childTypeUri, new TopicModel(childTypeUri, new SimpleValue(value)));
343                return this;
344            } catch (Exception e) {
345                throw new RuntimeException("Putting a value in a CompositeValueModel failed (childTypeUri=\"" +
346                    childTypeUri + "\", value=" + value + ", composite=" + this + ")", e);
347            }
348        }
349    
350        /**
351         * Convenience method to put a *composite* value in a single-valued child.
352         * An existing value is overwritten.
353         *
354         * @return  this CompositeValueModel.
355         */
356        public CompositeValueModel put(String childTypeUri, CompositeValueModel value) {
357            values.put(childTypeUri, new TopicModel(childTypeUri, value));
358            return this;
359        }
360    
361        // ---
362    
363        /**
364         * Puts a by-ID topic reference for a single-valued child.
365         * An existing reference is overwritten.
366         * <p>
367         * Used to maintain the assigment of an *aggregated* child.
368         * Not applicable for a *compositioned* child.
369         */
370        public CompositeValueModel putRef(String childTypeUri, long refTopicId) {
371            put(childTypeUri, new TopicReferenceModel(refTopicId));
372            return this;
373        }
374    
375        /**
376         * Puts a by-URI topic reference for a single-valued child.
377         * An existing reference is overwritten.
378         * <p>
379         * Used to maintain the assigment of an *aggregated* child.
380         * Not applicable for a *compositioned* child.
381         */
382        public CompositeValueModel putRef(String childTypeUri, String refTopicUri) {
383            put(childTypeUri, new TopicReferenceModel(refTopicUri));
384            return this;
385        }
386    
387        // ---
388    
389        /**
390         * Adds a value to a multiple-valued child.
391         */
392        public CompositeValueModel add(String childTypeUri, TopicModel value) {
393            List<TopicModel> topics = getTopics(childTypeUri, null);     // defaultValue=null
394            // Note: topics just created have no child topics yet
395            if (topics == null) {
396                topics = new ArrayList();
397                values.put(childTypeUri, topics);
398            }
399            // Note 1: we must not add a topic twice.
400            // This would happen e.g. when updating multi-facets: the facet values are added while updating, and
401            // would be added again through the PRE_SEND event (Kiezatlas plugin).
402            //
403            // Note 2: we must not add a topic twice *unless* its ID is -1.
404            // This happens when adding a couple of new child topics at once e.g. by pressing the "Add" button
405            // serveral times in a webclient form. These new topic models have no ID yet (-1).
406            if (value.getId() == -1 || !topics.contains(value)) {
407                topics.add(value);
408            }
409            //
410            return this;
411        }
412    
413        /**
414         * Removes a value from a multiple-valued child.
415         */
416        public CompositeValueModel remove(String childTypeUri, TopicModel value) {
417            List<TopicModel> topics = getTopics(childTypeUri, null);     // defaultValue=null
418            if (topics != null) {
419                topics.remove(value);
420            }
421            return this;
422        }
423    
424        // ---
425    
426        /**
427         * Adds a by-ID topic reference to a multiple-valued child.
428         *
429         * Used to maintain the assigments of *aggregated* childs.
430         * Not applicable for *compositioned* childs.
431         */
432        public CompositeValueModel addRef(String childTypeUri, long refTopicId) {
433            add(childTypeUri, new TopicReferenceModel(refTopicId));
434            return this;
435        }
436    
437        /**
438         * Adds a by-URI topic reference to a multiple-valued child.
439         *
440         * Used to maintain the assigments of *aggregated* childs.
441         * Not applicable for *compositioned* childs.
442         */
443        public CompositeValueModel addRef(String childTypeUri, String refTopicUri) {
444            add(childTypeUri, new TopicReferenceModel(refTopicUri));
445            return this;
446        }
447    
448    
449    
450        // ===
451    
452        public JSONObject toJSON() {
453            try {
454                JSONObject json = new JSONObject();
455                for (String childTypeUri : keys()) {
456                    Object value = get(childTypeUri);
457                    if (value instanceof TopicModel) {
458                        json.put(childTypeUri, ((TopicModel) value).toJSON());
459                    } else if (value instanceof List) {
460                        json.put(childTypeUri, DeepaMehtaUtils.objectsToJSON((List<TopicModel>) value));
461                    } else {
462                        throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value);
463                    }
464                }
465                return json;
466            } catch (Exception e) {
467                throw new RuntimeException("Serialization of a CompositeValueModel failed (" + this + ")", e);
468            }
469        }
470    
471    
472    
473        // ****************
474        // *** Java API ***
475        // ****************
476    
477    
478    
479        @Override
480        public CompositeValueModel clone() {
481            CompositeValueModel clone = new CompositeValueModel();
482            for (String childTypeUri : keys()) {
483                Object value = get(childTypeUri);
484                if (value instanceof TopicModel) {
485                    TopicModel model = (TopicModel) value;
486                    clone.put(childTypeUri, model.clone());
487                } else if (value instanceof List) {
488                    for (TopicModel model : (List<TopicModel>) value) {
489                        clone.add(childTypeUri, model.clone());
490                    }
491                } else {
492                    throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value);
493                }
494            }
495            return clone;
496        }
497    
498        @Override
499        public String toString() {
500            return values.toString();
501        }
502    
503    
504    
505        // ------------------------------------------------------------------------------------------------- Private Methods
506    
507        /**
508         * Creates a topic model from a JSON value.
509         *
510         * Both topic serialization formats are supported:
511         * 1) canonic format -- contains entire topic models.
512         * 2) compact format -- contains the topic value only (simple or composite).
513         */
514        private TopicModel createTopicModel(String childTypeUri, Object value) {
515            if (value instanceof JSONObject) {
516                JSONObject val = (JSONObject) value;
517                // we detect the canonic format by checking for a mandatory topic property
518                // ### TODO: "type_uri" should not be regarded mandatory. It would simplify update requests.
519                // ### Can we use another heuristic for detection: "value" exists OR "composite" exists?
520                if (val.has("type_uri")) {
521                    // canonic format
522                    return new TopicModel(val);
523                } else {
524                    // compact format (composite topic)
525                    return new TopicModel(childTypeUri, new CompositeValueModel(val));
526                }
527            } else {
528                // compact format (simple topic or topic reference)
529                if (value instanceof String) {
530                    String val = (String) value;
531                    if (val.startsWith(REF_ID_PREFIX)) {
532                        return new TopicReferenceModel(refTopicId(val));    // topic reference by-ID
533                    } else if (val.startsWith(REF_URI_PREFIX)) {
534                        return new TopicReferenceModel(refTopicUri(val));   // topic reference by-URI
535                    } else if (val.startsWith(DEL_PREFIX)) {
536                        return new TopicDeletionModel(delTopicId(val));     // topic deletion reference
537                    }
538                }
539                // compact format (simple topic)
540                return new TopicModel(childTypeUri, new SimpleValue(value));
541            }
542        }
543    
544        // ---
545    
546        private long refTopicId(String val) {
547            return Long.parseLong(val.substring(REF_ID_PREFIX.length()));
548        }
549    
550        private String refTopicUri(String val) {
551            return val.substring(REF_URI_PREFIX.length());
552        }
553    
554        private long delTopicId(String val) {
555            return Long.parseLong(val.substring(DEL_PREFIX.length()));
556        }
557    
558        // ---
559    
560        /**
561         * ### TODO: should not be public. Specify interfaces also for model classes?
562         */
563        public void throwInvalidAccess(String childTypeUri, ClassCastException e) {
564            if (e.getMessage().endsWith("cannot be cast to java.util.List")) {
565                throw new RuntimeException("Invalid access to CompositeValueModel entry \"" + childTypeUri +
566                    "\": the caller assumes it to be multiple-value but it is single-value in\n" + this, e);
567            } else {
568                throw new RuntimeException("Invalid access to CompositeValueModel entry \"" + childTypeUri +
569                    "\" in\n" + this, e);
570            }
571        }
572    }