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 }