001/* 002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.storage; 020 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Calendar; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.Iterator; 029import java.util.LinkedHashSet; 030import java.util.List; 031import java.util.Map; 032import java.util.NoSuchElementException; 033import java.util.Map.Entry; 034import java.util.Set; 035import java.util.concurrent.ConcurrentHashMap; 036 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039 040import com.google.common.collect.ImmutableSet; 041 042/** 043 * Abstraction for a Map<String, Serializable> that is Serializable. 044 * <p> 045 * Internal storage is optimized to avoid a full {@link HashMap} when there is a small number of keys. 046 * 047 * @since 5.9.5 048 */ 049public class State implements StateAccessor, Serializable { 050 051 private static final long serialVersionUID = 1L; 052 053 protected static final Log log = LogFactory.getLog(State.class); 054 055 private static final int HASHMAP_DEFAULT_INITIAL_CAPACITY = 16; 056 057 private static final float HASHMAP_DEFAULT_LOAD_FACTOR = 0.75f; 058 059 // maximum size to use an array after which we switch to a full HashMap 060 public static final int ARRAY_MAX = 5; 061 062 private static final int DEBUG_MAX_STRING = 100; 063 064 private static final int DEBUG_MAX_ARRAY = 10; 065 066 public static final State EMPTY = new State(Collections.<String, Serializable> emptyMap()); 067 068 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 069 070 /** Initial key order for the {@link #toString} method. */ 071 private static final Set<String> TO_STRING_KEY_ORDER = new LinkedHashSet<>(Arrays.asList( 072 new String[] { "ecm:id", "ecm:primaryType", "ecm:name", "ecm:parentId", "ecm:isVersion", "ecm:isProxy" })); 073 074 /** 075 * A diff for a {@link State}. 076 * <p> 077 * Each value is applied to the existing {@link State}. An element can be: 078 * <ul> 079 * <li>a {@link StateDiff}, to be applied on a {@link State}, 080 * <li>a {@link ListDiff}, to be applied on an array/{@link List}, 081 * <li>an actual value to be set (including {@code null}). 082 * </ul> 083 * 084 * @since 5.9.5 085 */ 086 public static class StateDiff extends State { 087 private static final long serialVersionUID = 1L; 088 089 @Override 090 public void put(String key, Serializable value) { 091 // for a StateDiff, we don't have concurrency problems 092 // and we want to store nulls explicitly 093 putEvenIfNull(key, value); 094 } 095 } 096 097 /** 098 * Singleton marker. 099 */ 100 private enum Nop { 101 NOP 102 } 103 104 /** 105 * Denotes no change to an element. 106 */ 107 public static final Nop NOP = Nop.NOP; 108 109 /** 110 * A diff for an array or {@link List}. 111 * <p> 112 * This diff is applied onto an existing array/{@link List} in the following manner: 113 * <ul> 114 * <li>{@link #diff}, if any, is applied, 115 * <li>{@link #rpush}, if any, is applied. 116 * <li>{@link #pull}, if any, is applied. 117 * </ul> 118 * 119 * @since 5.9.5 120 */ 121 public static class ListDiff implements Serializable { 122 123 private static final long serialVersionUID = 1L; 124 125 /** 126 * Whether this {@link ListDiff} applies to an array ({@code true}) or a {@link List} ({@code false}). 127 */ 128 public boolean isArray; 129 130 /** 131 * If diff is not {@code null}, each element of the list is applied to the existing array/{@link List}. An 132 * element can be: 133 * <ul> 134 * <li>a {@link StateDiff}, to be applied on a {@link State}, 135 * <li>an actual value to be set (including {@code null}), 136 * <li>{@link #NOP} if no change is needed. 137 * </ul> 138 */ 139 public List<Object> diff; 140 141 /** 142 * If rpush is not {@code null}, this is appended to the right of the existing array/{@link List}. 143 */ 144 public List<Object> rpush; 145 146 /** 147 * If pull is not {@code null}, this is removed from the existing array/{@link List}. 148 * 149 * @since 11.5 150 */ 151 public List<Object> pull; 152 153 @Override 154 public String toString() { 155 return getClass().getSimpleName() + '(' + (isArray ? "array" : "list") 156 + (diff == null ? "" : ", DIFF " + diff) // 157 + (rpush == null ? "" : ", RPUSH " + rpush) // 158 + (pull == null ? "" : ", PULL " + pull) + ')'; 159 } 160 } 161 162 // if map != null then use it 163 protected Map<String, Serializable> map; 164 165 // else use keys / values 166 protected List<String> keys; 167 168 protected List<Serializable> values; 169 170 /** 171 * Private constructor with explicit map. 172 */ 173 private State(Map<String, Serializable> map) { 174 this.map = map; 175 } 176 177 /** 178 * Constructor with default capacity. 179 */ 180 public State() { 181 this(0, false); 182 } 183 184 /** 185 * Constructor with default capacity, optionally thread-safe. 186 * 187 * @param threadSafe if {@code true}, then a {@link ConcurrentHashMap} is used 188 */ 189 public State(boolean threadSafe) { 190 this(0, threadSafe); 191 } 192 193 /** 194 * Constructor for a given default size. 195 */ 196 public State(int size) { 197 this(size, false); 198 } 199 200 /** 201 * Constructor for a given default size, optionally thread-safe. 202 * 203 * @param threadSafe if {@code true}, then a {@link ConcurrentHashMap} is used 204 */ 205 public State(int size, boolean threadSafe) { 206 if (threadSafe) { 207 map = new ConcurrentHashMap<>(initialCapacity(size)); 208 } else { 209 if (size > ARRAY_MAX) { 210 map = new HashMap<>(initialCapacity(size)); 211 } else { 212 keys = new ArrayList<>(size); 213 values = new ArrayList<>(size); 214 } 215 } 216 } 217 218 protected static int initialCapacity(int size) { 219 return Math.max((int) (size / HASHMAP_DEFAULT_LOAD_FACTOR) + 1, HASHMAP_DEFAULT_INITIAL_CAPACITY); 220 } 221 222 /** 223 * Gets the number of elements. 224 */ 225 public int size() { 226 if (map != null) { 227 return map.size(); 228 } else { 229 return keys.size(); 230 } 231 } 232 233 /** 234 * Checks if the state is empty. 235 */ 236 public boolean isEmpty() { 237 if (map != null) { 238 return map.isEmpty(); 239 } else { 240 return keys.isEmpty(); 241 } 242 } 243 244 /** 245 * Gets a value for a key, or {@code null} if the key is not present. 246 */ 247 public Serializable get(Object key) { 248 if (map != null) { 249 return map.get(key); 250 } else { 251 int i = keys.indexOf(key); 252 return i >= 0 ? values.get(i) : null; 253 } 254 } 255 256 /** 257 * Sets a key/value. 258 */ 259 public void put(String key, Serializable value) { 260 if (value == null) { 261 // if we're using a ConcurrentHashMap 262 // then null values are forbidden 263 // this is ok given our semantics of null vs absent key 264 if (map != null) { 265 map.remove(key); 266 } else { 267 int i = keys.indexOf(key); 268 if (i >= 0) { 269 // cost is not trivial but we don't use this often, if at all 270 keys.remove(i); 271 values.remove(i); 272 } 273 } 274 } else { 275 putEvenIfNull(key, value); 276 } 277 } 278 279 protected void putEvenIfNull(String key, Serializable value) { 280 if (map != null) { 281 map.put(key, value); 282 } else { 283 int i = keys.indexOf(key); 284 if (i >= 0) { 285 // existing key 286 values.set(i, value); 287 } else { 288 // new key 289 if (keys.size() < ARRAY_MAX) { 290 keys.add(key); 291 values.add(value); 292 } else { 293 // upgrade to a full HashMap 294 map = new HashMap<>(initialCapacity(keys.size() + 1)); 295 for (int j = 0; j < keys.size(); j++) { 296 map.put(keys.get(j), values.get(j)); 297 } 298 map.put(key, value); 299 keys = null; 300 values = null; 301 } 302 } 303 } 304 } 305 306 /** 307 * Removes the mapping for a key. 308 * 309 * @return the previous value associated with the key, or {@code null} if there was no mapping for the key 310 */ 311 public Serializable remove(Object key) { 312 if (map != null) { 313 return map.remove(key); 314 } else { 315 int i = keys.indexOf(key); 316 if (i >= 0) { 317 keys.remove(i); 318 return values.remove(i); 319 } else { 320 return null; 321 } 322 } 323 } 324 325 /** 326 * Gets the key set. IT MUST NOT BE MODIFIED. 327 */ 328 public Set<String> keySet() { 329 if (map != null) { 330 return map.keySet(); 331 } else { 332 return ImmutableSet.copyOf(keys); 333 } 334 } 335 336 /** 337 * Gets an array of keys. 338 */ 339 public String[] keyArray() { 340 if (map != null) { 341 return map.keySet().toArray(EMPTY_STRING_ARRAY); 342 } else { 343 return keys.toArray(EMPTY_STRING_ARRAY); 344 } 345 } 346 347 /** 348 * Checks if there is a mapping for the given key. 349 */ 350 public boolean containsKey(Object key) { 351 if (map != null) { 352 return map.containsKey(key); 353 } else { 354 return keys.contains(key); 355 } 356 } 357 358 /** 359 * Gets the entry set. IT MUST NOT BE MODIFIED. 360 */ 361 public Set<Entry<String, Serializable>> entrySet() { 362 if (map != null) { 363 return map.entrySet(); 364 } else { 365 return new ArraysEntrySet(); 366 } 367 } 368 369 /** EntrySet optimized to just return a simple Iterator on the entries. */ 370 protected class ArraysEntrySet implements Set<Entry<String, Serializable>> { 371 372 @Override 373 public int size() { 374 return keys.size(); 375 } 376 377 @Override 378 public boolean isEmpty() { 379 return keys.isEmpty(); 380 } 381 382 @Override 383 public Iterator<Entry<String, Serializable>> iterator() { 384 return new ArraysEntryIterator(); 385 } 386 387 @Override 388 public boolean contains(Object o) { 389 throw new UnsupportedOperationException(); 390 } 391 392 @Override 393 public Object[] toArray() { 394 throw new UnsupportedOperationException(); 395 } 396 397 @Override 398 public <T> T[] toArray(T[] a) { 399 throw new UnsupportedOperationException(); 400 } 401 402 @Override 403 public boolean add(Entry<String, Serializable> e) { 404 throw new UnsupportedOperationException(); 405 } 406 407 @Override 408 public boolean remove(Object o) { 409 throw new UnsupportedOperationException(); 410 } 411 412 @Override 413 public boolean containsAll(Collection<?> c) { 414 throw new UnsupportedOperationException(); 415 } 416 417 @Override 418 public boolean addAll(Collection<? extends Entry<String, Serializable>> c) { 419 throw new UnsupportedOperationException(); 420 } 421 422 @Override 423 public boolean retainAll(Collection<?> c) { 424 throw new UnsupportedOperationException(); 425 } 426 427 @Override 428 public boolean removeAll(Collection<?> c) { 429 throw new UnsupportedOperationException(); 430 } 431 432 @Override 433 public void clear() { 434 throw new UnsupportedOperationException(); 435 } 436 } 437 438 public class ArraysEntryIterator implements Iterator<Entry<String, Serializable>> { 439 440 private int index; 441 442 @Override 443 public boolean hasNext() { 444 return index < keys.size(); 445 } 446 447 @Override 448 public Entry<String, Serializable> next() { 449 if (!hasNext()) { 450 throw new NoSuchElementException(); 451 } 452 return new ArraysEntry(index++); 453 } 454 } 455 456 public class ArraysEntry implements Entry<String, Serializable> { 457 458 private final int index; 459 460 public ArraysEntry(int index) { 461 this.index = index; 462 } 463 464 @Override 465 public String getKey() { 466 return keys.get(index); 467 } 468 469 @Override 470 public Serializable getValue() { 471 return values.get(index); 472 } 473 474 @Override 475 public Serializable setValue(Serializable value) { 476 throw new UnsupportedOperationException(); 477 } 478 } 479 480 /** 481 * Overridden to display Calendars and arrays better, and truncate long strings and arrays. 482 * <p> 483 * Also displays some keys first (ecm:id, ecm:name, ecm:primaryType) 484 */ 485 @Override 486 public String toString() { 487 if (isEmpty()) { 488 return "{}"; 489 } 490 StringBuilder sb = new StringBuilder(); 491 sb.append('{'); 492 boolean empty = true; 493 // some keys go first 494 for (String key : TO_STRING_KEY_ORDER) { 495 if (containsKey(key)) { 496 if (!empty) { 497 sb.append(", "); 498 } 499 empty = false; 500 sb.append(key); 501 sb.append('='); 502 toString(sb, get(key)); 503 } 504 } 505 // sort keys 506 String[] keys = keyArray(); 507 Arrays.sort(keys); 508 for (String key : keys) { 509 if (TO_STRING_KEY_ORDER.contains(key)) { 510 // already done 511 continue; 512 } 513 if (!empty) { 514 sb.append(", "); 515 } 516 empty = false; 517 sb.append(key); 518 sb.append('='); 519 toString(sb, get(key)); 520 } 521 sb.append('}'); 522 return sb.toString(); 523 } 524 525 @SuppressWarnings("boxing") 526 protected static void toString(StringBuilder sb, Object value) { 527 if (value instanceof String) { 528 String v = (String) value; 529 if (v.length() > DEBUG_MAX_STRING) { 530 v = v.substring(0, DEBUG_MAX_STRING) + "...(" + v.length() + " chars)..."; 531 } 532 sb.append(v); 533 } else if (value instanceof Calendar) { 534 Calendar cal = (Calendar) value; 535 char sign; 536 int offset = cal.getTimeZone().getOffset(cal.getTimeInMillis()) / 60000; 537 if (offset < 0) { 538 offset = -offset; 539 sign = '-'; 540 } else { 541 sign = '+'; 542 } 543 sb.append(String.format("Calendar(%04d-%02d-%02dT%02d:%02d:%02d.%03d%c%02d:%02d)", cal.get(Calendar.YEAR), // 544 cal.get(Calendar.MONTH) + 1, // 545 cal.get(Calendar.DAY_OF_MONTH), // 546 cal.get(Calendar.HOUR_OF_DAY), // 547 cal.get(Calendar.MINUTE), // 548 cal.get(Calendar.SECOND), // 549 cal.get(Calendar.MILLISECOND), // 550 sign, offset / 60, offset % 60)); 551 } else if (value instanceof Object[]) { 552 Object[] v = (Object[]) value; 553 sb.append('['); 554 for (int i = 0; i < v.length; i++) { 555 if (i > 0) { 556 sb.append(','); 557 if (i > DEBUG_MAX_ARRAY) { 558 sb.append("...(").append(v.length).append(" items)..."); 559 break; 560 } 561 } 562 toString(sb, v[i]); 563 } 564 sb.append(']'); 565 } else { 566 sb.append(value); 567 } 568 } 569 570 @Override 571 public Object getSingle(String name) { 572 Serializable object = get(name); 573 if (object instanceof Object[]) { 574 Object[] array = (Object[]) object; 575 if (array.length == 0) { 576 return null; 577 } else if (array.length == 1) { 578 // data migration not done in database, return a simple value anyway 579 return array[0]; 580 } else { 581 log.warn("Property " + name + ": expected a simple value but read an array: " + Arrays.toString(array)); 582 return array[0]; 583 } 584 } else { 585 return object; 586 } 587 } 588 589 @Override 590 public Object[] getArray(String name) { 591 Serializable object = get(name); 592 if (object == null) { 593 return null; 594 } else if (object instanceof Object[]) { 595 return (Object[]) object; 596 } else { 597 // data migration not done in database, return an array anyway 598 return new Object[] { object }; 599 } 600 } 601 602 @Override 603 public void setSingle(String name, Object value) { 604 put(name, (Serializable) value); 605 } 606 607 @Override 608 public void setArray(String name, Object[] value) { 609 put(name, value); 610 } 611 612 @Override 613 public boolean equals(Object other) { 614 return StateHelper.equalsStrict(this, other); 615 } 616 617}