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 }