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