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.Map.Entry; 033import java.util.Set; 034import java.util.concurrent.ConcurrentHashMap; 035 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038 039import com.google.common.collect.ImmutableSet; 040 041/** 042 * Abstraction for a Map<String, Serializable> that is Serializable. 043 * <p> 044 * Internal storage is optimized to avoid a full {@link HashMap} when there is a small number of keys. 045 * 046 * @since 5.9.5 047 */ 048public class State implements StateAccessor, Serializable { 049 050 private static final long serialVersionUID = 1L; 051 052 protected static final Log log = LogFactory.getLog(State.class); 053 054 private static final int HASHMAP_DEFAULT_INITIAL_CAPACITY = 16; 055 056 private static final float HASHMAP_DEFAULT_LOAD_FACTOR = 0.75f; 057 058 // maximum size to use an array after which we switch to a full HashMap 059 public static final int ARRAY_MAX = 5; 060 061 private static final int DEBUG_MAX_STRING = 100; 062 063 private static final int DEBUG_MAX_ARRAY = 10; 064 065 public static final State EMPTY = new State(Collections.<String, Serializable> emptyMap()); 066 067 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 068 069 /** Initial key order for the {@link #toString} method. */ 070 private static final Set<String> TO_STRING_KEY_ORDER = new LinkedHashSet<>(Arrays.asList( 071 new String[] { "ecm:id", "ecm:primaryType", "ecm:name", "ecm:parentId", "ecm:isVersion", "ecm:isProxy" })); 072 073 /** 074 * A diff for a {@link State}. 075 * <p> 076 * Each value is applied to the existing {@link State}. An element can be: 077 * <ul> 078 * <li>a {@link StateDiff}, to be applied on a {@link State}, 079 * <li>a {@link ListDiff}, to be applied on an array/{@link List}, 080 * <li>an actual value to be set (including {@code null}). 081 * </ul> 082 * 083 * @since 5.9.5 084 */ 085 public static class StateDiff extends State { 086 private static final long serialVersionUID = 1L; 087 088 @Override 089 public void put(String key, Serializable value) { 090 // for a StateDiff, we don't have concurrency problems 091 // and we want to store nulls explicitly 092 putEvenIfNull(key, value); 093 } 094 } 095 096 /** 097 * Singleton marker. 098 */ 099 private static enum Nop { 100 NOP 101 } 102 103 /** 104 * Denotes no change to an element. 105 */ 106 public static final Nop NOP = Nop.NOP; 107 108 /** 109 * A diff for an array or {@link List}. 110 * <p> 111 * This diff is applied onto an existing array/{@link List} in the following manner: 112 * <ul> 113 * <li>{@link #diff}, if any, is applied, 114 * <li>{@link #rpush}, if any, is applied. 115 * </ul> 116 * 117 * @since 5.9.5 118 */ 119 public static class ListDiff implements Serializable { 120 121 private static final long serialVersionUID = 1L; 122 123 /** 124 * Whether this {@link ListDiff} applies to an array ({@code true}) or a {@link List} ({@code false}). 125 */ 126 public boolean isArray; 127 128 /** 129 * If diff is not {@code null}, each element of the list is applied to the existing array/{@link List}. An 130 * element can be: 131 * <ul> 132 * <li>a {@link StateDiff}, to be applied on a {@link State}, 133 * <li>an actual value to be set (including {@code null}), 134 * <li>{@link #NOP} if no change is needed. 135 * </ul> 136 */ 137 public List<Object> diff; 138 139 /** 140 * If rpush is not {@code null}, this is appended to the right of the existing array/{@link List}. 141 */ 142 public List<Object> rpush; 143 144 @Override 145 public String toString() { 146 return getClass().getSimpleName() + '(' + (isArray ? "array" : "list") 147 + (diff == null ? "" : ", DIFF " + diff) + (rpush == null ? "" : ", RPUSH " + rpush) + ')'; 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 put(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, 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); 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, value); 288 keys = null; 289 values = null; 290 } 291 } 292 } 293 } 294 295 /** 296 * Removes the mapping for a key. 297 * 298 * @return the previous value associated with the key, or {@code null} if there was no mapping for the key 299 */ 300 public Serializable remove(Object key) { 301 if (map != null) { 302 return map.remove(key); 303 } else { 304 int i = keys.indexOf(key); 305 if (i >= 0) { 306 keys.remove(i); 307 return values.remove(i); 308 } else { 309 return null; 310 } 311 } 312 } 313 314 /** 315 * Gets the key set. IT MUST NOT BE MODIFIED. 316 */ 317 public Set<String> keySet() { 318 if (map != null) { 319 return map.keySet(); 320 } else { 321 return ImmutableSet.copyOf(keys); 322 } 323 } 324 325 /** 326 * Gets an array of keys. 327 */ 328 public String[] keyArray() { 329 if (map != null) { 330 return map.keySet().toArray(EMPTY_STRING_ARRAY); 331 } else { 332 return keys.toArray(EMPTY_STRING_ARRAY); 333 } 334 } 335 336 /** 337 * Checks if there is a mapping for the given key. 338 */ 339 public boolean containsKey(Object key) { 340 if (map != null) { 341 return map.containsKey(key); 342 } else { 343 return keys.contains(key); 344 } 345 } 346 347 /** 348 * Gets the entry set. IT MUST NOT BE MODIFIED. 349 */ 350 public Set<Entry<String, Serializable>> entrySet() { 351 if (map != null) { 352 return map.entrySet(); 353 } else { 354 return new ArraysEntrySet(); 355 } 356 } 357 358 /** EntrySet optimized to just return a simple Iterator on the entries. */ 359 protected class ArraysEntrySet implements Set<Entry<String, Serializable>> { 360 361 @Override 362 public int size() { 363 return keys.size(); 364 } 365 366 @Override 367 public boolean isEmpty() { 368 return keys.isEmpty(); 369 } 370 371 @Override 372 public Iterator<Entry<String, Serializable>> iterator() { 373 return new ArraysEntryIterator(); 374 } 375 376 @Override 377 public boolean contains(Object o) { 378 throw new UnsupportedOperationException(); 379 } 380 381 @Override 382 public Object[] toArray() { 383 throw new UnsupportedOperationException(); 384 } 385 386 @Override 387 public <T> T[] toArray(T[] a) { 388 throw new UnsupportedOperationException(); 389 } 390 391 @Override 392 public boolean add(Entry<String, Serializable> e) { 393 throw new UnsupportedOperationException(); 394 } 395 396 @Override 397 public boolean remove(Object o) { 398 throw new UnsupportedOperationException(); 399 } 400 401 @Override 402 public boolean containsAll(Collection<?> c) { 403 throw new UnsupportedOperationException(); 404 } 405 406 @Override 407 public boolean addAll(Collection<? extends Entry<String, Serializable>> c) { 408 throw new UnsupportedOperationException(); 409 } 410 411 @Override 412 public boolean retainAll(Collection<?> c) { 413 throw new UnsupportedOperationException(); 414 } 415 416 @Override 417 public boolean removeAll(Collection<?> c) { 418 throw new UnsupportedOperationException(); 419 } 420 421 @Override 422 public void clear() { 423 throw new UnsupportedOperationException(); 424 } 425 } 426 427 public class ArraysEntryIterator implements Iterator<Entry<String, Serializable>> { 428 429 private int index; 430 431 @Override 432 public boolean hasNext() { 433 return index < keys.size(); 434 } 435 436 @Override 437 public Entry<String, Serializable> next() { 438 return new ArraysEntry(index++); 439 } 440 } 441 442 public class ArraysEntry implements Entry<String, Serializable> { 443 444 private final int index; 445 446 public ArraysEntry(int index) { 447 this.index = index; 448 } 449 450 @Override 451 public String getKey() { 452 return keys.get(index); 453 } 454 455 @Override 456 public Serializable getValue() { 457 return values.get(index); 458 } 459 460 @Override 461 public Serializable setValue(Serializable value) { 462 throw new UnsupportedOperationException(); 463 } 464 } 465 466 /** 467 * Overridden to display Calendars and arrays better, and truncate long strings and arrays. 468 * <p> 469 * Also displays some keys first (ecm:id, ecm:name, ecm:primaryType) 470 */ 471 @Override 472 public String toString() { 473 if (isEmpty()) { 474 return "{}"; 475 } 476 StringBuilder buf = new StringBuilder(); 477 buf.append('{'); 478 boolean empty = true; 479 // some keys go first 480 for (String key : TO_STRING_KEY_ORDER) { 481 if (containsKey(key)) { 482 if (!empty) { 483 buf.append(", "); 484 } 485 empty = false; 486 buf.append(key); 487 buf.append('='); 488 toString(buf, get(key)); 489 } 490 } 491 // sort keys 492 String[] keys = keyArray(); 493 Arrays.sort(keys); 494 for (String key : keys) { 495 if (TO_STRING_KEY_ORDER.contains(key)) { 496 // already done 497 continue; 498 } 499 if (!empty) { 500 buf.append(", "); 501 } 502 empty = false; 503 buf.append(key); 504 buf.append('='); 505 toString(buf, get(key)); 506 } 507 buf.append('}'); 508 return buf.toString(); 509 } 510 511 @SuppressWarnings("boxing") 512 protected static void toString(StringBuilder buf, Object value) { 513 if (value instanceof String) { 514 String v = (String) value; 515 if (v.length() > DEBUG_MAX_STRING) { 516 v = v.substring(0, DEBUG_MAX_STRING) + "...(" + v.length() + " chars)..."; 517 } 518 buf.append(v); 519 } else if (value instanceof Calendar) { 520 Calendar cal = (Calendar) value; 521 char sign; 522 int offset = cal.getTimeZone().getOffset(cal.getTimeInMillis()) / 60000; 523 if (offset < 0) { 524 offset = -offset; 525 sign = '-'; 526 } else { 527 sign = '+'; 528 } 529 buf.append(String.format("Calendar(%04d-%02d-%02dT%02d:%02d:%02d.%03d%c%02d:%02d)", cal.get(Calendar.YEAR), // 530 cal.get(Calendar.MONTH) + 1, // 531 cal.get(Calendar.DAY_OF_MONTH), // 532 cal.get(Calendar.HOUR_OF_DAY), // 533 cal.get(Calendar.MINUTE), // 534 cal.get(Calendar.SECOND), // 535 cal.get(Calendar.MILLISECOND), // 536 sign, offset / 60, offset % 60)); 537 } else if (value instanceof Object[]) { 538 Object[] v = (Object[]) value; 539 buf.append('['); 540 for (int i = 0; i < v.length; i++) { 541 if (i > 0) { 542 buf.append(','); 543 if (i > DEBUG_MAX_ARRAY) { 544 buf.append("...(" + v.length + " items)..."); 545 break; 546 } 547 } 548 toString(buf, v[i]); 549 } 550 buf.append(']'); 551 } else { 552 buf.append(value); 553 } 554 } 555 556 @Override 557 public Object getSingle(String name) { 558 Serializable object = get(name); 559 if (object instanceof Object[]) { 560 Object[] array = (Object[]) object; 561 if (array.length == 0) { 562 return null; 563 } else if (array.length == 1) { 564 // data migration not done in database, return a simple value anyway 565 return array[0]; 566 } else { 567 log.warn("Property " + name + ": expected a simple value but read an array: " + Arrays.toString(array)); 568 return array[0]; 569 } 570 } else { 571 return object; 572 } 573 } 574 575 @Override 576 public Object[] getArray(String name) { 577 Serializable object = get(name); 578 if (object == null) { 579 return null; 580 } else if (object instanceof Object[]) { 581 return (Object[]) object; 582 } else { 583 // data migration not done in database, return an array anyway 584 return new Object[] { object }; 585 } 586 } 587 588 @Override 589 public void setSingle(String name, Object value) { 590 put(name, (Serializable) value); 591 } 592 593 @Override 594 public void setArray(String name, Object[] value) { 595 put(name, value); 596 } 597 598 @Override 599 public boolean equals(Object other) { 600 return StateHelper.equalsStrict(this, other); 601 } 602 603}