/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jackrabbit.core.state; import org.apache.commons.collections.map.LinkedMap; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.util.EmptyLinkedMap; import org.apache.jackrabbit.spi.Name; import java.util.List; import java.util.HashMap; import java.util.Collections; import java.util.ArrayList; import java.util.Map; /** * <code>ChildNodeEntries</code> represents an insertion-ordered * collection of <code>ChildNodeEntry</code>s that also maintains * the index values of same-name siblings on insertion and removal. */ class ChildNodeEntries implements Cloneable { /** * Insertion-ordered map of entries * (key=NodeId, value=entry) */ private LinkedMap entries; /** * Map used for lookup by name * (key=name, value=either a single entry or a list of sns entries) */ private Map<Name, Object> nameMap; /** * Indicates whether the entries and nameMap are shared with another * ChildNodeEntries instance. */ private boolean shared; ChildNodeEntries() { init(); } ChildNodeEntry get(NodeId id) { return (ChildNodeEntry) entries.get(id); } @SuppressWarnings("unchecked") List<ChildNodeEntry> get(Name nodeName) { Object obj = nameMap.get(nodeName); if (obj == null) { return Collections.emptyList(); } if (obj instanceof List<?>) { // map entry is a list of siblings return Collections.unmodifiableList((List<ChildNodeEntry>) obj); } else { // map entry is a single child node entry return Collections.singletonList((ChildNodeEntry) obj); } } @SuppressWarnings("unchecked") ChildNodeEntry get(Name nodeName, int index) { if (index < 1) { throw new IllegalArgumentException("index is 1-based"); } Object obj = nameMap.get(nodeName); if (obj == null) { return null; } if (obj instanceof List<?>) { // map entry is a list of siblings List<ChildNodeEntry> siblings = (List<ChildNodeEntry>) obj; if (index <= siblings.size()) { return siblings.get(index - 1); } } else { // map entry is a single child node entry if (index == 1) { return (ChildNodeEntry) obj; } } return null; } @SuppressWarnings("unchecked") ChildNodeEntry add(Name nodeName, NodeId id) { ensureModifiable(); List<ChildNodeEntry> siblings = null; int index = 0; Object obj = nameMap.get(nodeName); if (obj != null) { if (obj instanceof List<?>) { // map entry is a list of siblings siblings = (List<ChildNodeEntry>) obj; if (siblings.size() > 0) { // reuse immutable Name instance from 1st same name sibling // in order to help gc conserving memory nodeName = siblings.get(0).getName(); } } else { // map entry is a single child node entry, // convert to siblings list siblings = new ArrayList<ChildNodeEntry>(); siblings.add((ChildNodeEntry) obj); nameMap.put(nodeName, siblings); } index = siblings.size(); } index++; ChildNodeEntry entry = new ChildNodeEntry(nodeName, id, index); if (siblings != null) { siblings.add(entry); } else { nameMap.put(nodeName, entry); } entries.put(id, entry); return entry; } void addAll(List<ChildNodeEntry> entriesList) { for (ChildNodeEntry entry : entriesList) { // delegate to add(Name, String) to maintain consistency add(entry.getName(), entry.getId()); } } // The index may have changed because of changes by another session. Use remove(NodeId id) // instead @Deprecated @SuppressWarnings("unchecked") public ChildNodeEntry remove(Name nodeName, int index) { if (index < 1) { throw new IllegalArgumentException("index is 1-based"); } ensureModifiable(); Object obj = nameMap.get(nodeName); if (obj == null) { return null; } if (obj instanceof ChildNodeEntry) { // map entry is a single child node entry if (index != 1) { return null; } ChildNodeEntry removedEntry = (ChildNodeEntry) obj; nameMap.remove(nodeName); entries.remove(removedEntry.getId()); return removedEntry; } // map entry is a list of siblings List<ChildNodeEntry> siblings = (List<ChildNodeEntry>) obj; if (index > siblings.size()) { return null; } // remove from siblings list ChildNodeEntry removedEntry = siblings.remove(index - 1); // remove from ordered entries map entries.remove(removedEntry.getId()); // update indices of subsequent same-name siblings for (int i = index - 1; i < siblings.size(); i++) { ChildNodeEntry oldEntry = siblings.get(i); ChildNodeEntry newEntry = new ChildNodeEntry(nodeName, oldEntry.getId(), oldEntry.getIndex() - 1); // overwrite old entry with updated entry in siblings list siblings.set(i, newEntry); // overwrite old entry with updated entry in ordered entries map entries.put(newEntry.getId(), newEntry); } // clean up name lookup map if necessary if (siblings.size() == 0) { // no more entries with that name left: // remove from name lookup map as well nameMap.remove(nodeName); } else if (siblings.size() == 1) { // just one entry with that name left: // discard siblings list and update name lookup map accordingly nameMap.put(nodeName, siblings.get(0)); } // we're done return removedEntry; } /** * Removes the child node entry refering to the node with the given id. * * @param id id of node whose entry is to be removed. * @return the removed entry or <code>null</code> if there is no such entry. */ ChildNodeEntry remove(NodeId id) { ChildNodeEntry entry = (ChildNodeEntry) entries.get(id); if (entry != null) { return remove(entry.getName(), entry.getIndex()); } return entry; } /** * Removes the given child node entry. * * @param entry entry to be removed. * @return the removed entry or <code>null</code> if there is no such entry. */ public ChildNodeEntry remove(ChildNodeEntry entry) { return remove(entry.getId()); } /** * Removes all child node entries */ public void removeAll() { init(); } /** * Returns a list of <code>ChildNodeEntry</code>s who do only exist in * <code>this</code> but not in <code>other</code>. * <p> * Note that two entries are considered identical in this context if * they have the same name and uuid, i.e. the index is disregarded * whereas <code>ChildNodeEntry.equals(Object)</code> also compares * the index. * * @param other entries to be removed * @return a new list of those entries that do only exist in * <code>this</code> but not in <code>other</code> */ List<ChildNodeEntry> removeAll(ChildNodeEntries other) { if (entries.isEmpty()) { return Collections.emptyList(); } if (other.isEmpty()) { return list(); } List<ChildNodeEntry> result = new ArrayList<ChildNodeEntry>(); for (Object e : entries.values()) { ChildNodeEntry entry = (ChildNodeEntry) e; ChildNodeEntry otherEntry = other.get(entry.getId()); if (entry == otherEntry) { continue; } if (otherEntry == null || !entry.getName().equals(otherEntry.getName())) { result.add(entry); } } return result; } /** * Returns a list of <code>ChildNodeEntry</code>s who do exist in * <code>this</code> <i>and</i> in <code>other</code>. * <p> * Note that two entries are considered identical in this context if * they have the same name and uuid, i.e. the index is disregarded * whereas <code>ChildNodeEntry.equals(Object)</code> also compares * the index. * * @param other entries to be retained * @return a new list of those entries that do exist in * <code>this</code> <i>and</i> in <code>other</code> */ List<ChildNodeEntry> retainAll(ChildNodeEntries other) { if (entries.isEmpty() || other.isEmpty()) { return Collections.emptyList(); } List<ChildNodeEntry> result = new ArrayList<ChildNodeEntry>(); for (Object e : entries.values()) { ChildNodeEntry entry = (ChildNodeEntry) e; ChildNodeEntry otherEntry = other.get(entry.getId()); if (entry == otherEntry) { result.add(entry); } else if (otherEntry != null && entry.getName().equals(otherEntry.getName())) { result.add(entry); } } return result; } //-----------------------------------------------< unmodifiable List view > public boolean isEmpty() { return entries.isEmpty(); } @SuppressWarnings("unchecked") public List<ChildNodeEntry> list() { return new ArrayList<ChildNodeEntry>(entries.values()); } public List<ChildNodeEntry> getRenamedEntries(ChildNodeEntries that) { List<ChildNodeEntry> renamed = Collections.emptyList(); for (Object e : entries.values()) { ChildNodeEntry entry = (ChildNodeEntry) e; ChildNodeEntry other = that.get(entry.getId()); if (other != null && !entry.getName().equals(other.getName())) { // child node entry with same id but different name exists in // overlaid and this state => renamed entry detected if (renamed.isEmpty()) { renamed = new ArrayList<ChildNodeEntry>(); } renamed.add(entry); } } return renamed; } public int size() { return entries.size(); } //-------------------------------------------< java.lang.Object overrides > public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof ChildNodeEntries) { ChildNodeEntries other = (ChildNodeEntries) obj; return (nameMap.equals(other.nameMap) && entries.equals(other.entries) && shared == other.shared); } return false; } /** * Returns zero to satisfy the Object equals/hashCode contract. * This class is mutable and not meant to be used as a hash key. * * @return always zero * @see Object#hashCode() */ public int hashCode() { return 0; } //----------------------------------------------------< Cloneable support > /** * Returns a shallow copy of this <code>ChildNodeEntries</code> instance; * the entries themselves are not cloned. * * @return a shallow copy of this instance. */ protected Object clone() { try { ChildNodeEntries clone = (ChildNodeEntries) super.clone(); if (nameMap != Collections.EMPTY_MAP) { clone.shared = true; shared = true; } return clone; } catch (CloneNotSupportedException e) { // never happens, this class is cloneable throw new InternalError(); } } //-------------------------------------------------------------< internal > /** * Initializes the name and entries map with unmodifiable empty instances. */ private void init() { nameMap = Collections.emptyMap(); entries = EmptyLinkedMap.INSTANCE; shared = false; } /** * Ensures that the {@link #nameMap} and {@link #entries} map are * modifiable. */ @SuppressWarnings("unchecked") private void ensureModifiable() { if (nameMap == Collections.EMPTY_MAP) { nameMap = new HashMap<Name, Object>(); entries = new LinkedMap(); } else if (shared) { entries = (LinkedMap) entries.clone(); nameMap = new HashMap<Name, Object>(nameMap); for (Map.Entry<Name, Object> entry : nameMap.entrySet()) { Object value = entry.getValue(); if (value instanceof List<?>) { entry.setValue(new ArrayList<ChildNodeEntry>( (List<ChildNodeEntry>) value)); } } shared = false; } } }