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>ChildTopicsModel</code>. ### FIXDOC 022 */ 023 public class ChildTopicsModel 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>. 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 ChildTopicsModel() { 048 } 049 050 public ChildTopicsModel(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 ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel 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 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 TopicModel 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 /** 306 * Puts a value in a single-valued child. 307 * An existing value is overwritten. 308 */ 309 public ChildTopicsModel put(String childTypeUri, TopicModel value) { 310 try { 311 // check argument 312 if (value == null) { 313 throw new IllegalArgumentException("Tried to put null in a ChildTopicsModel"); 314 } 315 // 316 childTopics.put(childTypeUri, value); 317 return this; 318 } catch (Exception e) { 319 throw new RuntimeException("Putting a value in a ChildTopicsModel failed (childTypeUri=\"" + 320 childTypeUri + "\", value=" + value + ")", 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 ChildTopicsModel. 333 */ 334 public ChildTopicsModel 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 ChildTopicsModel failed (childTypeUri=\"" + 340 childTypeUri + "\", value=" + value + ")", 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 ChildTopicsModel. 349 */ 350 public ChildTopicsModel put(String childTypeUri, ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel 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 ChildTopicsModel: " + value); 483 } 484 } 485 return json; 486 } catch (Exception e) { 487 throw new RuntimeException("Serialization of a ChildTopicsModel failed (" + this + ")", e); 488 } 489 } 490 491 492 493 // **************** 494 // *** Java API *** 495 // **************** 496 497 498 499 @Override 500 public ChildTopicsModel clone() { 501 ChildTopicsModel clone = new ChildTopicsModel(); 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 ChildTopicsModel: " + 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 ChildTopicsModel(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 ChildTopicsModel 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 ChildTopicsModel entry \"" + childTypeUri + 589 "\" in\n" + this, e); 590 } 591 } 592 }