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