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