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 }