| /***************************************************************** |
| * 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.cayenne.util; |
| |
| import java.util.AbstractList; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| import org.apache.cayenne.CayenneRuntimeException; |
| import org.apache.cayenne.ValueHolder; |
| import org.apache.cayenne.reflect.PropertyUtils; |
| |
| /** |
| * A List implementation that would maintain its internal ordering based on some object |
| * numeric "index" property. When objects are added to the list at a certain index, an |
| * "index" property is modified to reflect list order, when objects are removed, their |
| * index property is set to the negative number. |
| * <p> |
| * For performance reasons this implementation does not guarantee that there is no gaps in |
| * the integer ordering sequence (i.e. generally |
| * <code>object.getIndexProperty() != list.indexOf(object)</code>). However it |
| * guarantees the right ordering based on index property. |
| * |
| * @since 1.2 |
| */ |
| public class IndexPropertyList extends AbstractList implements ValueHolder { |
| |
| /** |
| * A default gap maintained between elements index property values. Gaps bigger than 1 |
| * ensure faster and less intrusive additions and removals. |
| */ |
| static final int DEFAULT_GAP = 3; |
| |
| /** |
| * A list used for the actual objects storage. |
| */ |
| protected List list; |
| protected String indexProperty; |
| |
| boolean dirty; |
| Comparator comparator; |
| |
| /** |
| * Creates an empty NumericPropertyOrderedList. |
| */ |
| public IndexPropertyList(String indexProperty) { |
| this(indexProperty, new ArrayList(), false); |
| } |
| |
| /** |
| * Creates a NumericPropertyOrderedList that decorates another list. If the list is |
| * not known to be properly sorted, caller must set <em>sortNeeded</em> to true. |
| * This will result in sorting the original list on first access attempt. |
| */ |
| public IndexPropertyList(String indexProperty, List objects, boolean sortNeeded) { |
| |
| if (indexProperty == null) { |
| throw new IllegalArgumentException("Null sortProperty"); |
| } |
| |
| if (objects == null) { |
| throw new IllegalArgumentException("Null objects list."); |
| } |
| |
| this.indexProperty = indexProperty; |
| this.list = objects; |
| |
| // be lazy - don't sort here, as (a) it may never be needed and (b) a list can be |
| // a Cayenne fault, so resolving it too early is undesirable |
| this.dirty = sortNeeded; |
| } |
| |
| ValueHolder getWrappedValueHolder() { |
| return (list instanceof ValueHolder) ? (ValueHolder) list : null; |
| } |
| |
| public boolean isFault() { |
| ValueHolder h = getWrappedValueHolder(); |
| return (h != null) ? h.isFault() : false; |
| } |
| |
| public Object setValueDirectly(Object value) throws CayenneRuntimeException { |
| ValueHolder h = getWrappedValueHolder(); |
| return h != null ? h.setValueDirectly(value) : null; |
| } |
| |
| public Object setValue(Object value) throws CayenneRuntimeException { |
| ValueHolder h = getWrappedValueHolder(); |
| return h != null ? h.setValue(value) : null; |
| } |
| |
| public Object getValue() throws CayenneRuntimeException { |
| ValueHolder h = getWrappedValueHolder(); |
| return h != null ? h.getValue() : null; |
| } |
| |
| public Object getValueDirectly() throws CayenneRuntimeException { |
| ValueHolder h = getWrappedValueHolder(); |
| return h != null ? h.getValueDirectly() : null; |
| } |
| |
| public void invalidate() { |
| ValueHolder h = getWrappedValueHolder(); |
| if (h != null) { |
| h.invalidate(); |
| } |
| } |
| |
| /** |
| * Changes list state to "dirty" forcing reordering on next access. |
| */ |
| public void touch() { |
| this.dirty = true; |
| } |
| |
| @Override |
| public Object get(int index) { |
| if (dirty) { |
| sort(); |
| } |
| |
| return list.get(index); |
| } |
| |
| @Override |
| public int size() { |
| if (dirty) { |
| sort(); |
| } |
| |
| return list.size(); |
| } |
| |
| @Override |
| public Object set(int index, Object element) { |
| if (dirty) { |
| sort(); |
| } |
| |
| Object removed = list.set(index, element); |
| |
| if (element != null) { |
| int indexValue = (removed != null) |
| ? getIndexValue(removed) |
| : calculateIndexValue(index); |
| |
| setIndexValue(element, indexValue); |
| shift(index + 1, indexValue); |
| } |
| |
| if (removed != null && removed != element) { |
| setIndexValue(removed, -1); |
| } |
| |
| return removed; |
| } |
| |
| @Override |
| public void add(int index, Object element) { |
| if (dirty) { |
| sort(); |
| } |
| |
| list.add(index, element); |
| |
| if (element != null) { |
| int indexValue = calculateIndexValue(index); |
| |
| setIndexValue(element, indexValue); |
| shift(index + 1, indexValue); |
| } |
| } |
| |
| @Override |
| public Object remove(int index) { |
| if (dirty) { |
| sort(); |
| } |
| |
| Object removed = list.remove(index); |
| |
| if (removed != null) { |
| setIndexValue(removed, -1); |
| } |
| |
| return removed; |
| } |
| |
| // ============================================ |
| // ***** Methods to maintain ordering ****** |
| // ============================================ |
| |
| /** |
| * Calculates an index value at the specified list index. Note that using this value |
| * may require a shift of the objects following this index. |
| */ |
| protected int calculateIndexValue(int listIndex) { |
| if (list.isEmpty()) { |
| throw new ArrayIndexOutOfBoundsException(listIndex); |
| } |
| |
| if (list.size() == 1 && listIndex == 0) { |
| return 0; |
| } |
| |
| // handle lists with teo or more elements... |
| |
| // last element |
| if (listIndex == list.size() - 1) { |
| return getIndexValue(get(listIndex - 1)) + DEFAULT_GAP; |
| } |
| |
| int from = (listIndex == 0) ? -1 : getIndexValue(get(listIndex - 1)); |
| int to = getIndexValue(get(listIndex + 1)); |
| return (to - from > 1) ? (to - from) / 2 + from : from + DEFAULT_GAP; |
| } |
| |
| protected int getIndexValue(Object object) { |
| Number n = (Number) PropertyUtils.getProperty(object, indexProperty); |
| if (n == null) { |
| throw new CayenneRuntimeException("Null index property '" |
| + indexProperty |
| + "' for object " |
| + object); |
| } |
| |
| return n.intValue(); |
| } |
| |
| protected void setIndexValue(Object object, int index) { |
| PropertyUtils.setProperty(object, indexProperty, Integer.valueOf(index)); |
| } |
| |
| protected void shift(int startIndex, int afterIndexValue) { |
| int size = size(); |
| for (int i = startIndex; i < size; i++) { |
| Object object = get(i); |
| |
| int indexValue = getIndexValue(object); |
| if (indexValue > afterIndexValue) { |
| break; |
| } |
| |
| int newValue = calculateIndexValue(i); |
| setIndexValue(object, newValue); |
| afterIndexValue = newValue; |
| } |
| } |
| |
| /** |
| * Sorts internal list. |
| */ |
| protected void sort() { |
| if (!dirty) { |
| return; |
| } |
| |
| // do not directly sort Cayenne lists, sort the underlying list instead to avoid a |
| // bunch of additions/removals |
| Collections.sort(unwrapList(), getComparator()); |
| dirty = false; |
| } |
| |
| List unwrapList() { |
| if (list instanceof PersistentObjectList) { |
| return ((PersistentObjectList) list).resolvedObjectList(); |
| } |
| else { |
| return list; |
| } |
| } |
| |
| /** |
| * Returns a property Comaparator, creating it on demand. |
| */ |
| Comparator getComparator() { |
| if (comparator == null) { |
| comparator = new Comparator() { |
| |
| public int compare(Object o1, Object o2) { |
| if ((o1 == null && o2 == null) || o1 == o2) { |
| return 0; |
| } |
| else if (o1 == null) { |
| return -1; |
| } |
| else if (o2 == null) { |
| return 1; |
| } |
| |
| Comparable p1 = (Comparable) PropertyUtils.getProperty( |
| o1, |
| indexProperty); |
| Comparable p2 = (Comparable) PropertyUtils.getProperty( |
| o2, |
| indexProperty); |
| return (p1 == null) ? -1 : p1.compareTo(p2); |
| } |
| }; |
| } |
| return comparator; |
| } |
| } |