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