├── settings.gradle ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── src ├── main │ └── java │ │ └── com │ │ └── skype │ │ └── research │ │ └── util │ │ ├── unique │ │ ├── Registry.java │ │ └── UniqueSequential.java │ │ ├── primitives │ │ ├── Factory.java │ │ ├── Filter.java │ │ └── Update.java │ │ ├── pool │ │ ├── Pool.java │ │ ├── MultipleEntryPool.java │ │ ├── SingleEntryPool.java │ │ └── AbstractPool.java │ │ ├── model │ │ ├── DataSource.java │ │ ├── RecyclerAdapter.java │ │ ├── Publisher.java │ │ ├── Subscriber.java │ │ └── DataSet.java │ │ ├── projection │ │ ├── Derivative.java │ │ ├── CompositeProjector.java │ │ ├── MutableProjection.java │ │ ├── ProjectorEditor.java │ │ ├── ProjectorBuilder.java │ │ ├── Projector.java │ │ ├── HopefulProjectorImpl.java │ │ └── CompositeProjectorImpl.java │ │ ├── adaptable │ │ ├── FlexibleAdaptable.java │ │ ├── BulkUpdatable.java │ │ ├── ElementObservable.java │ │ ├── Ranger.java │ │ ├── RangedAdaptable.java │ │ ├── ElementObserver.java │ │ ├── Trivial.java │ │ ├── ElementEditor.java │ │ ├── Distance.java │ │ ├── AdaptableFactory.java │ │ ├── RangedAdaptableSkipList.java │ │ ├── Adaptable.java │ │ └── AdaptableSkipList.java │ │ ├── datasets │ │ ├── EmptyDataSet.java │ │ └── FilteredDataSet.java │ │ └── adaptation │ │ └── Adaptation.java └── test │ └── java │ └── com │ └── skype │ └── research │ └── util │ ├── adaptable │ ├── mocks │ │ ├── DivisionFilter.java │ │ ├── DivisibleBy.java │ │ ├── IntValue.java │ │ └── Sample.java │ ├── Validation.java │ ├── Dump.java │ ├── Benchmark.java │ ├── TestCompositeProjector.java │ ├── TestRangedAdaptable.java │ └── TestAdaptable.java │ └── adaptation │ └── TestUpdates.java ├── .gitignore ├── LICENSE.txt ├── gradlew.bat ├── SECURITY.md ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | rootProject.name = 'skype-research-adaptable' 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 22 22:13:46 PDT 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-bin.zip 7 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/unique/Registry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.unique; 8 | 9 | /** 10 | * Through integer index. 11 | */ 12 | public interface Registry { 13 | public T get(int id); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/primitives/Factory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.primitives; 8 | 9 | /** 10 | * Factory of anything. 11 | */ 12 | public interface Factory { 13 | public T create(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/primitives/Filter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.primitives; 8 | 9 | /** 10 | * Generic filter interface. 11 | */ 12 | public interface Filter { 13 | boolean accept(T item); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/pool/Pool.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.pool; 8 | 9 | /** 10 | * Generic pool abstraction. 11 | */ 12 | public interface Pool { 13 | T allocate(); 14 | void recycle(T temp); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/model/DataSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.model; 8 | 9 | /** 10 | * Simplest possible common random-data access interface. 11 | */ 12 | public interface DataSource { 13 | int getCount(); 14 | T getItem(int index); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/projection/Derivative.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.projection; 8 | 9 | /** 10 | * Represents a boolean expression on an int[] argument list. 11 | */ 12 | public interface Derivative { 13 | public boolean accept(int... args); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/FlexibleAdaptable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.projection.MutableProjection; 10 | 11 | /** 12 | * Mix-in: {@link Adaptable} with filter mutation support. 13 | */ 14 | public interface FlexibleAdaptable extends Adaptable, MutableProjection { 15 | } -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/projection/CompositeProjector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.projection; 8 | 9 | /** 10 | * Mix-in composite {@link Projector} interface. 11 | */ 12 | public interface CompositeProjector extends Projector, ProjectorBuilder, ProjectorEditor { 13 | boolean shouldComputeForGroup(int filterIndex); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/BulkUpdatable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | public interface BulkUpdatable { 10 | /** 11 | * Allows to defer state update to honor a bulk add. 12 | */ 13 | void hintBulkOpBegin(); 14 | /** 15 | * Allows to finish state update after a bulk add. 16 | */ 17 | void hintBulkOpCompleted(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/ElementObservable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | /** 10 | * Allows subscriptions for item updates. 11 | */ 12 | public interface ElementObservable { 13 | public void addElementObserver(ElementObserver observer); 14 | public void removeElementObserver(ElementObserver observer); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/projection/MutableProjection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.projection; 8 | 9 | import java.util.BitSet; 10 | 11 | /** 12 | * Exposes a projection editor and a "commit" call to finalize modifications. 13 | */ 14 | public interface MutableProjection { 15 | ProjectorEditor getFilterEditor(); 16 | BitSet refreshFilters(BitSet dirtyMask); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/primitives/Update.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.primitives; 8 | 9 | /** 10 | * A modification operation. 11 | */ 12 | public interface Update { 13 | /** 14 | * Modify the passed object. 15 | * @param element object to modify. 16 | * @return true if the object contents have changed, false otherwise. 17 | */ 18 | public boolean apply(T element); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/Ranger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.primitives.Filter; 10 | 11 | /** 12 | * Aggregation delegate to accommodate custom client logic for {@link RangedAdaptable}. 13 | */ 14 | public interface Ranger extends Filter { 15 | public T getRangeItem(G rangeKey); 16 | public G getRangeKey(T rangeItem); 17 | public Iterable qualify(T element); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/mocks/DivisionFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable.mocks; 8 | 9 | import com.skype.research.util.primitives.Filter; 10 | 11 | /** 12 | * Simplest filter to guarantee a desired percentage of positives. 13 | */ 14 | public class DivisionFilter implements Filter { 15 | final int divisor; 16 | 17 | public DivisionFilter(int divisor) { 18 | this.divisor = divisor; 19 | } 20 | 21 | @Override 22 | public boolean accept(IntValue item) { 23 | return (item.getValue() % divisor) == 0; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/model/RecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.model; 8 | 9 | /** 10 | * Methods specific to RecyclerView.Adapter 11 | */ 12 | public interface RecyclerAdapter { 13 | void notifyItemChanged(int position); 14 | void notifyItemInserted(int position); 15 | void notifyItemRemoved(int position); 16 | void notifyItemMoved(int fromPosition, int toPosition); 17 | void notifyItemRangeChanged(int positionStart, int itemCount); 18 | void notifyItemRangeInserted(int positionStart, int itemCount); 19 | void notifyItemRangeRemoved(int positionStart, int itemCount); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/projection/ProjectorEditor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.projection; 8 | 9 | import com.skype.research.util.primitives.Filter; 10 | 11 | import java.util.BitSet; 12 | 13 | /** 14 | * Allows modifications to a composite {@link Projector} 15 | */ 16 | public interface ProjectorEditor { 17 | BitSet setFilter(int filterIndex, Filter filter); 18 | BitSet setNarrower(int filterIndex, Filter specialization); 19 | BitSet setNarrowerOf(int filterIndex, Filter specialization, Object... preconditions); 20 | BitSet setDerivative(int filterIndex, Derivative derivative); 21 | BitSet setDerivativeOf(int filterIndex, Derivative derivative, Object... arguments); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/model/Publisher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.model; 8 | 9 | /** 10 | * Common data source. Converts filters to data sets. 11 | */ 12 | public interface Publisher { 13 | 14 | /** 15 | * Request a given representation of underlying data. 16 | * @param filterSpec representation specifier. 17 | * @param callback subscriber instance to receive updates. 18 | */ 19 | public void subscribe(F filterSpec, Subscriber callback); 20 | 21 | /** 22 | * Detaches a subscriber. After a subscriber is detached, 23 | * its {@link DataSet} integrity guarantees are no longer held. 24 | * @param callback subscriber instance to stop being notified of updates. 25 | */ 26 | public void unsubscribe(Subscriber callback); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/projection/ProjectorBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.projection; 8 | 9 | import com.skype.research.util.primitives.Filter; 10 | 11 | import java.util.BitSet; 12 | 13 | /** 14 | * Projection builder. 15 | */ 16 | public interface ProjectorBuilder { 17 | int addFilter(Filter filter); 18 | int addNarrower(Filter specialization, int... preconditions); 19 | int addNarrowerOf(Filter specialization, Object... preconditions); 20 | int addDerivative(Derivative derivative, int... arguments); 21 | int addDerivativeOf(Derivative derivative, Object... arguments); 22 | BitSet setShouldComputeForGroup(int filterIndex, boolean shouldCompute); 23 | void freezeFilter(int filterIndex); 24 | int getFilterCount(); 25 | int getHorizon(); 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/mocks/DivisibleBy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable.mocks; 8 | 9 | import com.skype.research.util.primitives.Filter; 10 | 11 | /** 12 | * A simple "divides by" integer filter. 13 | */ 14 | public class DivisibleBy implements Filter { 15 | 16 | final int divisor; 17 | 18 | public DivisibleBy(int divisor) { 19 | this.divisor = divisor; 20 | } 21 | 22 | @Override 23 | public boolean accept(Object item) { 24 | int value; 25 | if (item instanceof Integer) { 26 | value = (Integer) item; 27 | } else if (item instanceof Sample) { 28 | value = ((Sample) item).getValue(); 29 | } else if (item instanceof IntValue) { 30 | value = ((IntValue) item).getValue(); 31 | } else { 32 | return false; 33 | } 34 | return value % divisor == 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/model/Subscriber.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.model; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * DataSet consumer. 13 | */ 14 | public interface Subscriber { 15 | /** 16 | * Callback provided to {@link Publisher#subscribe} 17 | * @param dataSet data set instance to display and browse. 18 | */ 19 | public void setDataSet(DataSet dataSet); 20 | 21 | /** 22 | * Tell the subscriber that the data set contents have been updated, 23 | * without replacing the data set instance. 24 | */ 25 | public void notifyDataSetChanged(); 26 | 27 | /** 28 | * Ancillary subscribers to subscribe for ancillary data sets. 29 | * @return dependent subscribers, or an empty list if N/A. 30 | * @see DataSet#getAncillaryDataSet(int) 31 | */ 32 | List> getAncillarySubscribers(); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/pool/MultipleEntryPool.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.pool; 8 | 9 | import com.skype.research.util.primitives.Factory; 10 | import com.skype.research.util.primitives.Update; 11 | 12 | import java.util.Queue; 13 | 14 | /** 15 | * Pool backed by a (bounded or unbounded) queue. 16 | */ 17 | public class MultipleEntryPool extends AbstractPool { 18 | final Queue queue; 19 | 20 | public MultipleEntryPool(Queue queue, Factory factory) { 21 | this(queue, factory, NO_UPDATE); 22 | } 23 | 24 | public MultipleEntryPool(Queue queue, Factory factory, Update laundry) { 25 | super(factory, laundry); 26 | this.queue = queue; 27 | } 28 | 29 | @Override 30 | protected T tryGet() { 31 | return queue.poll(); 32 | } 33 | 34 | @Override 35 | protected void offer(T temp) { 36 | queue.offer(temp); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Adaptable 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/model/DataSet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.model; 8 | 9 | import java.util.List; 10 | import java.util.RandomAccess; 11 | 12 | /** 13 | * Random-accessible, read-only list contract. 14 | * See android.widget.Adapter for explanation of methods. 15 | */ 16 | @SuppressWarnings("JavadocReference") 17 | public interface DataSet extends DataSource, RandomAccess, Iterable { 18 | // common 19 | public int getCount(); 20 | public T getItem(int index); 21 | // extra logic shared with {@link SlotAllocator} 22 | public DataSet getAncillaryDataSet(int ancillaryIndex); 23 | // reverse lookup 24 | public int indexOf(T element); 25 | // index conversion 26 | public int toAncillary(int ancillaryIndex, int elementIndex, boolean ceiling); 27 | public int fromAncillary(int ancillaryIndex, int elementIndex, boolean ceiling); 28 | // child count 29 | public int getChildCount(T item); 30 | // facility 31 | public List asList(); 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/mocks/IntValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable.mocks; 8 | 9 | /** 10 | * A mutable integer value holder. 11 | */ 12 | public class IntValue implements Comparable { 13 | private int value; 14 | 15 | public IntValue(int value) { 16 | this.setValue(value); 17 | } 18 | 19 | @Override 20 | public int compareTo(IntValue intValue) { 21 | return value == intValue.value ? 0 : value > intValue.value ? 1 : -1; 22 | } 23 | 24 | @Override 25 | public boolean equals(Object o) { 26 | return this == o || o instanceof IntValue && value == ((IntValue) o).value; 27 | 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return value; 33 | } 34 | 35 | public int getValue() { 36 | return value; 37 | } 38 | 39 | public void setValue(int value) { 40 | this.value = value; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return "{" + value + '}'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/pool/SingleEntryPool.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.pool; 8 | 9 | import com.skype.research.util.primitives.Factory; 10 | import com.skype.research.util.primitives.Update; 11 | 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | /** 15 | * Conserve allocations when using a temporary object while still staying thread safe. 16 | * Expecting low contention, stores at most one entry. 17 | */ 18 | public class SingleEntryPool extends AbstractPool implements Pool { 19 | 20 | final AtomicReference container = new AtomicReference(); 21 | 22 | public SingleEntryPool(Factory factory) { 23 | this(factory, NO_UPDATE); 24 | } 25 | 26 | public SingleEntryPool(Factory factory, Update laundry) { 27 | super(factory, laundry); 28 | container.set(factory.create()); 29 | } 30 | 31 | @Override 32 | protected T tryGet() { 33 | return container.getAndSet(null); 34 | } 35 | 36 | @Override 37 | protected void offer(T temp) { 38 | container.compareAndSet(null, temp); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/unique/UniqueSequential.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.unique; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.concurrent.locks.Lock; 12 | import java.util.concurrent.locks.ReentrantLock; 13 | 14 | /** 15 | * No hint needed to create elements. 16 | */ 17 | public class UniqueSequential implements Registry { 18 | final Lock lock = new ReentrantLock(); 19 | final List list = new ArrayList(); 20 | final Registry source; 21 | 22 | public UniqueSequential(final Registry source) { 23 | this.source = source; 24 | } 25 | 26 | @Override 27 | public T get(int id) { 28 | try { 29 | lock.lock(); 30 | T element; 31 | if (growLocked(id)) { 32 | element = list.get(id); 33 | if (element != null) { 34 | return element; 35 | } 36 | } 37 | element = source.get(id); 38 | list.set(id, element); 39 | return element; 40 | } finally { 41 | lock.unlock(); 42 | } 43 | } 44 | 45 | private boolean growLocked(int id) { 46 | boolean withinExisting = true; 47 | while (id >= list.size()) { 48 | list.add(null); 49 | withinExisting = false; 50 | } 51 | return withinExisting; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/mocks/Sample.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable.mocks; 8 | 9 | import java.util.Random; 10 | 11 | /** 12 | * A sample on a relative (base-adjustable) scale. 13 | */ 14 | public class Sample { 15 | static final Random random = new Random(924); 16 | 17 | static int baseLine = 0; 18 | 19 | public static void setBaseLine(int baseLine) { 20 | Sample.baseLine = baseLine; 21 | } 22 | 23 | public boolean boundary; 24 | public int value; 25 | 26 | public Sample() { 27 | boundary = false; 28 | value = random.nextInt(16384); 29 | } 30 | 31 | public Sample(int literal) { 32 | boundary = true; 33 | value = literal; 34 | } 35 | 36 | public int getRange() { 37 | // ensure top header placement 38 | int remainder = getValue() % 1000; 39 | if (remainder < 0) { 40 | remainder += 1000; 41 | } 42 | return getValue() - remainder; 43 | } 44 | 45 | public int getValue() { 46 | return boundary ? value : value - baseLine; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return (boundary ? "!" + (value + baseLine) : "," + value); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/projection/Projector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.projection; 8 | 9 | /** 10 | * A projection component computing block. 11 | * May apply a {@link com.skype.research.util.primitives.Filter}, use precomputed values (below filterIndex) 12 | * or query underlying aggregated data in aggregated container implementations. 13 | */ 14 | public interface Projector { 15 | /** 16 | * Evaluates a condition over an element value and/or conditions evaluated beforehand. 17 | * The method must have no side effects. No object state or argument value may be modified. 18 | * 19 | * @param element element to apply the condition to 20 | * @param filterIndex index of the condition to evaluate 21 | * @param precomputed dependency conditions (index < filterIndex) pre-evaluated on the same element. 22 | * Used for filter chaining (e.g. index=1: "is a man"; index=2: "is tall"; index=3: "is a tall man"). 23 | * Elements of indices equal to or greater than filterIndex should NOT be used, to avoid cyclic dependencies. 24 | * Integers rather than of boolean are used to allow further vector arithmetic (e.g. summarizing, differencing etc.). 25 | * @return true if the element passes the specified condition, false otherwise. 26 | */ 27 | boolean accept(T element, int filterIndex, int[] precomputed); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/datasets/EmptyDataSet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.datasets; 8 | 9 | import com.skype.research.util.model.DataSet; 10 | 11 | import java.util.Collections; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | 15 | /** 16 | * Returns no data at any circumstances, and returns data at no circumstances. 17 | */ 18 | public class EmptyDataSet implements DataSet { 19 | private static final DataSet instance = new EmptyDataSet(); 20 | 21 | @Override 22 | public int getCount() { 23 | return 0; 24 | } 25 | 26 | @Override 27 | public T getItem(int index) { 28 | throw new IndexOutOfBoundsException(); 29 | } 30 | 31 | @Override 32 | public DataSet getAncillaryDataSet(int ancillaryIndex) { 33 | return this; 34 | } 35 | 36 | @Override 37 | public int indexOf(T element) { 38 | return -1; // not found 39 | } 40 | 41 | @Override 42 | public int toAncillary(int ancillaryIndex, int elementIndex, boolean ceiling) { 43 | return -1; // not found 44 | } 45 | 46 | @Override 47 | public int fromAncillary(int ancillaryIndex, int elementIndex, boolean ceiling) { 48 | return -1; // not found 49 | } 50 | 51 | @Override 52 | public int getChildCount(T item) { 53 | return 0; 54 | } 55 | 56 | @Override 57 | public List asList() { 58 | return Collections.emptyList(); 59 | } 60 | 61 | @Override 62 | public Iterator iterator() { 63 | return Collections.emptySet().iterator(); 64 | } 65 | 66 | public static DataSet emptyDataSet() { 67 | //noinspection unchecked 68 | return instance; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/pool/AbstractPool.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.pool; 8 | 9 | import com.skype.research.util.primitives.Factory; 10 | import com.skype.research.util.primitives.Update; 11 | 12 | import java.util.Collection; 13 | 14 | /** 15 | * Abstract pool implementation. 16 | */ 17 | public abstract class AbstractPool implements Pool { 18 | static final Update NO_UPDATE = new Update() { 19 | @Override 20 | public boolean apply(Object element) { 21 | return false; 22 | } 23 | }; 24 | 25 | final Factory factory; 26 | final Update laundry; 27 | 28 | public AbstractPool(Factory factory) { 29 | this(factory, NO_UPDATE); 30 | } 31 | 32 | public AbstractPool(Factory factory, Update laundry) { 33 | this.factory = factory; 34 | this.laundry = laundry; 35 | } 36 | 37 | @Override 38 | public final T allocate() { 39 | T candidate = tryGet(); 40 | return candidate != null ? candidate : factory.create(); 41 | } 42 | 43 | protected abstract T tryGet(); 44 | 45 | @Override 46 | public final void recycle(T temp) { 47 | laundry.apply(temp); 48 | offer(temp); 49 | } 50 | 51 | protected abstract void offer(T temp); 52 | 53 | public static void drain(Pool pool, Collection container) { 54 | for (T element : container) { 55 | pool.recycle(element); 56 | } 57 | container.clear(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/RangedAdaptable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | /** 10 | * Represents an aggregated adaptable. 11 | */ 12 | public interface RangedAdaptable extends FlexibleAdaptable { 13 | /** 14 | * Get the index of the filter that distinguishes range headings from individual items. 15 | * @return "head" item filter index 16 | */ 17 | public int getHeadItemFilterIndex(); 18 | 19 | /** 20 | * Recomputes group membership throughout the container. 21 | */ 22 | public void updateRangeClassification(); 23 | 24 | /** 25 | * Recomputes group population throughout the container (assuming that group membership has not changed). 26 | */ 27 | public void updateRangePopulation(); 28 | 29 | /** 30 | * Locate an aggregate item and return its child count. 31 | * @param sourceElementIndex index of the item within the lookup selection 32 | * @param sourceFilterIndex selection index to look up the item in 33 | * @param targetFilterIndex selection index to count children in 34 | * @return child count of the item, or 0 if the item is not found or not aggregate 35 | */ 36 | public int getChildCount(int sourceElementIndex, int sourceFilterIndex, int targetFilterIndex); 37 | 38 | /** 39 | * Get the child count of an item. 40 | * @param item item to look up for 41 | * @param targetFilterIndex selection index to count children in 42 | * @return child count of the item, or 0 if the item is not found or not aggregate 43 | */ 44 | public int getChildCount(T item, int targetFilterIndex); 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/Validation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import junit.framework.Assert; 10 | 11 | import java.util.Iterator; 12 | 13 | /** 14 | * Through access to package methods. 15 | */ 16 | public class Validation { 17 | public static void validateIntegrity(FlexibleAdaptable asl) { 18 | ((AdaptableSkipList) asl).validateIntegrity(); 19 | } 20 | 21 | public static void validateIterators(FlexibleAdaptable adaptable) { 22 | for (int filterIndex = 0; filterIndex < adaptable.getFilterCount(); ++filterIndex) { 23 | Iterator iterator = adaptable.iterator(filterIndex); 24 | for (int elementIndex = 0; elementIndex < adaptable.size(filterIndex); ++ elementIndex) { 25 | Assert.assertSame(adaptable.get(filterIndex, elementIndex), iterator.next()); 26 | } 27 | } 28 | } 29 | 30 | public static void validateIterators(AdaptableSkipList adaptable) { 31 | for (int filterIndex = 0; filterIndex < adaptable.getFilterCount(); ++filterIndex) { 32 | Iterator cherryIterator = adaptable.cherryIterator(filterIndex); 33 | Iterator ladderIterator = adaptable.ladderIterator(filterIndex); 34 | Iterator walkerIterator = adaptable.walkerIterator(filterIndex); 35 | for (int elementIndex = 0; elementIndex < adaptable.size(filterIndex); ++ elementIndex) { 36 | final T expected = adaptable.get(filterIndex, elementIndex); 37 | Assert.assertSame("cherryIterator", expected, cherryIterator.next()); 38 | Assert.assertSame("walkerIterator", expected, walkerIterator.next()); 39 | Assert.assertSame("ladderIterator", expected, ladderIterator.next()); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/ElementObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.primitives.Update; 10 | 11 | /** 12 | * Incremental change observer. 13 | */ 14 | public interface ElementObserver { 15 | /** 16 | * Represents a single {@link Adaptable} state update. 17 | * 18 | * The format is chosen to minimize the amount of computations within the {@link ElementObservable}. 19 | * For instance, "deltaCount" may contain the same value in the cases of element addition or removal, 20 | * however addition will result in deltaSign=1 and removal in deltaSign=-1. The actual selection size 21 | * increment for selection i equals deltaSign * deltaCount[i]. No assumptions about the 22 | * value or sign of "deltaSign" or "deltaCount[i]" alone should be made. 23 | * 24 | * DeltaSign=0 indicates an update that does not, by definition, affect selection membership, 25 | * such as one resulting from {@link ElementEditor#updateInPlace(Object, Update)}. 26 | * 27 | * All array parameters have their length equal to the filter count (aka selection count). 28 | * 29 | * @param element element being updated, added or removed. null indicates a batch update ("all changed"). 30 | * @param position starting positions of the update within filtered selections. 31 | * @param changeEstimate represents affected selections. if a selection is changed, the respective element is nonzero, 32 | * otherwise zero. No other assumptions about the returned values should be made. 33 | * @param deltaSign represents the "direction" of the update. 34 | * @param deltaCount represents the "absolute value" of the update. 35 | */ 36 | public void onElementUpdated(T element, int[] position, int[] changeEstimate, int deltaSign, int[] deltaCount); 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/Dump.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import java.io.PrintStream; 10 | import java.io.PrintWriter; 11 | 12 | /** 13 | * Pretty-print an {@link AdaptableSkipList} container 14 | */ 15 | public class Dump { 16 | final PrintWriter printWriter; 17 | 18 | public Dump(PrintStream printStream) { 19 | this(new PrintWriter(printStream)); 20 | } 21 | 22 | public Dump(PrintWriter printWriter) { 23 | this.printWriter = printWriter; 24 | } 25 | 26 | public void dump(AdaptableSkipList adaptable) { 27 | for (int i = 0; i < adaptable.levelCount; ++i) { 28 | if (i < adaptable.orbitLevel && adaptable.absMinNode.nodes[i] == null) { 29 | continue; // collapse "atmosphere" levels 30 | } 31 | printWriter.printf("Level %d: ", i); 32 | dump(adaptable, i); 33 | printWriter.println(); 34 | } 35 | printWriter.flush(); 36 | } 37 | 38 | private void dump(AdaptableSkipList adaptable, int level) { 39 | AdaptableSkipList.Node node = adaptable.absMinNode; 40 | while (node != null) { 41 | if (node.element != null) { 42 | printWriter.print(node.element); 43 | } 44 | printWriter.print('+'); 45 | dump(node.distances[level]); 46 | printWriter.print("-> "); 47 | node = node.nodes[level]; 48 | } 49 | } 50 | 51 | private void dump(int[] distance) { 52 | for (int component : distance) { 53 | printWriter.print(component); 54 | printWriter.print('/'); 55 | } 56 | } 57 | 58 | public static void validateIntegrity(FlexibleAdaptable asl) { 59 | ((AdaptableSkipList) asl).validateIntegrity(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/Trivial.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.primitives.Filter; 10 | import com.skype.research.util.primitives.Update; 11 | 12 | import java.util.Comparator; 13 | 14 | /** 15 | * Simple Adaptable-related constructs. 16 | */ 17 | public class Trivial { 18 | public static final int[] NO_ANCILLARY_SLOTS = new int[0]; 19 | 20 | static final Comparator NATURAL_ORDER = new Comparator() { 21 | @Override 22 | public int compare(Object lhs, Object rhs) { 23 | if (lhs instanceof Comparable) { 24 | //noinspection unchecked 25 | return ((Comparable) lhs).compareTo(rhs); 26 | } 27 | throw new ClassCastException(lhs.getClass().getName()); 28 | } 29 | }; 30 | 31 | static final Filter ALL = new Filter() { 32 | @Override 33 | public boolean accept(Object item) { 34 | return true; 35 | } 36 | }; 37 | 38 | static final Filter NONE = new Filter() { 39 | @Override 40 | public boolean accept(Object item) { 41 | return false; 42 | } 43 | }; 44 | 45 | static final Update SUGGEST = new Update() { 46 | @Override 47 | public boolean apply(Object element) { 48 | return false; 49 | } 50 | }; 51 | 52 | static final Update REFRESH = new Update() { 53 | @Override 54 | public boolean apply(Object element) { 55 | return true; 56 | } 57 | }; 58 | 59 | public static Comparator naturalOrder() { 60 | return NATURAL_ORDER; 61 | } 62 | 63 | public static Filter universeFilter() { 64 | return ALL; 65 | } 66 | 67 | public static Filter placeholder() { 68 | return NONE; 69 | } 70 | 71 | public static Update suggest() { 72 | return SUGGEST; 73 | } 74 | 75 | public static Update refresh() { 76 | return REFRESH; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/ElementEditor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.primitives.Update; 10 | 11 | /** 12 | * Add-on interface to {@link Adaptable} to define incremental, "green" update operations. 13 | * 14 | * The "oldValue" parameters below should only be used for navigation within the ordered storage. 15 | * Neither reference identity nor strict value equality are required. The only requirement is that 16 | * the locator value must be neither greater nor less than the one to apply the {@link Update} to. 17 | */ 18 | public interface ElementEditor { 19 | 20 | // Note: generally speaking, we can use Comparable as object locator. 21 | // However, it's not completely clear how to ensure consistency with {@link Adaptable} comparator. 22 | // One possible solution could involve wrapping the Comparator together with a sample value into a 23 | // single Comparable closure. To allow sample types wider than T, we can add the exact comparator 24 | // type parameter ("locator") as a type parameter of the {@link AdaptableSkipList} itself, instead 25 | // of using a wildcard (? super T). 26 | // 27 | // Note on Note: please do not delete this comment until the team thinks twice on the idea. 28 | 29 | /** 30 | * Update an element in place, assuming element ordering and filtered "views" are unchanged. 31 | * @param oldValue existing element value. 32 | * @param modification modification operation to apply. 33 | * @return true if the modification operation has actually changed anything, false otherwise. 34 | */ 35 | public boolean updateInPlace(T oldValue, Update modification); 36 | 37 | /** 38 | * Update an element in place, assuming element ordering unchanged but filtering potentially affected. 39 | * @param oldValue existing element value. 40 | * @param modification modification operation to apply. 41 | * @return true if the modification operation has actually changed anything, false otherwise. 42 | */ 43 | public boolean updateFilters(T oldValue, Update modification); 44 | 45 | /** 46 | * Update an element in place, assuming both element ordering and filtering potentially affected. 47 | * @param oldValue existing element value. 48 | * @param modification modification operation to apply. 49 | * @return true if the modification operation has actually changed anything, false otherwise. 50 | */ 51 | public boolean updateReorder(T oldValue, Update modification); 52 | } 53 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/datasets/FilteredDataSet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.datasets; 8 | 9 | import com.skype.research.util.adaptable.Adaptable; 10 | import com.skype.research.util.adaptable.RangedAdaptable; 11 | import com.skype.research.util.model.DataSet; 12 | 13 | import java.util.AbstractList; 14 | import java.util.Iterator; 15 | import java.util.List; 16 | 17 | /** 18 | * A data set connected to an Adaptable, with optional ancillaries connected to the same Adaptable. 19 | */ 20 | public class FilteredDataSet extends AbstractList implements DataSet { 21 | private int filterIndex; 22 | private int[] ancillaries; 23 | private Adaptable adaptable; 24 | private RangedAdaptable ranged; 25 | 26 | public FilteredDataSet(Adaptable adaptable, int filterIndex) { 27 | this.adaptable = adaptable; 28 | this.ranged = adaptable instanceof RangedAdaptable ? (RangedAdaptable) adaptable : null; 29 | this.filterIndex = filterIndex; 30 | } 31 | 32 | public void setAncillaries(int... ancillaries) { 33 | this.ancillaries = ancillaries; 34 | } 35 | 36 | @Override 37 | public int getCount() { 38 | return adaptable.size(getFilterIndex()); 39 | } 40 | 41 | @Override 42 | public T getItem(int index) { 43 | return adaptable.get(getFilterIndex(), index); 44 | } 45 | 46 | @Override 47 | public DataSet getAncillaryDataSet(int ancillaryIndex) { 48 | return hasAncillaries() ? null : dataSet(getAncillaryFilterIndex(ancillaryIndex)); 49 | } 50 | 51 | @Override 52 | public int indexOf(Object element) { 53 | //noinspection unchecked 54 | return adaptable.indexOf(getFilterIndex(), (T) element); 55 | } 56 | 57 | @Override 58 | public int toAncillary(int ancillaryIndex, int elementIndex, boolean ceiling) { 59 | return adaptable.convertIndex(elementIndex, getFilterIndex(), getAncillaryFilterIndex(ancillaryIndex), ceiling); 60 | } 61 | 62 | @Override 63 | public int fromAncillary(int ancillaryIndex, int elementIndex, boolean ceiling) { 64 | return adaptable.convertIndex(elementIndex, getAncillaryFilterIndex(ancillaryIndex), getFilterIndex(), ceiling); 65 | } 66 | 67 | @Override 68 | public int getChildCount(T item) { 69 | return ranged == null ? 0 : ranged.getChildCount(item, getFilterIndex()); 70 | } 71 | 72 | @Override 73 | public List asList() { 74 | return this; 75 | } 76 | 77 | protected int getFilterIndex() { 78 | return filterIndex; 79 | } 80 | 81 | protected boolean hasAncillaries() { 82 | return ancillaries == null; 83 | } 84 | 85 | protected int getAncillaryFilterIndex(int ancillaryIndex) { 86 | return ancillaries[ancillaryIndex]; 87 | } 88 | 89 | protected DataSet dataSet(int filterIndex) { 90 | return new FilteredDataSet(adaptable, filterIndex); 91 | } 92 | 93 | @Override 94 | public T get(int location) { 95 | return getItem(location); 96 | } 97 | 98 | @Override 99 | public Iterator iterator() { 100 | return adaptable.iterator(getFilterIndex()); 101 | } 102 | 103 | @Override 104 | public int size() { 105 | return getCount(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/Benchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.adaptable.mocks.DivisionFilter; 10 | import com.skype.research.util.adaptable.mocks.IntValue; 11 | import com.skype.research.util.projection.CompositeProjectorImpl; 12 | 13 | import java.io.PrintStream; 14 | import java.util.Iterator; 15 | import java.util.Random; 16 | 17 | /** 18 | * Benchmark {@link AdaptableSkipList} and {@link RangedAdaptableSkipList}. 19 | */ 20 | public class Benchmark { 21 | 22 | public static void main(String[] args) { 23 | final PrintStream out = System.err; 24 | AdaptableSkipList integers = createDichotomy(); 25 | final int filterCount = integers.getFilterCount(); 26 | 27 | final int VALUE_COUNT = 1 << 16; 28 | 29 | Random random = new Random(0); 30 | for (int i = 0; i < VALUE_COUNT; ++i) { 31 | integers.add(new IntValue(random.nextInt())); 32 | } 33 | int fi = filterCount; 34 | while (fi > 0) { 35 | --fi; 36 | out.println("Divisor number " + fi + " value " + (1 << fi)); 37 | runFullBanchmark(out, integers, fi, false); 38 | runFullBanchmark(out, integers, fi, true); 39 | } 40 | } 41 | 42 | protected static void runFullBanchmark(PrintStream out, AdaptableSkipList integers, int fi, boolean smart) { 43 | long time = System.currentTimeMillis(); 44 | final int CYCLE_COUNT = 1 << 10; 45 | for (int i = 0; i < CYCLE_COUNT; ++i) { 46 | Iterator itr = smart ? integers.ladderIterator(fi) : integers.walkerIterator(fi); 47 | while (itr.hasNext()) { 48 | itr.next().getValue(); 49 | } 50 | } 51 | long done = System.currentTimeMillis(); 52 | out.println((smart ? "Ladder" : "Walker") + ": done in " + (done - time) + " ms"); 53 | } 54 | 55 | protected static AdaptableSkipList createDichotomy() { 56 | CompositeProjectorImpl projector = new CompositeProjectorImpl(); 57 | projector.addFilter(Trivial.universeFilter()); 58 | int fi = 1; 59 | projector.addFilter(new DivisionFilter(1 << (fi++))); 60 | projector.addFilter(new DivisionFilter(1 << (fi++))); 61 | projector.addFilter(new DivisionFilter(1 << (fi++))); 62 | projector.addFilter(new DivisionFilter(1 << (fi++))); 63 | projector.addFilter(new DivisionFilter(1 << (fi++))); 64 | projector.addFilter(new DivisionFilter(1 << (fi++))); 65 | projector.addFilter(new DivisionFilter(1 << (fi++))); 66 | projector.addFilter(new DivisionFilter(1 << (fi++))); 67 | return new AdaptableSkipList(4, 8, 0, projector); 68 | } 69 | 70 | /* 71 | Divisor number 8 value 256 72 | Walker: done in 2813 ms 73 | Ladder: done in 208 ms 74 | Divisor number 7 value 128 75 | Walker: done in 2630 ms 76 | Ladder: done in 518 ms 77 | Divisor number 6 value 64 78 | Walker: done in 2645 ms 79 | Ladder: done in 845 ms 80 | Divisor number 5 value 32 81 | Walker: done in 2643 ms 82 | Ladder: done in 1232 ms 83 | Divisor number 4 value 16 84 | Walker: done in 2623 ms 85 | Ladder: done in 1705 ms 86 | Divisor number 3 value 8 87 | Walker: done in 2626 ms 88 | Ladder: done in 2183 ms 89 | Divisor number 2 value 4 90 | Walker: done in 2606 ms 91 | Ladder: done in 2451 ms 92 | Divisor number 1 value 2 93 | Walker: done in 2565 ms 94 | Ladder: done in 2678 ms 95 | Divisor number 0 value 1 96 | Walker: done in 2453 ms 97 | Ladder: done in 2663 ms 98 | 99 | Process finished with exit code 0 100 | */ 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/Distance.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.projection.Projector; 10 | 11 | import java.util.BitSet; 12 | 13 | /** 14 | * Operations on vectors in the filtered space. 15 | */ 16 | class Distance { 17 | private Distance() {} 18 | 19 | static int[] toArray(BitSet mask) { 20 | int[] array = new int[mask.cardinality()]; 21 | for (int ai = 0, fi = mask.nextSetBit(0); fi >= 0; fi = mask.nextSetBit(fi + 1)) { 22 | array[ai++] = fi; 23 | } 24 | return array; 25 | } 26 | 27 | static int[] project(int[] target, T element, Projector projector) { 28 | return project(target, element, target.length, projector); 29 | } 30 | 31 | static int[] project(int[] target, T element, int horizon, Projector projector) { 32 | for (int filterIndex = 0; filterIndex < horizon; ++filterIndex) { 33 | target[filterIndex] = projector.accept(element, filterIndex, target) ? 1 : 0; 34 | } 35 | return target; 36 | } 37 | 38 | static int[] project(int[] target, T element, int[] indices, Projector projector) { 39 | for (int fi : indices) { 40 | target[fi] = projector.accept(element, fi, target) ? 1 : 0; 41 | } 42 | return target; 43 | } 44 | 45 | static int[] add(int[] target, int[] source) { 46 | return add(target, source, target.length); 47 | } 48 | 49 | static int[] add(int[] target, int[] source, int horizon) { 50 | for (int filterIndex = 0; filterIndex < horizon; ++filterIndex) { 51 | target[filterIndex] += source[filterIndex]; 52 | } 53 | return target; 54 | } 55 | 56 | static int[] sub(int[] target, int[] source) { 57 | return sub(target, source, target.length); 58 | } 59 | 60 | static int[] sub(int[] target, int[] source, int horizon) { 61 | for (int filterIndex = 0; filterIndex < horizon; ++filterIndex) { 62 | target[filterIndex] -= source[filterIndex]; 63 | } 64 | return target; 65 | } 66 | 67 | static int[] add(int[] target, int sourceMultiplier, int[] source) { 68 | return add(target, sourceMultiplier, source, target.length); 69 | } 70 | 71 | static int[] add(int[] target, int sourceMultiplier, int[] source, int horizon) { 72 | if (sourceMultiplier != 0) { 73 | for (int filterIndex = 0; filterIndex < horizon; ++filterIndex) { 74 | target[filterIndex] += sourceMultiplier * source[filterIndex]; 75 | } 76 | } 77 | return target; 78 | } 79 | 80 | static int[] add(int[] target, int[] source, int[] indices) { 81 | for (int fi : indices) { 82 | target[fi] += source[fi]; 83 | } 84 | return target; 85 | } 86 | 87 | static int[] sub(int[] target, int[] source, int[] indices) { 88 | for (int fi : indices) { 89 | target[fi] -= source[fi]; 90 | } 91 | return target; 92 | } 93 | 94 | static int[] add(int[] target, int sourceMultiplier, int[] source, int[] indices) { 95 | if (sourceMultiplier != 0) { 96 | for (int fi : indices) { 97 | target[fi] += sourceMultiplier * source[fi]; 98 | } 99 | } 100 | return target; 101 | } 102 | 103 | static int[] set(int[] target, int[] source) { 104 | return set(target, source, target.length); 105 | } 106 | 107 | static int[] set(int[] target, int[] source, int horizon) { 108 | System.arraycopy(source, 0, target, 0, horizon); 109 | return target; 110 | } 111 | 112 | static int[] set(int[] target, int[] source, int[] indices) { 113 | for (int fi : indices) { 114 | target[fi] = source[fi]; 115 | } 116 | return target; 117 | } 118 | 119 | static int[] bis(int[] target, BitSet indices) { 120 | return fill(target, 1, indices); 121 | } 122 | 123 | static int[] bic(int[] target, BitSet indices) { 124 | return fill(target, 0, indices); 125 | } 126 | 127 | static int[] fill(int[] target, int value, BitSet indices) { 128 | for (int fi = indices.nextSetBit(0); fi >= 0; fi = indices.nextSetBit(fi + 1)) { 129 | target[fi] = value; 130 | } 131 | return target; 132 | } 133 | 134 | static boolean isZero(int[] vector, int horizon) { 135 | for (int i = 0; i < horizon; i++) { 136 | if (vector[i] != 0) 137 | return false; 138 | } 139 | return true; 140 | } 141 | 142 | static boolean isZero(int[] vector) { 143 | return isZero(vector, vector.length); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/AdaptableFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.primitives.Factory; 10 | import com.skype.research.util.primitives.Filter; 11 | import com.skype.research.util.projection.CompositeProjectorImpl; 12 | import com.skype.research.util.projection.Derivative; 13 | import com.skype.research.util.projection.ProjectorBuilder; 14 | import com.skype.research.util.projection.HopefulProjectorImpl; 15 | 16 | import java.util.BitSet; 17 | import java.util.Comparator; 18 | 19 | /** 20 | * Encapsulates pre-instantiation Adaptable configuration. 21 | */ 22 | public class AdaptableFactory implements ProjectorBuilder, Factory> { 23 | 24 | // skeletal dependencies 25 | final CompositeProjectorImpl projector = new HopefulProjectorImpl(); 26 | final boolean withRanging; 27 | 28 | // replaceable dependencies 29 | Comparator comparator = Trivial.naturalOrder(); 30 | final int universeFilter = 0; 31 | final int headItemFilter = 1; 32 | 33 | int denominator = 3; 34 | int levelCount = 15; 35 | 36 | private boolean allowDuplicates; 37 | private boolean broadcastOldValue; 38 | 39 | public AdaptableFactory() { 40 | this(false); 41 | } 42 | 43 | public AdaptableFactory(boolean withRanging) { 44 | this(Trivial.universeFilter(), withRanging); 45 | } 46 | 47 | public AdaptableFactory(Filter customUniverseFilter, boolean withRanging) { 48 | projector.addFilter(customUniverseFilter); 49 | projector.freezeFilter(universeFilter); 50 | this.withRanging = withRanging; 51 | if (withRanging) { 52 | projector.addFilter(Trivial.placeholder()); 53 | } 54 | } 55 | 56 | public void setDenominator(int denominator) { 57 | this.denominator = denominator; 58 | } 59 | 60 | public void setLevelCount(int levelCount) { 61 | this.levelCount = levelCount; 62 | } 63 | 64 | public void setComparator(Comparator comparator) { 65 | this.comparator = comparator; 66 | } 67 | 68 | public void setAllowDuplicates(boolean allowDuplicates) { 69 | this.allowDuplicates = allowDuplicates; 70 | } 71 | 72 | public void setBroadcastOldValue(boolean broadcastOldValue) { 73 | this.broadcastOldValue = broadcastOldValue; 74 | } 75 | 76 | @Override 77 | public int addFilter(Filter filter) { 78 | return projector.addFilter(filter); 79 | } 80 | 81 | @Override 82 | public int addNarrower(Filter specialization, int... preconditions) { 83 | return projector.addNarrower(specialization, preconditions); 84 | } 85 | 86 | @Override 87 | public int addNarrowerOf(Filter specialization, Object... preconditions) { 88 | return projector.addNarrowerOf(specialization, preconditions); 89 | } 90 | 91 | @Override 92 | public int addDerivative(Derivative derivative, int... arguments) { 93 | return projector.addDerivative(derivative, arguments); 94 | } 95 | 96 | @Override 97 | public int addDerivativeOf(Derivative derivative, Object... arguments) { 98 | return projector.addDerivativeOf(derivative, arguments); 99 | } 100 | 101 | @Override 102 | public BitSet setShouldComputeForGroup(int filterIndex, boolean shouldCompute) { 103 | return projector.setShouldComputeForGroup(filterIndex, shouldCompute); 104 | } 105 | 106 | @Override 107 | public void freezeFilter(int filterIndex) { 108 | projector.freezeFilter(filterIndex); 109 | } 110 | 111 | @Override 112 | public int getFilterCount() { 113 | return projector.getFilterCount(); 114 | } 115 | 116 | @Override 117 | public int getHorizon() { 118 | return projector.getHorizon(); 119 | } 120 | 121 | public FlexibleAdaptable create() { 122 | if (withRanging) { 123 | throw new IllegalStateException("Ranger expected"); 124 | } 125 | projector.freeze(); 126 | AdaptableSkipList adaptable = new AdaptableSkipList(levelCount, denominator, comparator, universeFilter, projector); 127 | adaptable.setBroadcastOldValue(broadcastOldValue); 128 | adaptable.setAllowDuplicates(allowDuplicates); 129 | return adaptable; 130 | } 131 | 132 | public RangedAdaptable create(Ranger ranger) { 133 | if (!withRanging) { 134 | throw new IllegalStateException("Ranger not expected"); 135 | } 136 | projector.setFilter(headItemFilter, ranger); 137 | projector.freezeFilter(headItemFilter); 138 | projector.freeze(); 139 | RangedAdaptableSkipList adaptable = new RangedAdaptableSkipList(ranger, levelCount, denominator, comparator, universeFilter, headItemFilter, projector); 140 | // adaptable.setBroadcastOldValue(broadcastOldValue) is not needed here as it's automatic for RASL 141 | adaptable.setAllowDuplicates(allowDuplicates); 142 | return adaptable; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adaptable 2 | ## What's it? 3 | **Adaptable** is a highly conservative representation of long-living, often-modified, naturally ordered sequences of items. You can think of it as of a small in-memory cache or database. It fills the gap between `List` and `SortedSet` by allowing _O(log N)_ access **_both by item value and by numeric item index_**. 4 | 5 | Multiple predicate-based selections of the stored data can be exposed and randomly accessed by element index, allowing value-to-index, index-to-value and cross index-to-index lookup. 6 | 7 | Element-wise updates (unlike those in `SortedList`, which is array-backed) are _O(log N)_; further optimization is possible by allowing updates that knowingly leave item ordering and/or predicate satisfaction intact. 8 | 9 | Thanks to probabilistic partitioning, the underlying storage never needs to be rebalanced. Update notifications are selection-wise and range-wise in a way expected by [RecyclerView.Adapter](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html). 10 | 11 | Bonus features include 12 | 13 | + predicate chaining and custom dependencies between predicates (without cycles); 14 | + comparison order and selection predicate update on the fly; 15 | + optional support of duplicate items, as in a `Multiset`; 16 | + item grouping into ranges with automatically inserted range headers and cached range length (as in _"there are 3 clients with last names starting with **A**"_). 17 | 18 | ## Examples 19 | 20 | Let's assume 21 | 22 | public interface Task { 23 | public int id(); // primary index for tie breaking 24 | public String name(); 25 | public String description(); 26 | public float importance(); 27 | public Person assignee(); 28 | } 29 | 30 | Then the following ordering and filters can be defined (in Java 8 syntax): 31 | 32 | Comparator importantFirst = Comparator.comparing(Task.importance).reversed().thenComparing(Task.id); 33 | Filter isWellDefined = (item) -> item.description().length() > item.name().length(); 34 | Filter isBurning = (item) -> item.importance() >= Importance.BURNING.value(); 35 | Filter isMine = (item) -> item.assignee().isMe(); 36 | 37 | Based on the ordering and filters, we can create an Adaptable task list: 38 | 39 | AdaptableFactory factory = new AdaptableFactory<>(); 40 | factory.setComparator(importantFirst); 41 | final int definedFilterId = factory.addFilter(isWellDefined); 42 | final int burningFilterId = factory.addFilter(isBurning); 43 | final int myTasksFilterId = factory.addFilter(isMine); 44 | Adaptable adaptable = factory.create(); 45 | final int allPassFilterId = adaptabe.getUniverseFilterId(); // 0 46 | 47 | Then we can run the following operations on the list: 48 | 49 | adaptable.add(wakeUpTask); // will appear in universe and matching selections 50 | adaptable.add(wakeUpTask); // no-op, because we haven't allowed duplicates 51 | adaptable.updateInPlace(wakeUpTask, (task) -> { 52 | task.setDescription(task.description() + "\nDon't forget to tie your shoes!"); 53 | return true; 54 | }); // will update the item in the selections it is already present in 55 | adaptable.updateFilters(wakeUpTask, (task) -> { 56 | boolean changed = task.assignee() != Person.MOM; 57 | task.setAssignee(Person.MOM); 58 | return changed; 59 | }); // will update the item and recalculate the predicates 60 | adaptable.updateReorder(wakeUpTask, (task) -> { 61 | final float ultimate = Float.POSITIVE_INFINITY; 62 | boolean changed = task.importance() != ultimate; 63 | task.setImportance(ultimate); 64 | return changed; 65 | }); // will fully recalculate the item placement 66 | 67 | // find the index of the first task assigned to "me", e.g. to scroll to it: 68 | int firstMyTask = adaptable.convertIndex(0, myTasksFilterId, allPassFilterId); 69 | 70 | For incremental update broadcasting, see the `ElementObserver` interface, `Adaptation` and `TestUpdates`. 71 | 72 | For more sophisticated examples, see the unit test package. 73 | 74 | ## Dependencies 75 | 76 | Though originally developed for an Android environment, **Adaptable** is pure Java 1.7. A Gradle build script is provided but you can use a build system of your choice. 77 | 78 | ## Contributing 79 | 80 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. 81 | 82 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 83 | 84 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 85 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/projection/HopefulProjectorImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.projection; 8 | 9 | import com.skype.research.util.adaptable.Trivial; 10 | import com.skype.research.util.primitives.Filter; 11 | 12 | import java.util.ArrayList; 13 | import java.util.BitSet; 14 | import java.util.Collections; 15 | import java.util.IdentityHashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * Distinguishes between hopeful and hopeless filters. 21 | * Also allows restoration of the original filter set. 22 | */ 23 | public class HopefulProjectorImpl extends CompositeProjectorImpl { 24 | 25 | // optimization: filters that pass nothing 26 | final BitSet hopefulFilterSet = new BitSet(); 27 | private int lastHopefulFilter = -1; 28 | 29 | public boolean isFilterHopeful(Filter specialization) { 30 | return specialization != Trivial.placeholder(); 31 | } 32 | 33 | private boolean areAllPreconditionsHopeful(int[] preconditionPositions) { 34 | for (int preconditionPos : preconditionPositions) { 35 | if (!hopefulFilterSet.get(preconditionPos)) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | 42 | private boolean isNarrowerHopeful(Filter specialization, int[] arguments) { 43 | return isFilterHopeful(specialization) && areAllPreconditionsHopeful(arguments); 44 | } 45 | 46 | private void markLandOfHope() { 47 | lastHopefulFilter = children.size(); 48 | hopefulFilterSet.set(lastHopefulFilter); 49 | } 50 | 51 | @Override 52 | public int addFilter(Filter filter) { 53 | if (isFilterHopeful(filter)) { 54 | markLandOfHope(); 55 | } 56 | return super.addFilter(filter); 57 | } 58 | 59 | @Override 60 | public int addNarrower(Filter specialization, int... preconditions) { 61 | if (isNarrowerHopeful(specialization, preconditions)) { 62 | markLandOfHope(); 63 | } 64 | return super.addNarrower(specialization, preconditions); 65 | } 66 | 67 | @Override 68 | public int addDerivativeOf(Derivative derivative, Object... arguments) { 69 | markLandOfHope(); 70 | return super.addDerivativeOf(derivative, arguments); 71 | } 72 | 73 | @Override 74 | public int getHorizon() { 75 | return lastHopefulFilter + 1; 76 | } 77 | 78 | @Override 79 | public BitSet setFilter(int filterIndex, Filter filter) { 80 | revisitLandOfHope(filterIndex, isFilterHopeful(filter)); 81 | return super.setFilter(filterIndex, filter); 82 | } 83 | 84 | private void revisitLandOfHope(int filterIndex, boolean filterHopeful) { 85 | hopefulFilterSet.set(filterIndex, filterHopeful); 86 | if (filterHopeful) { 87 | BitSet dependents = getDependents(filterIndex); 88 | dependents.andNot(hopefulFilterSet); 89 | int dependentIndex = filterIndex; 90 | while ((dependentIndex = dependents.nextSetBit(dependentIndex + 1)) >= 0) { 91 | // derivatives are all hopeful. single filters are independent. 92 | // so any successor found here is a specialization. 93 | final Filter oldFilter = getOldFilter(dependentIndex); 94 | final int[] preconditions = getOldNarrowerArgs(dependentIndex); 95 | final boolean newHope = isNarrowerHopeful(oldFilter, preconditions); 96 | hopefulFilterSet.set(dependentIndex, newHope); 97 | } 98 | } 99 | lastHopefulFilter = Math.max(lastHopefulFilter, hopefulFilterSet.length() - 1); 100 | } 101 | 102 | class State { 103 | final List> children; 104 | final Map reverseLookup; 105 | final BitSet hopefulFilterSet; 106 | 107 | State(List> children, 108 | Map reverseLookup, 109 | BitSet hopefulFilterSet) { 110 | this.children = new ArrayList>(children); 111 | this.reverseLookup = new IdentityHashMap(reverseLookup); 112 | this.hopefulFilterSet = (BitSet) hopefulFilterSet.clone(); 113 | } 114 | 115 | public void copyOut(List> children, 116 | Map reverseLookup, 117 | BitSet hopefulFilterSet) { 118 | Collections.copy(this.children, children); 119 | reverseLookup.clear(); 120 | reverseLookup.putAll(this.reverseLookup); 121 | hopefulFilterSet.clear(); 122 | hopefulFilterSet.or(this.hopefulFilterSet); 123 | } 124 | } 125 | 126 | State state; 127 | 128 | @Override 129 | public void freeze() { 130 | state = new State(children, reverseLookup, hopefulFilterSet); 131 | super.freeze(); 132 | } 133 | 134 | public void factoryReset() { 135 | state.copyOut(children, reverseLookup, hopefulFilterSet); 136 | lastHopefulFilter = hopefulFilterSet.length() - 1; 137 | } 138 | 139 | @Override 140 | public BitSet setNarrower(int filterIndex, Filter specialization, int... arguments) { 141 | revisitLandOfHope(filterIndex, isNarrowerHopeful(specialization, arguments)); 142 | return super.setNarrower(filterIndex, specialization, arguments); 143 | } 144 | 145 | @Override 146 | public BitSet setDerivative(int filterIndex, Derivative derivative) { 147 | revisitLandOfHope(filterIndex, true); 148 | return super.setDerivative(filterIndex, derivative); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/TestCompositeProjector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.primitives.Filter; 10 | import com.skype.research.util.projection.CompositeProjectorImpl; 11 | import com.skype.research.util.projection.Derivative; 12 | import junit.framework.Assert; 13 | import junit.framework.TestCase; 14 | 15 | import java.util.BitSet; 16 | 17 | /** 18 | * Composite filter tests. 19 | */ 20 | public class TestCompositeProjector extends TestCase { 21 | 22 | Filter isEven = new Filter() { 23 | @Override 24 | public boolean accept(Integer item) { 25 | return item % 2 == 0; 26 | } 27 | }; 28 | Filter isMultipleOf3 = new Filter() { 29 | @Override 30 | public boolean accept(Integer item) { 31 | return item % 3 == 0; 32 | } 33 | }; 34 | Filter isPowerOf2 = new Filter() { 35 | @Override 36 | public boolean accept(Integer item) { 37 | return Long.bitCount(item) == 1; 38 | } 39 | }; 40 | Derivative binaryIntersection = new Derivative() { 41 | @Override 42 | public boolean accept(int... args) { 43 | return args[0] * args[1] != 0; 44 | } 45 | }; 46 | Derivative binaryDifference = new Derivative() { 47 | @Override 48 | public boolean accept(int... args) { 49 | return args[0] == 0 ^ args[1] == 0; 50 | } 51 | }; 52 | Derivative binaryUnion = new Derivative() { 53 | @Override 54 | public boolean accept(int... args) { 55 | return (args[0] | args[1]) != 0; 56 | } 57 | }; 58 | Derivative ternaryIntersection = new Derivative() { 59 | @Override 60 | public boolean accept(int... args) { 61 | return args[0] * args[1] * args[2] != 0; 62 | } 63 | }; 64 | CompositeProjectorImpl projector; 65 | 66 | int isEvenIndex; 67 | int isMul3Index; 68 | int binDifIndex; 69 | int binIntIndex; 70 | int binUniIndex; 71 | int isPow2Index; 72 | int triIntIndex; 73 | 74 | public void setUp() throws Exception { 75 | super.setUp(); 76 | projector = new CompositeProjectorImpl(); 77 | isEvenIndex = projector.addFilter(isEven); 78 | isMul3Index = projector.addFilter(isMultipleOf3); 79 | binDifIndex = projector.addDerivativeOf(binaryDifference, isEven, isMultipleOf3); 80 | binIntIndex = projector.addDerivativeOf(binaryIntersection, isEven, 1); 81 | binUniIndex = projector.addDerivativeOf(binaryUnion, 0, 1); 82 | isPow2Index = projector.addFilter(isPowerOf2); 83 | triIntIndex = projector.addDerivativeOf(ternaryIntersection, 0, isPowerOf2, binaryUnion); 84 | } 85 | 86 | public void testAmalgam() throws Exception { 87 | Assert.assertEquals("Total filter count", 7, projector.getFilterCount()); 88 | // we pass null to make sure that the precomputed array is not accessed by direct filters 89 | Assert.assertFalse(projector.accept(7, isEvenIndex, null)); 90 | Assert.assertFalse(projector.accept(7, isMul3Index, null)); 91 | Assert.assertFalse(projector.accept(7, isPow2Index, null)); 92 | Assert.assertTrue(projector.accept(8, isPow2Index, null)); 93 | int[] precomputed = new int[] { 1, 1, 0, 0, 1, 1, 0 }; // even, mul3, NOT dif, NOT int, uni, pow2, NOT 3-int 94 | // now we pass null as element value 95 | Assert.assertFalse(projector.accept(null, binDifIndex, precomputed)); // 1 ^ 1 96 | Assert.assertTrue(projector.accept(null, binIntIndex, precomputed)); // 1 & 1 97 | Assert.assertTrue(projector.accept(null, binUniIndex, precomputed)); // 1 | 1 98 | Assert.assertTrue(projector.accept(null, triIntIndex, precomputed)); // 1 & 1 & 1 99 | } 100 | 101 | private static BitSet createBitSet(int... args) { 102 | final BitSet out = new BitSet(); 103 | for (int arg : args) { 104 | out.set(arg); 105 | } 106 | return out; 107 | } 108 | 109 | public void testDependencies() throws Exception { 110 | Assert.assertEquals(createBitSet(isEvenIndex, binUniIndex, binDifIndex, binIntIndex, triIntIndex), 111 | projector.getDependents(isEvenIndex)); 112 | Assert.assertEquals(createBitSet(isMul3Index, binUniIndex, binDifIndex, binIntIndex, triIntIndex), 113 | projector.getDependents(isMul3Index)); 114 | Assert.assertEquals(createBitSet(binDifIndex), 115 | projector.getDependents(binDifIndex)); 116 | Assert.assertEquals(createBitSet(binIntIndex), 117 | projector.getDependents(binIntIndex)); 118 | Assert.assertEquals(createBitSet(binUniIndex, triIntIndex), 119 | projector.getDependents(binUniIndex)); 120 | Assert.assertEquals(createBitSet(isPow2Index, triIntIndex), 121 | projector.getDependents(isPow2Index)); 122 | Assert.assertEquals(createBitSet(triIntIndex), 123 | projector.getDependents(triIntIndex)); 124 | } 125 | 126 | public void tearDown() throws Exception { 127 | ;;; 128 | super.tearDown(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/RangedAdaptableSkipList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.projection.CompositeProjector; 10 | 11 | import java.util.Comparator; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * A sectioned skip list, i.e. one capable of on-the-fly item classification and ranging. 17 | */ 18 | public class RangedAdaptableSkipList extends AdaptableSkipList implements RangedAdaptable { 19 | 20 | final Map aggregates = new HashMap(); 21 | final Ranger ranger; 22 | final int headItemFilter; 23 | 24 | // optimizations 25 | boolean delayAggregation; 26 | 27 | public RangedAdaptableSkipList(Ranger ranger, 28 | int levelCount, int denominator, 29 | int universeFilter, 30 | int headItemFilter, 31 | CompositeProjector projector) { 32 | this(ranger, levelCount, denominator, Trivial.naturalOrder(), universeFilter, headItemFilter, projector); 33 | } 34 | 35 | public RangedAdaptableSkipList(Ranger ranger, 36 | int levelCount, int denominator, 37 | Comparator comparator, 38 | int universeFilter, 39 | int headItemFilter, 40 | CompositeProjector projector) { 41 | super(levelCount, denominator, comparator, universeFilter, projector); 42 | this.ranger = ranger; 43 | this.headItemFilter = headItemFilter; 44 | setBroadcastOldValue(true); 45 | addElementObserver(new ElementObserver() { 46 | @Override 47 | public final void onElementUpdated(T element, 48 | int[] position, 49 | int[] changeEstimate, 50 | int deltaSign, 51 | int[] deltaCount) { 52 | adjustAggregation(element, deltaSign, deltaCount); 53 | } 54 | }); 55 | } 56 | 57 | final void adjustAggregation(T element, int deltaSign, int[] deltaCount) { 58 | if (element != null && deltaSign != 0 && !ranger.accept(element) && !Distance.isZero(deltaCount, horizon)) { 59 | Iterable sectionKeys = ranger.qualify(element); 60 | for (G sectionKey : sectionKeys) { 61 | int[] aggregate = aggregates.get(sectionKey); 62 | boolean isNewSection = aggregate == null; 63 | if (isNewSection) { 64 | // create if not exists 65 | aggregate = newDistance(); 66 | aggregates.put(sectionKey, aggregate); 67 | } 68 | Distance.add(aggregate, deltaSign, deltaCount, horizon); 69 | if (!delayAggregation) { 70 | final T sectionItem = ranger.getRangeItem(sectionKey); 71 | if (isNewSection) { 72 | add(sectionItem); 73 | } else { 74 | updateFilters(sectionItem, Trivial.refresh()); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | @Override 82 | public boolean accept(T element, int filterIndex, int[] precomputed) { 83 | if (filterIndex == headItemFilter) { 84 | return ranger.accept(element); 85 | } else if (projector.shouldComputeForGroup(filterIndex) || !ranger.accept(element)) { 86 | return super.accept(element, filterIndex, precomputed); 87 | } 88 | final int[] distance = aggregates.get(ranger.getRangeKey(element)); 89 | return distance != null && distance[filterIndex] > 0; 90 | } 91 | 92 | @Override 93 | public int getHeadItemFilterIndex() { 94 | return headItemFilter; 95 | } 96 | 97 | @Override 98 | public void updateRangeClassification() { 99 | // we assume fewer ranges, so it's easier to wipe them out 100 | while (size(headItemFilter) != 0) { 101 | remove(headItemFilter, 0); 102 | } 103 | aggregates.clear(); 104 | // full rebuild - wipe all existing sections 105 | Node node = absMinNode; 106 | Node prev = node; 107 | while ((node = node.nodes[0]) != null) { // this is safe against any upcoming insertions 108 | adjustAggregation(node.element, 1, prev.distances[0]); 109 | prev = node; 110 | } 111 | } 112 | 113 | @Override 114 | public void clear() { 115 | aggregates.clear(); 116 | super.clear(); 117 | } 118 | 119 | @Override 120 | public void hintBulkOpBegin() { 121 | delayAggregation = true; 122 | } 123 | 124 | @Override 125 | public void hintBulkOpCompleted() { 126 | if (delayAggregation) { 127 | for (G sectionKey : aggregates.keySet()) { 128 | T sectionItem = ranger.getRangeItem(sectionKey); 129 | if (!updateFilters(sectionItem, Trivial.refresh())) { 130 | add(sectionItem); 131 | } 132 | } 133 | } 134 | delayAggregation = false; 135 | } 136 | 137 | @Override 138 | public void updateRangePopulation() { 139 | int aggregates = size(headItemFilter); 140 | for (int index = 0; index < aggregates; ++index) { 141 | // FIXME implement update*() by index and filter 142 | updateFilters(get(headItemFilter, index), Trivial.refresh()); 143 | } 144 | } 145 | 146 | @Override 147 | public int getChildCount(int sourceElementIndex, int sourceFilterIndex, int targetFilterIndex) { 148 | return getChildCount(get(sourceFilterIndex, sourceElementIndex), targetFilterIndex); 149 | } 150 | 151 | @Override 152 | public int getChildCount(T item, int targetFilterIndex) { 153 | if (ranger.accept(item)) { 154 | final int[] aggregate = aggregates.get(ranger.getRangeKey(item)); 155 | if (aggregate == null) { 156 | return 0; 157 | } 158 | return aggregate[targetFilterIndex]; 159 | } 160 | return 0; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptation/Adaptation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptation; 8 | 9 | import com.skype.research.util.adaptable.Adaptable; 10 | import com.skype.research.util.adaptable.BulkUpdatable; 11 | import com.skype.research.util.adaptable.ElementObserver; 12 | import com.skype.research.util.datasets.FilteredDataSet; 13 | import com.skype.research.util.model.Publisher; 14 | import com.skype.research.util.model.RecyclerAdapter; 15 | import com.skype.research.util.model.Subscriber; 16 | import com.skype.research.util.primitives.Update; 17 | import com.skype.research.util.unique.Registry; 18 | import com.skype.research.util.unique.UniqueSequential; 19 | 20 | import java.util.BitSet; 21 | import java.util.IdentityHashMap; 22 | import java.util.Map; 23 | import java.util.WeakHashMap; 24 | 25 | /** 26 | * Dispatcher of Adaptable changes into [RecyclerView.Adapter]s and [ListAdapter]s. 27 | * Not yet a {@link Publisher} because knows nothing about filter specifications. 28 | * Single-thread. 29 | */ 30 | public class Adaptation implements ElementObserver, BulkUpdatable { 31 | 32 | final Adaptable adaptable; 33 | final int filterCount; 34 | final BitSet dirtyMask; 35 | /** 36 | * Position-unaware subscribers that only receive updates of the whole dataset 37 | * ({@link Subscriber#notifyDataSetChanged()}). 38 | */ 39 | final Map, Integer> coarseRoutingMap; 40 | /** 41 | * Position-aware subscribers stored here to receive wrap-up updates at the end 42 | * of large bulk operations, instead of a long series of small updates. 43 | * (RecyclerView processes long series of small updates at O(N^2) complexity.) 44 | * {@link #deferDispatch} flag is used to postpone small updates in this case. 45 | */ 46 | final Map, Integer> wrapUpRoutingMap; 47 | /** 48 | * Position-aware subscribers to deliver incremental updates to. 49 | */ 50 | final Map fineRoutingMap; 51 | final boolean autoDetach; 52 | 53 | boolean deferDispatch = false; 54 | 55 | final Registry> selections = new UniqueSequential>(new Registry>() { 56 | @Override 57 | public FilteredDataSet get(int id) { 58 | return new FilteredDataSet(adaptable, id); 59 | } 60 | }); 61 | 62 | public Adaptation(Adaptable adaptable, boolean autoDetach) { 63 | this.adaptable = adaptable; 64 | filterCount = adaptable.getFilterCount(); 65 | dirtyMask = new BitSet(filterCount); 66 | dirtyMask.set(0, filterCount); 67 | coarseRoutingMap = createMap(autoDetach); 68 | wrapUpRoutingMap = createMap(autoDetach); 69 | fineRoutingMap = createMap(autoDetach); 70 | this.adaptable.addElementObserver(this); 71 | this.autoDetach = autoDetach; 72 | } 73 | 74 | @Override 75 | public void onElementUpdated(T element, int[] position, int[] changeEstimate, int deltaSign, int[] deltaCount) { 76 | if (!deferDispatch) { 77 | for (Map.Entry subscription : fineRoutingMap.entrySet()) { 78 | final int filterIndex = subscription.getValue(); 79 | if (changeEstimate[filterIndex] != 0) { 80 | RecyclerAdapter adapter = subscription.getKey(); 81 | final int realDelta = deltaSign * deltaCount[filterIndex]; 82 | if (element == null) { 83 | if (realDelta > 0) { 84 | adapter.notifyItemRangeInserted(position[filterIndex], Math.abs(realDelta)); 85 | } else if (realDelta < 0) { 86 | adapter.notifyItemRangeRemoved(position[filterIndex], Math.abs(realDelta)); 87 | } 88 | // MOREINFO consider sending #notifyItemRangeChanged() (not the case now, but for completeness) 89 | } else { 90 | if (realDelta > 0) { 91 | adapter.notifyItemInserted(position[filterIndex]); 92 | } else if (realDelta < 0) { 93 | adapter.notifyItemRemoved(position[filterIndex]); 94 | } else { 95 | adapter.notifyItemChanged(position[filterIndex]); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | for (int i = 0; i < changeEstimate.length; i++) { 102 | if(changeEstimate[i]!= 0) { 103 | dirtyMask.set(i); 104 | } 105 | } 106 | } 107 | 108 | static Map createMap(boolean autoDetach) { 109 | return autoDetach ? new WeakHashMap() : new IdentityHashMap(); 110 | } 111 | 112 | protected Adaptable getAdaptable() { 113 | return adaptable; 114 | } 115 | 116 | public void subscribe(int filterIndex, Subscriber subscriber) { 117 | subscriber.setDataSet(selections.get(filterIndex)); 118 | if (subscriber instanceof RecyclerAdapter) { 119 | fineRoutingMap.put((RecyclerAdapter) subscriber, filterIndex); 120 | wrapUpRoutingMap.put(subscriber, filterIndex); 121 | } else { 122 | coarseRoutingMap.put(subscriber, filterIndex); 123 | } 124 | } 125 | 126 | public void unsubscribe(Subscriber subscriber) { 127 | coarseRoutingMap.remove(subscriber); 128 | wrapUpRoutingMap.remove(subscriber); 129 | // instanceof on ART mb costly here, so remove always and suppress the warning instead: 130 | //noinspection SuspiciousMethodCalls 131 | fineRoutingMap.remove(subscriber); 132 | } 133 | 134 | public boolean hasChanged() { 135 | return dirtyMask.cardinality() > 0; 136 | } 137 | 138 | public void apply(Update> modification) { 139 | if (modification.apply(adaptable)) { 140 | if (!deferDispatch) { 141 | sendBulkUpdates(coarseRoutingMap); 142 | dirtyMask.clear(); 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * Helper method. Use it when the modification is large enough to be better processed as a whole. 149 | * @param bulkModification a knowingly large modification. 150 | */ 151 | public void applyBulk(Update> bulkModification) { 152 | try { 153 | hintBulkOpBegin(); 154 | bulkModification.apply(adaptable); 155 | } finally { 156 | hintBulkOpCompleted(); 157 | } 158 | } 159 | 160 | private void sendBulkUpdates(Map, Integer> routingMap) { 161 | for (Map.Entry, Integer> subscription : routingMap.entrySet()) { 162 | final int filterIndex = subscription.getValue(); 163 | if (dirtyMask.get(filterIndex)) { 164 | subscription.getKey().notifyDataSetChanged(); 165 | } 166 | } 167 | } 168 | 169 | protected void markExposed(BitSet target, boolean markValue) { 170 | for (Integer filterIndex : coarseRoutingMap.values()) { 171 | target.set(filterIndex, markValue); 172 | } 173 | for (Integer filterIndex : fineRoutingMap.values()) { 174 | target.set(filterIndex, markValue); 175 | } 176 | } 177 | 178 | @Override 179 | public void hintBulkOpBegin() { 180 | deferDispatch = true; 181 | adaptable.hintBulkOpBegin(); 182 | } 183 | 184 | @Override 185 | public void hintBulkOpCompleted() { 186 | deferDispatch = false; 187 | adaptable.hintBulkOpCompleted(); 188 | sendBulkUpdates(coarseRoutingMap); 189 | sendBulkUpdates(wrapUpRoutingMap); 190 | dirtyMask.clear(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/Adaptable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import java.util.Comparator; 10 | import java.util.Iterator; 11 | import java.util.Map; 12 | 13 | /** Container with the following properties: 14 | * - ordered at model level (the universe) 15 | * - filtered at view level, preserving order 16 | * - random access by element position within any view, including the universe, is O(log N) 17 | * - per-element modification operations are as granular as possible and come at O(log N) too 18 | * 19 | * Methods with "filterIndex" argument treat element index and element count 20 | * in the context of the specified filtered selection ("view"). 21 | */ 22 | @SuppressWarnings("JavadocReference") 23 | public interface Adaptable extends Iterable>, 24 | ElementEditor, 25 | ElementObservable, 26 | BulkUpdatable { 27 | /** 28 | * Return the total filter count, aka the count of selections instantly maintained by this container. 29 | * Cannot change during the lifetime of the container. 30 | * @return the number of filters specified at construction/build time. 31 | */ 32 | int getFilterCount(); 33 | 34 | /** 35 | * Return the index of the special filter to return *all* elements, 36 | * as opposed to other filters that may return incomplete selections. 37 | * The container implementation need not expose such a filter, in general, 38 | * and does not use it internally. However, it might be easier 39 | * from the design standpoint to ensure that such filter exists (and is number 0). 40 | * @return the index of the trivial return-all filter. 41 | */ 42 | int getUniverseFilterIndex(); 43 | 44 | /** 45 | * The size of the universe represented by this container, i.e. the total number of elements stored. 46 | * @return the total number of elements stored. 47 | */ 48 | int size(); 49 | 50 | /** 51 | * The size of a filtered selection represented by this container. 52 | * @param filterIndex number of the filter (and, respectively, the selection) 53 | * @return the current number of elements passing a given filter. 54 | */ 55 | int size(int filterIndex); 56 | 57 | /** 58 | * Add an item to the container, if there is no such item yet or duplicates are allowed. 59 | * The item will be placed according to the current comparison order 60 | * and filtered according to the current filters. 61 | * @param element item to add 62 | * @return whether the operation resulted in a modification of the container. 63 | */ 64 | boolean add(T element); 65 | 66 | /** 67 | * Remove an item from the container. 68 | * Elements are matched by comparison equality, NOT by identity. 69 | * @param element item to remove. 70 | * @return true if the item was found and has been removed, false otherwise. 71 | */ 72 | boolean remove(T element); 73 | 74 | /** 75 | * Remove an item from the container by selection index and element index within the selection 76 | * Use {@link #getUniverseFilterIndex()} as filterIndex to remove from the universe. 77 | * (i.e. "remove the second red ball"). 78 | * @param filterIndex index of the selection in which the element will be looked up. 79 | * @param elementIndex index of the element within the selection. 80 | * @return true if the item was found and has been removed, false otherwise. 81 | */ 82 | boolean remove(int filterIndex, int elementIndex); 83 | 84 | /** 85 | * Return iterator over a given selection. 86 | * Use {@link #getUniverseFilterIndex()} as filterIndex to iterate over all stored elements. 87 | * @param filterIndex index of the selection to iterate over. 88 | * @return iterator that returns elements matching a specific filter. 89 | */ 90 | Iterator iterator(int filterIndex); 91 | 92 | /** 93 | * Convenience method: get element by index from the universe. 94 | * Equivalent to #get(#getUniverseFilterIndex(), elementIndex). 95 | * @param elementIndex index of the element to return. 96 | * @return element to find. 97 | */ 98 | T get(int elementIndex); 99 | 100 | /** 101 | * Get element by selection index and element index within the selection. 102 | * @param filterIndex index of the selection in which the element will be looked up. 103 | * @param elementIndex index of the element within the selection. 104 | * @return the element at the specified position, or null if bounds are not satisfied. 105 | */ 106 | T get(int filterIndex, int elementIndex); 107 | int indexOf(T item); 108 | int indexOf(int filterIndex, T item); 109 | 110 | /** 111 | * Overload of {@link #convertIndex(int, int, int, boolean)} with default policy ("floor"). 112 | * @param sourceElementIndex zero-based position number within the source view 113 | * @param sourceFilterIndex index of the original filtered view (may be the universe) 114 | * @param targetFilterIndex index of the target filtered view (may be the universe) 115 | * @return converted index, as required by research.widget.SectionIndexer#getSectionForPosition 116 | */ 117 | int convertIndex(int sourceElementIndex, int sourceFilterIndex, int targetFilterIndex); 118 | 119 | /** 120 | * Convert element index in a view to the index of the same element in another view. 121 | * If the element does not pass the target filter, the behavior depends on the policy. 122 | * 123 | * @param sourceElementIndex zero-based position number within the source view 124 | * @param sourceFilterIndex index of the original filtered view (may be the universe) 125 | * @param targetFilterIndex index of the target filtered view (may be the universe) 126 | * @param roundToCeiling if exact match is not found, return the next available element (otherwise return the previous one) 127 | * @return such a convertedIndex that 128 | * {@link Adaptable#get(int sourceFilterIndex, int sourceElementIndex) == 129 | * {@link Adaptable#get(int targetFilterIndex, int convertedIndex)}}, at best effort. 130 | * and the sourceElementIndex is outside the [0; {@link Adaptable#size(int sourceFilterIndex)}) range. 131 | */ 132 | int convertIndex(int sourceElementIndex, int sourceFilterIndex, int targetFilterIndex, boolean roundToCeiling); 133 | /** 134 | * Delete all contents without broadcasting removal of any individual element. 135 | * That this method breaks the usual {@link ElementObserver} contract in its 136 | * strict sense: only one 137 | * {@link ElementObserver#onElementUpdated(Object, int[], int[], int, int[])} is 138 | * broadcast, with null as element value, the total size as the estimate vector 139 | * and -1 the total size as the deltaCount vector. 140 | * Internal state: if there is a pending comparator, it is immediately applied. 141 | */ 142 | void clear(); 143 | /** 144 | * Replace all contents in bulk, without broadcasting update of any individual element. 145 | * That this method breaks the usual {@link ElementObserver} contract in its 146 | * strict sense: only one 147 | * {@link ElementObserver#onElementUpdated(Object, int[], int[], int, int[])} is 148 | * broadcast, with null as element value, the total size as the estimate vector 149 | * and -1 the total size as the deltaCount vector. 150 | * Internal state: if there is a pending comparator, it is immediately applied. 151 | * @param adaptable data source to copy from 152 | */ 153 | void setAll(Adaptable adaptable); 154 | 155 | /** 156 | * Add contents in bulk, without broadcasting update of any individual element. 157 | * @param adaptable source of data to add 158 | */ 159 | void addAll(Adaptable adaptable); 160 | 161 | /** 162 | * Set the next comparator to apply when the data are explicitly cleared or reordered. 163 | * @param pendingComparator comparator to be used upon a subsequent call to 164 | * {@link #setAll(Adaptable)} or {@link #clear()}. 165 | */ 166 | void setComparator(Comparator pendingComparator); 167 | 168 | /** 169 | * Get the current comparator (the one items are currently sorted according to). 170 | * @return the active comparator. 171 | */ 172 | Comparator getComparator(); 173 | 174 | /** 175 | * Controls auto-add mode: when a locator is not found, a modification is applied to old value 176 | * and the modified value is inserted in the container. 177 | * @param autoAdd true to set auto-add on, false to turn off. 178 | */ 179 | void setAutoAdd(boolean autoAdd); 180 | } 181 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptation/TestUpdates.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptation; 8 | 9 | import com.skype.research.util.adaptable.Adaptable; 10 | import com.skype.research.util.adaptable.AdaptableFactory; 11 | import com.skype.research.util.adaptable.Dump; 12 | import com.skype.research.util.adaptable.ElementObserver; 13 | import com.skype.research.util.adaptable.FlexibleAdaptable; 14 | import com.skype.research.util.adaptable.mocks.DivisibleBy; 15 | import com.skype.research.util.adaptable.mocks.IntValue; 16 | import com.skype.research.util.model.DataSet; 17 | import com.skype.research.util.model.RecyclerAdapter; 18 | import com.skype.research.util.model.Subscriber; 19 | import com.skype.research.util.primitives.Update; 20 | import junit.framework.Assert; 21 | import junit.framework.AssertionFailedError; 22 | import junit.framework.TestCase; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.Comparator; 27 | import java.util.Iterator; 28 | import java.util.List; 29 | import java.util.Random; 30 | 31 | /** 32 | * Test identification and routing of incremental positional updates. 33 | */ 34 | public class TestUpdates extends TestCase { 35 | 36 | static class Representation implements Subscriber, RecyclerAdapter { 37 | final Adaptable adaptable; 38 | final int filterIndex; 39 | final List selection = new ArrayList(); 40 | 41 | Representation(Adaptable source, int filterIndex) { 42 | this.adaptable = source; 43 | this.filterIndex = filterIndex; 44 | } 45 | 46 | T getItem(int position) { 47 | return adaptable.get(filterIndex, position); 48 | } 49 | 50 | @Override 51 | public void notifyItemChanged(int position) { 52 | selection.set(position, getItem(position)); 53 | } 54 | 55 | @Override 56 | public void notifyItemInserted(int position) { 57 | selection.add(position, getItem(position)); 58 | } 59 | 60 | @Override 61 | public void notifyItemMoved(int fromPosition, int toPosition) { 62 | throw new UnsupportedOperationException(); // should not be triggered 63 | } 64 | 65 | @Override 66 | public void notifyItemRangeInserted(int positionStart, int itemCount) { 67 | for (int position = 0; position < itemCount; ++position, ++positionStart) { 68 | selection.add(positionStart, getItem(positionStart)); 69 | } 70 | } 71 | 72 | @Override 73 | public void notifyItemRangeChanged(int positionStart, int itemCount) { 74 | int positionEnd = positionStart + itemCount; 75 | for (int position = positionStart; position < positionEnd; ++position) { 76 | notifyItemChanged(position); 77 | } 78 | } 79 | 80 | @Override 81 | public void notifyItemRangeRemoved(int positionStart, int itemCount) { 82 | for (int position = 0; position < itemCount; ++position) { 83 | selection.remove(positionStart); 84 | } 85 | } 86 | 87 | @Override 88 | public void notifyItemRemoved(int position) { 89 | selection.remove(position); 90 | } 91 | 92 | void validate() { 93 | final int size = adaptable.size(filterIndex); 94 | Assert.assertEquals("Size[" + filterIndex + "] matches", size, selection.size()); 95 | Iterator refIterator = adaptable.iterator(filterIndex); 96 | Iterator ourIterator = selection.iterator(); 97 | int pos = 0; 98 | while (refIterator.hasNext()) { 99 | Assert.assertSame("Contents[" + filterIndex + "][" + (pos++) + "] match", 100 | refIterator.next(), ourIterator.next()); 101 | } 102 | } 103 | 104 | void assertClear() { 105 | Assert.assertEquals("Clear[" + filterIndex + "]", 0, selection.size()); 106 | } 107 | 108 | @Override 109 | public void setDataSet(DataSet dataSet) { 110 | Assert.assertEquals("The original assigned data set is empty", 0, dataSet.getCount()); 111 | } 112 | 113 | @Override 114 | public void notifyDataSetChanged() { 115 | throw new AssertionFailedError("No all-in-one update is expected"); 116 | } 117 | 118 | @Override 119 | public List> getAncillarySubscribers() { 120 | return Collections.emptyList(); 121 | } 122 | }; 123 | 124 | /** 125 | * Get ready for RecyclerView$Adapter 126 | * @throws Exception 127 | */ 128 | public void testPositionalUpdates() throws Exception { 129 | AdaptableFactory builder = new AdaptableFactory(); 130 | builder.setComparator(new Comparator() { 131 | @Override 132 | public int compare(IntValue lhs, IntValue rhs) { 133 | int lhs1 = lhs.getValue(); 134 | int rhs1 = rhs.getValue(); 135 | return lhs1 < rhs1 ? -1 : (lhs1 == rhs1 ? 0 : 1); 136 | } 137 | }); 138 | for (int i = 1, divisor = 1; i <= 8; ++i) { 139 | divisor<<=1; 140 | divisor++; 141 | builder.addFilter(new DivisibleBy(divisor)); 142 | } 143 | FlexibleAdaptable adaptable = builder.create(); 144 | final int filterCount = adaptable.getFilterCount(); 145 | final Adaptation adaptation = new Adaptation(adaptable, false); 146 | final List> reflected = new ArrayList>(); 147 | for (int filterIndex = 0; filterIndex < filterCount; ++filterIndex) { 148 | Representation selectionRepresentation = new Representation(adaptable, filterIndex); 149 | adaptation.subscribe(filterIndex, selectionRepresentation); 150 | reflected.add(selectionRepresentation); 151 | } 152 | adaptable.addElementObserver(new ElementObserver() { 153 | @Override 154 | public void onElementUpdated(IntValue element, int[] position, int[] changeEstimate, int deltaSign, int[] deltaCount) { 155 | validateReflection(filterCount, reflected); 156 | } 157 | }); 158 | final Random random = new Random(1 << 11); 159 | for (int i = 0; i < 1 << 11; ++i) { 160 | adaptable.add(new IntValue(random.nextInt(1 << 15))); 161 | } 162 | for (int i = 0; i < 1 << 14; ++i) { 163 | validateReflection(filterCount, reflected); 164 | final int randomValue = random.nextInt(1 << 15); 165 | final IntValue randomLocator = new IntValue(randomValue); 166 | switch (random.nextInt(4)) { 167 | case 0: 168 | adaptable.add(randomLocator); 169 | if (random.nextInt(1 << 6) == 0) { 170 | validateReflection(filterCount, reflected); 171 | } 172 | break; 173 | case 1: 174 | adaptable.updateReorder(randomLocator, new Update() { 175 | @Override 176 | public boolean apply(IntValue element) { 177 | final int updatedValue = random.nextInt(4) == 0 ? randomValue : random.nextInt(1 << 15); 178 | boolean changed = element.getValue() != updatedValue; 179 | element.setValue(updatedValue); 180 | return changed; 181 | } 182 | }); 183 | if (random.nextInt(1 << 6) == 0) { 184 | validateReflection(filterCount, reflected); 185 | } 186 | break; 187 | case 2: 188 | adaptable.remove(randomLocator); 189 | if (random.nextInt(1 << 6) == 0) { 190 | validateReflection(filterCount, reflected); 191 | } 192 | break; 193 | case 3: 194 | final int randomFilter = random.nextInt(filterCount); 195 | final int size = adaptable.size(randomFilter); 196 | if (size > 0) { 197 | adaptable.remove(randomFilter, random.nextInt(size)); 198 | } 199 | if (random.nextInt(1 << 6) == 0) { 200 | validateReflection(filterCount, reflected); 201 | } 202 | break; 203 | } 204 | if (random.nextInt(1 << 12) == 0) { 205 | adaptable.clear(); 206 | for (int filterIndex = 0; filterIndex < filterCount; ++filterIndex) { 207 | reflected.get(filterIndex).assertClear(); 208 | } 209 | } 210 | if (random.nextInt(1 << 12) == 0) { 211 | adaptable.setComparator(new Comparator() { 212 | final int mask = random.nextInt(); 213 | 214 | @Override 215 | public int compare(IntValue lhs, IntValue rhs) { 216 | int lhs1 = lhs.getValue() ^ mask; 217 | int rhs1 = rhs.getValue() ^ mask; 218 | return lhs1 < rhs1 ? -1 : (lhs1 == rhs1 ? 0 : 1); 219 | } 220 | }); 221 | adaptable.setAll(adaptable); 222 | } 223 | if (random.nextInt(1 << 8) == 0) { 224 | Dump.validateIntegrity(adaptable); 225 | validateReflection(filterCount, reflected); 226 | } 227 | } 228 | } 229 | 230 | protected static void validateReflection(int filterCount, List> reflected) { 231 | for (int filterIndex = 0; filterIndex < filterCount; ++filterIndex) { 232 | reflected.get(filterIndex).validate(); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/projection/CompositeProjectorImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.projection; 8 | 9 | import com.skype.research.util.pool.SingleEntryPool; 10 | import com.skype.research.util.primitives.Factory; 11 | import com.skype.research.util.primitives.Filter; 12 | 13 | import java.util.ArrayList; 14 | import java.util.BitSet; 15 | import java.util.IdentityHashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * Dynamically built projector used internally by filterable structures. 21 | */ 22 | public class CompositeProjectorImpl implements CompositeProjector { 23 | 24 | final List> children = new ArrayList>(); 25 | final BitSet forceComputeSpec = new BitSet(); 26 | final BitSet immutableFilters = new BitSet(); 27 | final Map reverseLookup = new IdentityHashMap(); 28 | private boolean[] forceCompute; 29 | private boolean frozen; 30 | 31 | @Override 32 | public int addFilter(Filter filter) { 33 | return doAdd(new FilterProjector(filter), filter); 34 | } 35 | 36 | @Override 37 | public int addNarrowerOf(Filter specialization, Object... preconditions) { 38 | return addNarrower(specialization, lookup(preconditions)); 39 | } 40 | 41 | public int addNarrower(Filter specialization, int... preconditions) { 42 | return doAdd(new NarrowerProjector(specialization, preconditions), null); 43 | } 44 | 45 | @Override 46 | public int addDerivativeOf(Derivative derivative, Object... arguments) { 47 | return addDerivative(derivative, lookup(arguments)); 48 | } 49 | 50 | @Override 51 | public int addDerivative(Derivative derivative, int... arguments) { 52 | return doAdd(new DerivativeProjector(derivative, arguments), derivative); 53 | } 54 | 55 | private int[] lookup(Object[] arguments) { 56 | final int[] positions = new int[arguments.length]; 57 | for (int i = 0; i < arguments.length; i++) { 58 | Object argument = arguments[i]; 59 | positions[i] = argument instanceof Number 60 | // allow direct specification of SOME positional parameters 61 | ? ((Number) argument).intValue() 62 | // when not found, will throw a NPE on unboxing 63 | : reverseLookup.get(argument); 64 | } 65 | return positions; 66 | } 67 | 68 | private int doAdd(SocialProjector projector, Object anchor) { 69 | checkFrozen(); 70 | final int position = children.size(); 71 | if (projector.getDependencies().nextSetBit(position) >= 0) { 72 | throw new IllegalArgumentException("Cyclic dependency in filter " + anchor); 73 | } 74 | if (anchor != null) { 75 | reverseLookup.put(anchor, position); 76 | } 77 | children.add(projector); 78 | return position; 79 | } 80 | 81 | private void checkFrozen() { 82 | if (frozen) { 83 | throw new IllegalStateException("structural changes frozen"); 84 | } 85 | } 86 | 87 | public int getFilterCount() { 88 | return children.size(); 89 | } 90 | 91 | public int getHorizon() { 92 | return getFilterCount(); 93 | } 94 | 95 | public void freeze() { 96 | forceCompute = new boolean[getFilterCount()]; 97 | for (int i = 0; i < forceCompute.length; i++) { 98 | forceCompute[i] = forceComputeSpec.get(i); 99 | } 100 | frozen = true; 101 | } 102 | 103 | @Override 104 | public final boolean shouldComputeForGroup(int filterIndex) { 105 | return forceCompute[filterIndex]; 106 | } 107 | 108 | @Override 109 | public final void freezeFilter(int filterIndex) { 110 | immutableFilters.set(filterIndex); 111 | } 112 | 113 | @Override 114 | public BitSet setShouldComputeForGroup(int filterIndex, boolean shouldCompute) { 115 | forceComputeSpec.set(filterIndex, shouldCompute); 116 | return getDependents(filterIndex); 117 | } 118 | 119 | public final BitSet getDependents(int filterIndex) { 120 | BitSet dependents = new BitSet(); 121 | getDependents(filterIndex, dependents, true); 122 | return dependents; 123 | } 124 | 125 | public final void getDependents(int touchedIndex, BitSet out, boolean markValue) { 126 | out.set(touchedIndex); 127 | for (int filterIndex = touchedIndex; filterIndex < children.size(); filterIndex++) { 128 | if (children.get(filterIndex).getDependencies().intersects(out)) { 129 | out.set(filterIndex, markValue); 130 | } 131 | } 132 | } 133 | 134 | protected final Filter getOldFilter(int filterIndex) { 135 | // WISDOM also works for NarrowerProjector (extends FilterProjector) 136 | return ((FilterProjector) children.get(filterIndex)).filter; 137 | } 138 | 139 | @Override 140 | public BitSet setFilter(int filterIndex, Filter filter) { 141 | clear(filterIndex); 142 | children.set(filterIndex, new FilterProjector(filter)); 143 | reverseLookup.put(filter, filterIndex); 144 | return getDependents(filterIndex); 145 | } 146 | 147 | @Override 148 | public BitSet setNarrower(int filterIndex, Filter specialization) { 149 | // the following line throws a ClassCastException if the predecessor is not a Narrower 150 | return setNarrower(filterIndex, specialization, getOldNarrowerArgs(filterIndex)); 151 | } 152 | 153 | protected final int[] getOldNarrowerArgs(int filterIndex) { 154 | return ((NarrowerProjector) children.get(filterIndex)).positions; 155 | } 156 | 157 | @Override 158 | public BitSet setNarrowerOf(int filterIndex, Filter specialization, Object... arguments) { 159 | return setNarrower(filterIndex, specialization, lookup(arguments)); 160 | } 161 | 162 | public BitSet setNarrower(int filterIndex, Filter specialization, int... positions) { 163 | clear(filterIndex); 164 | children.set(filterIndex, new NarrowerProjector(specialization, positions)); 165 | return getDependents(filterIndex); 166 | } 167 | 168 | @Override 169 | public BitSet setDerivative(int filterIndex, Derivative derivative) { 170 | // the following line throws a ClassCastException if the predecessor is not a Derivative 171 | return setDerivative(filterIndex, derivative, getOldDerivativeArgs(filterIndex)); 172 | } 173 | 174 | protected final int[] getOldDerivativeArgs(int filterIndex) { 175 | return ((DerivativeProjector) children.get(filterIndex)).positions; 176 | } 177 | 178 | @Override 179 | public BitSet setDerivativeOf(int filterIndex, Derivative derivative, Object... arguments) { 180 | return setDerivative(filterIndex, derivative, lookup(arguments)); 181 | } 182 | 183 | public BitSet setDerivative(int filterIndex, Derivative derivative, int... positions) { 184 | clear(filterIndex); 185 | children.set(filterIndex, new DerivativeProjector(derivative, positions)); 186 | reverseLookup.put(derivative, filterIndex); 187 | return getDependents(filterIndex); 188 | } 189 | 190 | private void clear(int filterIndex) { 191 | if (immutableFilters.get(filterIndex)) { 192 | throw new IllegalArgumentException(String.format("Attempt to alter immutable filter %d", filterIndex)); 193 | } 194 | reverseLookup.remove(children.get(filterIndex).getAnchor()); 195 | } 196 | 197 | @Override 198 | public final boolean accept(T element, int filterIndex, int[] precomputed) { 199 | return children.get(filterIndex).accept(element, filterIndex, precomputed); 200 | } 201 | 202 | final void ensureDirect(int filterIndex) { 203 | if (!isDirect(filterIndex)) { 204 | throw new IllegalStateException(String.format("Filter %d must be directly computable", filterIndex)); 205 | } 206 | } 207 | 208 | final boolean isDirect(int filterIndex) { 209 | return children.get(filterIndex) instanceof FilterProjector; 210 | } 211 | 212 | static interface SocialProjector extends Projector { 213 | BitSet getDependencies(); 214 | Object getAnchor(); 215 | } 216 | 217 | static class FilterProjector implements SocialProjector { 218 | 219 | static final BitSet noDependencies = new BitSet(); 220 | 221 | final Filter filter; 222 | public FilterProjector(Filter filter) { 223 | this.filter = filter; 224 | } 225 | 226 | @Override 227 | public boolean accept(T element, int filterIndex, int[] precomputed) { 228 | return filter.accept(element); 229 | } 230 | 231 | @Override 232 | public BitSet getDependencies() { 233 | return noDependencies; 234 | } 235 | 236 | @Override 237 | public final Object getAnchor() { 238 | return filter; 239 | } 240 | } 241 | 242 | static class NarrowerProjector extends FilterProjector { 243 | final int[] positions; 244 | final BitSet dependencies = new BitSet(); 245 | 246 | public NarrowerProjector(Filter filter, int[] positions) { 247 | super(filter); 248 | this.positions = positions; 249 | for (int dependency : positions) { 250 | dependencies.set(dependency); 251 | } 252 | } 253 | 254 | @Override 255 | public final BitSet getDependencies() { 256 | return dependencies; 257 | } 258 | 259 | @Override 260 | public final boolean accept(T element, int filterIndex, int[] precomputed) { 261 | for (int position : positions) { 262 | if (precomputed[position] == 0) { 263 | return false; 264 | } 265 | } 266 | return super.accept(element, filterIndex, precomputed); 267 | } 268 | } 269 | 270 | static class DerivativeProjector implements SocialProjector { 271 | final Derivative derivative; 272 | final int[] positions; 273 | final int argListSize; 274 | final SingleEntryPool tmpArgPool; 275 | final BitSet dependencies = new BitSet(); 276 | 277 | public DerivativeProjector(Derivative derivative, int[] positions) { 278 | this.derivative = derivative; 279 | this.positions = positions; 280 | this.argListSize = positions.length; 281 | tmpArgPool = new SingleEntryPool(new Factory() { 282 | @Override 283 | public final int[] create() { 284 | return new int[argListSize]; 285 | } 286 | }); 287 | for (int dependency : positions) { 288 | dependencies.set(dependency); 289 | } 290 | } 291 | 292 | @Override 293 | public final boolean accept(T element, int filterIndex, int[] precomputed) { 294 | int[] arguments = tmpArgPool.allocate(); 295 | try { 296 | for (int i = 0; i < argListSize; i++) { 297 | arguments[i] = precomputed[positions[i]]; 298 | } 299 | return derivative.accept(arguments); 300 | } finally { 301 | tmpArgPool.recycle(arguments); 302 | } 303 | } 304 | 305 | @Override 306 | public final BitSet getDependencies() { 307 | return dependencies; 308 | } 309 | 310 | @Override 311 | public final Object getAnchor() { 312 | return derivative; 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/TestRangedAdaptable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.adaptable.mocks.DivisibleBy; 10 | import com.skype.research.util.adaptable.mocks.Sample; 11 | import com.skype.research.util.primitives.Filter; 12 | import com.skype.research.util.projection.Derivative; 13 | import com.skype.research.util.projection.ProjectorEditor; 14 | import org.junit.Assert; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.junit.runners.Parameterized; 18 | 19 | import java.util.Arrays; 20 | import java.util.Collection; 21 | import java.util.Collections; 22 | import java.util.Comparator; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | import java.util.Random; 26 | import java.util.Set; 27 | import java.util.TreeSet; 28 | import java.util.regex.Pattern; 29 | 30 | /** 31 | * Test captioned list. 32 | */ 33 | @RunWith(Parameterized.class) 34 | public class TestRangedAdaptable { 35 | 36 | @Parameterized.Parameters(name = "DuplicatesAllowed: {0}") 37 | public static Collection data() { 38 | return Arrays.asList(new Object[][] {{Boolean.FALSE}, {Boolean.TRUE}}); 39 | } 40 | 41 | private final boolean allowDuplicates; 42 | 43 | public TestRangedAdaptable(boolean allowDuplicates) { 44 | this.allowDuplicates = allowDuplicates; 45 | } 46 | 47 | private AdaptableFactory createRangeAdaptableFactory() { 48 | AdaptableFactory factory = new AdaptableFactory(true); 49 | factory.setAllowDuplicates(allowDuplicates); 50 | return factory; 51 | } 52 | 53 | static final Comparator contentsComparator = new Comparator() { 54 | @Override 55 | public int compare(Object lhs, Object rhs) { 56 | return lhs.toString().compareTo(rhs.toString()); 57 | } 58 | }; 59 | 60 | static final Ranger classifier = new Ranger() { 61 | @Override 62 | public Character getRangeItem(Character rangeKey) { 63 | return rangeKey; 64 | } 65 | 66 | @Override 67 | public Character getRangeKey(Object rangeItem) { 68 | return (Character) rangeItem; 69 | } 70 | 71 | @Override 72 | public boolean accept(Object item) { 73 | return isCharacter.accept(item); 74 | } 75 | 76 | @Override 77 | public Iterable qualify(Object element) { 78 | String representation = String.valueOf(element); 79 | if (representation.length() == 0) { 80 | return Collections.emptyList(); 81 | } else { 82 | return Collections.singleton(representation.charAt(0)); 83 | } 84 | } 85 | }; 86 | 87 | static class PatternMatcher implements Filter { 88 | final Pattern pattern; 89 | 90 | PatternMatcher(String regex) { 91 | this.pattern = Pattern.compile(regex); 92 | } 93 | 94 | @Override 95 | public boolean accept(Object item) { 96 | return pattern.matcher((CharSequence) item).find(); 97 | } 98 | } 99 | 100 | static final Filter isCharacter = new Filter() { 101 | @Override 102 | public boolean accept(Object item) { 103 | return item instanceof Character; 104 | } 105 | }; 106 | 107 | static final Filter hasSpaces = new PatternMatcher("\\s+"); 108 | static final Filter hasDigits = new PatternMatcher("\\d+"); 109 | 110 | @Test 111 | public void testClassification() throws Exception { 112 | AdaptableFactory builder = createRangeAdaptableFactory(); 113 | builder.setComparator(contentsComparator); 114 | builder.addFilter(hasSpaces); 115 | builder.addFilter(hasDigits); 116 | FlexibleAdaptable ras = builder.create(classifier); 117 | populateNormalizedAdaptable(ras, 2048); 118 | assertFilteredIndex(0, ras); // all 119 | assertFilteredIndex(2, ras); // spatial 120 | assertFilteredIndex(3, ras); // digital 121 | } 122 | 123 | private void populateNormalizedAdaptable(FlexibleAdaptable ras, int count) { 124 | Random random = new Random(count); 125 | for (int i = 0; i < count; ++i) { 126 | ras.add(Long.toHexString(random.nextLong()).replace("[0-8]", "q").replace('z', ' ')); 127 | } 128 | } 129 | 130 | @Test 131 | public void testDependencies() throws Exception { 132 | AdaptableFactory builder = createRangeAdaptableFactory(); 133 | builder.setComparator(contentsComparator); 134 | builder.addFilter(hasSpaces); 135 | builder.addFilter(hasDigits); 136 | // add un-grouping 137 | Derivative butNot = new Derivative() { 138 | @Override 139 | public boolean accept(int... args) { 140 | return args[0] != 0 && args[1] == 0; 141 | } 142 | }; 143 | int derivativeOne = builder.addDerivativeOf(butNot, 144 | hasSpaces, 1); // 2, 1 145 | // computeForGroups==true => headers will disappear 146 | builder.setShouldComputeForGroup(derivativeOne, true); 147 | int derivativeTwo = builder.addDerivativeOf(butNot, 148 | hasDigits, 1); // 3, 1 149 | // computeForGroups==false => headers will stay 150 | builder.setShouldComputeForGroup(derivativeTwo, false); 151 | FlexibleAdaptable ras = builder.create(classifier); 152 | // spy 153 | final Map spy = new HashMap(); 154 | ras.addElementObserver(new ElementObserver() { 155 | @Override 156 | public void onElementUpdated(Object element, int[] position, int[] changeEstimate, int deltaSign, int[] deltaCount) { 157 | int[] accumulated = spy.get(element); 158 | if (accumulated == null) { 159 | accumulated = new int[deltaCount.length]; 160 | for (int i = 0; i < deltaCount.length; i++) { 161 | accumulated[i] = deltaSign * deltaCount[i]; 162 | } 163 | spy.put(element, accumulated); 164 | } else { 165 | for (int i = 0; i < deltaCount.length; i++) { 166 | accumulated[i] += deltaSign * deltaCount[i]; 167 | } 168 | } 169 | } 170 | }); 171 | // fill 172 | populateNormalizedAdaptable(ras, 2048); 173 | // check evidence 174 | checkEvidence(ras, spy); 175 | ensureHeadersDrop(ras, derivativeOne); 176 | ensureHeadersStay(ras, 3, derivativeTwo); 177 | // can add extra proof: modify, validate modifications, check evidence once again... 178 | // for now, grouping just works 179 | } 180 | 181 | private void ensureHeadersDrop(FlexibleAdaptable ras, int compoundFilter) { 182 | for (int index = 0; index < ras.size(compoundFilter); ++index) { 183 | Object element = ras.get(compoundFilter, index); 184 | Assert.assertFalse(isCharacter.accept(element)); 185 | } 186 | } 187 | 188 | private void ensureHeadersStay(FlexibleAdaptable ras, int componentFilter, int compoundFilter) { 189 | Set headSet = getHeaders(ras, componentFilter); 190 | Set handSet = getHeaders(ras, compoundFilter); 191 | Assert.assertEquals(headSet, handSet); 192 | } 193 | 194 | private Set getHeaders(FlexibleAdaptable ras, int componentFilter) { 195 | Set headSet = new TreeSet(); 196 | for (int index = 0; index < ras.size(componentFilter); ++index) { 197 | Object element = ras.get(componentFilter, index); 198 | if (isCharacter.accept(element)) { 199 | Assert.assertTrue(ras.indexOf(1, element) >= 0); 200 | headSet.add((Character) element); 201 | } 202 | } 203 | ; 204 | return headSet; 205 | } 206 | 207 | private void checkEvidence(FlexibleAdaptable ras, Map spy) { 208 | int size = ras.size(); 209 | for (int universalIndex = 0; universalIndex < size; ++universalIndex) { 210 | Object element = ras.get(universalIndex); 211 | int[] evidence = spy.get(element); 212 | Assert.assertEquals(1, evidence[0]); 213 | for (int filterIndex = 1; filterIndex < ras.getFilterCount(); ++filterIndex) { 214 | String location = String.format("Element %s[%d] in filter %d", element, universalIndex, filterIndex); 215 | int lookup = ras.indexOf(filterIndex, element); 216 | if (lookup >= 0) { // extra precondition check - make sure the element exists if its index is non-neg 217 | Assert.assertSame(location, ras.get(filterIndex, lookup), element); 218 | } 219 | Assert.assertEquals(location, lookup < 0 ? 0 : 1, evidence[filterIndex]); 220 | } 221 | } 222 | } 223 | 224 | private void assertFilteredIndex(int filterIndex, FlexibleAdaptable ras) { 225 | int size = ras.size(filterIndex); 226 | char c = 0; 227 | for (int elementIndex = 0; elementIndex < size; ++elementIndex) { 228 | Object element = ras.get(filterIndex, elementIndex); 229 | if (element instanceof Character) { 230 | c = (Character) element; 231 | } else { 232 | Assert.assertEquals(String.format("(%d, %d) => ''%s'' begins with '%c'", 233 | filterIndex, elementIndex, String.valueOf(element), c), 234 | c, ((CharSequence) element).charAt(0)); 235 | } 236 | } 237 | } 238 | 239 | @Test 240 | public void testRefreshFilters() throws Exception { 241 | AdaptableFactory builder = createRangeAdaptableFactory(); 242 | builder.setComparator(sampleComparator); 243 | int fa = builder.addFilter(new DivisibleBy(5)); 244 | int fb = builder.addFilter(new DivisibleBy(7)); 245 | int fac = builder.addNarrower(new DivisibleBy(3), fa); 246 | RangedAdaptable ras = builder.create(ranger); 247 | for (int i = 0; i < 1981; ++i) { 248 | ras.add(new Sample()); 249 | } 250 | verifyRanges(ras, 0); 251 | verifyRanges(ras, fa); 252 | verifyRanges(ras, fb); 253 | verifyRanges(ras, fac); 254 | ProjectorEditor editor = ras.getFilterEditor(); 255 | ras.refreshFilters(editor.setFilter(fb, new DivisibleBy(9))); 256 | verifyRanges(ras, fb); 257 | verifyRanges(ras, fac); 258 | ras.refreshFilters(editor.setFilter(fa, new DivisibleBy(7))); 259 | verifyRanges(ras, fa); 260 | verifyRanges(ras, fb); 261 | verifyRanges(ras, fac); 262 | } 263 | 264 | static final Comparator sampleComparator = new Comparator() { 265 | @Override 266 | public int compare(Sample lhs, Sample rhs) { 267 | return lhs.getValue() > rhs.getValue() ? 1 : lhs.getValue() < rhs.getValue() ? -1 268 | : lhs.boundary == rhs.boundary ? 0 269 | : lhs.boundary ? -1 : 1; 270 | } 271 | }; 272 | 273 | Ranger ranger = new Ranger() { 274 | @Override 275 | public Sample getRangeItem(Integer rangeKey) { 276 | return new Sample(rangeKey); 277 | } 278 | 279 | @Override 280 | public Integer getRangeKey(Sample rangeItem) { 281 | return rangeItem.value; 282 | } 283 | 284 | @Override 285 | public boolean accept(Sample item) { 286 | return item.boundary; 287 | } 288 | 289 | @Override 290 | public Iterable qualify(Sample element) { 291 | if (element.boundary) { 292 | return Collections.emptySet(); 293 | } else { 294 | return Collections.singleton(element.getRange()); 295 | } 296 | } 297 | }; 298 | 299 | @Test 300 | public void testReclassification() throws Exception { 301 | AdaptableFactory builder = createRangeAdaptableFactory(); 302 | builder.setComparator(sampleComparator); 303 | RangedAdaptable ras = builder.create(ranger); 304 | for (int i = 0; i < 1981; ++i) { 305 | ras.add(new Sample()); 306 | } 307 | verifyRanges(ras, 0); 308 | Sample.setBaseLine(9084); 309 | ras.updateRangeClassification(); 310 | verifyRanges(ras, 0); 311 | } 312 | 313 | void verifyRanges(RangedAdaptable ras, int filterIndex) { 314 | int lastRange = Integer.MIN_VALUE; 315 | int maxSample = Integer.MIN_VALUE; 316 | int size = ras.size(filterIndex); 317 | int lastCount = 0, lastRealCount = 0; 318 | for (int elementIndex = 0; elementIndex < size; ++elementIndex) { 319 | Sample sample = ras.get(filterIndex, elementIndex); 320 | int value = sample.getValue(); 321 | if (sample.boundary) { 322 | Assert.assertEquals("Element count", lastCount, lastRealCount); 323 | Assert.assertTrue(String.format("Boundary %d exceeds maximum %d", value, maxSample), 324 | value > maxSample); 325 | lastRange = value; 326 | lastCount = ras.getChildCount(sample, filterIndex); 327 | lastRealCount = 0; 328 | } else { 329 | Assert.assertTrue(String.format("Sample %d exceeds last boundary %d", value, lastRange), 330 | value >= lastRange); 331 | maxSample = value; 332 | ++lastRealCount; 333 | } 334 | } 335 | Assert.assertEquals("Element count", lastCount, lastRealCount); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/test/java/com/skype/research/util/adaptable/TestAdaptable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.adaptable.mocks.DivisibleBy; 10 | import com.skype.research.util.primitives.Filter; 11 | import com.skype.research.util.primitives.Update; 12 | import com.skype.research.util.projection.Derivative; 13 | import com.skype.research.util.projection.ProjectorEditor; 14 | import org.junit.Assert; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.junit.runners.Parameterized; 18 | 19 | import java.io.IOException; 20 | import java.util.Arrays; 21 | import java.util.BitSet; 22 | import java.util.Collection; 23 | import java.util.Comparator; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | import java.util.Random; 27 | import java.util.SortedMap; 28 | import java.util.TreeMap; 29 | import java.util.UUID; 30 | import java.util.regex.Pattern; 31 | 32 | /** 33 | * Tests of {@link com.skype.research.util.adaptable.AdaptableSkipList} structure. 34 | */ 35 | @RunWith(Parameterized.class) 36 | public class TestAdaptable { 37 | 38 | @Parameterized.Parameters(name = "DuplicatesAllowed: {0}") 39 | public static Collection data() { 40 | return Arrays.asList(new Object[][] {{Boolean.FALSE}, {Boolean.TRUE}}); 41 | } 42 | 43 | private final boolean allowDuplicates; 44 | 45 | public TestAdaptable(boolean allowDuplicates) { 46 | this.allowDuplicates = allowDuplicates; 47 | } 48 | 49 | private AdaptableFactory createAdaptableFactory() { 50 | AdaptableFactory factory = new AdaptableFactory(); 51 | factory.setAllowDuplicates(allowDuplicates); 52 | return factory; 53 | } 54 | 55 | @Test 56 | public void testSequentialPlacement() throws Exception { 57 | AdaptableFactory ab = createAdaptableFactory(); 58 | FlexibleAdaptable adaptable = ab.create(); 59 | placeAlphabet(adaptable); 60 | dumpContents(adaptable); 61 | assertAlphabet(adaptable); 62 | } 63 | 64 | private void placeAlphabet(FlexibleAdaptable adaptable) { 65 | for (char c = 'A'; c <= 'Z'; ++c) { 66 | adaptable.add(Character.toString(c)); 67 | Validation.validateIntegrity(adaptable); 68 | } 69 | } 70 | 71 | private void dumpContents(FlexibleAdaptable adaptable) { 72 | new Dump(System.out).dump((AdaptableSkipList) adaptable); 73 | } 74 | 75 | @Test 76 | public void testReorderedPlacement() throws Exception { 77 | AdaptableFactory ab = createAdaptableFactory(); 78 | FlexibleAdaptable adaptable = ab.create(); 79 | char[] chars = getAlphabet(); 80 | shuffle(chars, 256); 81 | place(adaptable, chars); 82 | dumpContents(adaptable); 83 | assertAlphabet(adaptable); 84 | } 85 | 86 | @Test 87 | public void testFilteredPlacement() throws Exception { 88 | FlexibleAdaptable adaptable = createFilterableAdaptable(); 89 | char[] chars = getAlphabet(); 90 | shuffle(chars, 256); 91 | place(adaptable, chars); 92 | dumpContents(adaptable); 93 | Assert.assertEquals("placement of A vowel", "A", adaptable.get(1, 0)); 94 | Assert.assertEquals("placement of A chess", "A", adaptable.get(3, 0)); 95 | Assert.assertEquals("count of lower cases", 0, adaptable.size(2)); 96 | Assert.assertNull("first lowercase = null", adaptable.get(2, 0)); 97 | Assert.assertEquals("placement of O vowel", "O", adaptable.get(1, 3)); 98 | Assert.assertEquals("placement of H chess", "H", adaptable.get(3, 7)); 99 | assertAlphabet(adaptable); 100 | } 101 | 102 | final Filter isVowel = new Filter() { 103 | @Override 104 | public boolean accept(String item) { 105 | return item.length() == 1 && "aeiouy".indexOf(item.toLowerCase().charAt(0)) >= 0; 106 | } 107 | }; 108 | 109 | final Filter isChess = new Filter() { 110 | @Override 111 | public boolean accept(String item) { 112 | if (item.length() == 1) { 113 | char c = item.toLowerCase().charAt(0); 114 | if (c >= 'a' && c <= 'h') return true; 115 | } 116 | return false; 117 | } 118 | }; 119 | 120 | final Filter isAlphanumeric = new Filter() { 121 | Pattern pattern = Pattern.compile("^\\w+$"); 122 | @Override 123 | public boolean accept(CharSequence item) { 124 | return pattern.matcher(item).matches(); 125 | } 126 | }; 127 | 128 | final Filter isLowercase = new Filter() { 129 | @Override 130 | public boolean accept(CharSequence item) { 131 | String string = item.toString(); 132 | return string.equals(string.toLowerCase()); 133 | } 134 | }; 135 | 136 | final Filter isSpatial = new Filter() { 137 | Pattern pattern = Pattern.compile("\\s"); 138 | @Override 139 | public boolean accept(CharSequence item) { 140 | return pattern.matcher(item).find(); 141 | } 142 | }; 143 | 144 | private FlexibleAdaptable createFilterableAdaptable() { 145 | AdaptableFactory builder = createAdaptableFactory(); 146 | builder.addFilter(isVowel); 147 | builder.addFilter(isLowercase); 148 | builder.addFilter(isChess); 149 | return builder.create(); 150 | } 151 | 152 | @Test 153 | public void testRemovalByIndex() throws Exception { 154 | FlexibleAdaptable adaptable = createFilterableAdaptable(); 155 | placeAlphabet(adaptable); 156 | dumpContents(adaptable); 157 | // remove all vowels 158 | for(int i = adaptable.size(1) - 1; i >= 0; --i) { 159 | adaptable.remove(1, i); 160 | Validation.validateIntegrity(adaptable); 161 | } 162 | dumpContents(adaptable); 163 | assertConsonants(adaptable); 164 | } 165 | 166 | @Test 167 | public void testRemovalByValue() throws Exception { 168 | FlexibleAdaptable adaptable = createFilterableAdaptable(); 169 | placeAlphabet(adaptable); 170 | dumpContents(adaptable); 171 | // remove all vowels 172 | for(char vowel : "YAIOUEAI".toCharArray()) { 173 | adaptable.remove(Character.toString(vowel)); 174 | Validation.validateIntegrity(adaptable); 175 | } 176 | dumpContents(adaptable); 177 | assertConsonants(adaptable); 178 | } 179 | 180 | final Comparator contentsComparator = new Comparator() { 181 | @Override 182 | public int compare(CharSequence lhs, CharSequence rhs) { 183 | return lhs.toString().compareTo(rhs.toString()); 184 | } 185 | }; 186 | 187 | static final int mutableAdaptableAlphaFilterId = 1; 188 | static final int mutableAdaptableSpaceFilterId = 3; 189 | 190 | private FlexibleAdaptable createMutableFilterableAdaptable() { 191 | AdaptableFactory builder = createAdaptableFactory(); 192 | builder.setComparator(contentsComparator); 193 | builder.addFilter(isAlphanumeric); 194 | builder.addFilter(isLowercase); 195 | builder.addFilter(isSpatial); 196 | return builder.create(); 197 | } 198 | 199 | @Test 200 | public void testUpdateInPlace() throws Exception { 201 | FlexibleAdaptable adaptable = createMutableFilterableAdaptable(); 202 | SortedMap reference = new TreeMap(); 203 | populatePseudoDictionary(adaptable, reference); 204 | // note that we will break container invariants in this test. never do likewise in production! 205 | final int size = adaptable.size(); 206 | final Update distort = createDistortionEdit(); 207 | final Random random = new Random(size); 208 | Map touched = new HashMap(); 209 | int unmatched = 0; 210 | int distorted = 0; 211 | int index; 212 | for (int j = 0; j < 256; ++j) { 213 | do { index = random.nextInt(size); } while (touched.containsKey(index)); 214 | touched.put(index, true); 215 | StringBuilder victimLocator = new StringBuilder(adaptable.get(index)); // copy 216 | if (adaptable.updateInPlace(victimLocator, distort)) { 217 | distorted++; 218 | } 219 | } 220 | index = 0; 221 | for (Map.Entry pair : reference.entrySet()) { 222 | if (!pair.getKey().equals(adaptable.get(index).toString())) { 223 | unmatched++; 224 | } 225 | index++; 226 | } 227 | Assert.assertEquals("distorted = unmatched", distorted, unmatched); 228 | } 229 | 230 | private void populatePseudoDictionary(FlexibleAdaptable adaptable, Map reference) throws IOException { 231 | final int count = 1024; 232 | Random random = new Random(count); 233 | for (int i = 0; i < count; ++i) { 234 | String string = UUID.randomUUID().toString(); 235 | string = string.replace('S', ' '); 236 | string = string.replace('T', '\t'); 237 | if (random.nextBoolean()) { 238 | string = string.toLowerCase(); 239 | } 240 | StringBuilder element = new StringBuilder(string); 241 | adaptable.add(element); 242 | reference.put(string, element); 243 | Validation.validateIntegrity(adaptable); 244 | } 245 | } 246 | 247 | private Update createDistortionEdit() { 248 | return new Update() { 249 | final Random random = new Random(8); 250 | 251 | @Override 252 | public boolean apply(StringBuilder element) { 253 | int length = element.length(); 254 | int subInt = Math.min(length, random.nextInt(8)); 255 | if (subInt == 0) { 256 | return false; // the sequence is unchanged 257 | } 258 | int offset = random.nextInt(length - subInt); 259 | if (random.nextBoolean()) { 260 | // replace with garbage 261 | for (int i = 0; i < subInt; ++i) { 262 | element.setCharAt(offset++, (char) (random.nextInt(128) + 128)); 263 | } 264 | } else { 265 | // punch 266 | element.replace(offset, offset + subInt, ""); 267 | } 268 | return true; 269 | } 270 | }; 271 | } 272 | 273 | @Test 274 | public void testUpdateFilters() throws Exception { 275 | FlexibleAdaptable adaptable = createMutableFilterableAdaptable(); 276 | SortedMap reference = new TreeMap(); 277 | populatePseudoDictionary(adaptable, reference); 278 | final Random random = new Random(128); 279 | final Update appendGarbage = new Update() { 280 | @Override 281 | public boolean apply(StringBuilder element) { 282 | char c = (char) random.nextInt(128); 283 | if (c == 0) { 284 | return false; 285 | } else { 286 | element.append(c); 287 | return true; 288 | } 289 | } 290 | }; 291 | for (int index = 0; index < 2048; ++index) { 292 | int randomIndex = random.nextInt(adaptable.size()); 293 | StringBuilder randomElement = adaptable.get(randomIndex); 294 | StringBuilder victimLocator = new StringBuilder(randomElement); // copy 295 | adaptable.updateFilters(victimLocator, appendGarbage); 296 | } 297 | Validation.validateIntegrity(adaptable); 298 | int plainIndex = 0; 299 | StringBuilder prevElement = adaptable.get(plainIndex); 300 | for (plainIndex = 1; plainIndex < adaptable.size(); ++plainIndex) { 301 | StringBuilder nextElement = adaptable.get(plainIndex); 302 | Assert.assertTrue("Ordering holds", contentsComparator.compare(prevElement, nextElement) < 0); 303 | prevElement = nextElement; 304 | } 305 | int alphaIndex = 0, spaceIndex = 0; 306 | for (StringBuilder element : reference.values()) { 307 | if (isAlphanumeric.accept(element)) { 308 | Assert.assertSame(String.format("Filter 1 reference integrity [%d]", alphaIndex), element, 309 | adaptable.get(mutableAdaptableAlphaFilterId, alphaIndex)); 310 | alphaIndex++; 311 | } 312 | if (isSpatial.accept(element)) { 313 | Assert.assertSame(String.format("Filter 3 reference integrity [%d]", spaceIndex), element, 314 | adaptable.get(mutableAdaptableSpaceFilterId, spaceIndex)); 315 | spaceIndex++; 316 | } 317 | } 318 | Assert.assertEquals("Alpha element count", alphaIndex, adaptable.size(mutableAdaptableAlphaFilterId)); 319 | Assert.assertEquals("Space element count", spaceIndex, adaptable.size(mutableAdaptableSpaceFilterId)); 320 | } 321 | 322 | @Test 323 | public void testUpdateReorder() throws Exception { 324 | FlexibleAdaptable adaptable = createMutableFilterableAdaptable(); 325 | SortedMap reference = new TreeMap(); 326 | populatePseudoDictionary(adaptable, reference); 327 | // we remove and re-add elements into the reference structure to verify reordering 328 | Update invertOrder = new Update() { 329 | @Override 330 | public boolean apply(StringBuilder element) { 331 | String original = element.toString(); 332 | int length = element.length(); 333 | int middle = length / 2; 334 | for (int i = 0; i < middle; ++i) { 335 | int inverse = length - 1 - i; 336 | char tmp = element.charAt(i); 337 | element.setCharAt(i, element.charAt(inverse)); 338 | element.setCharAt(inverse, tmp); 339 | } 340 | return !original.equals(element.toString()); 341 | } 342 | }; 343 | int edits = 512; 344 | Random random = new Random(edits); 345 | for (int i = 0; i < edits; ++i) { 346 | int randomIndex = random.nextInt(adaptable.size()); 347 | StringBuilder victim = adaptable.get(randomIndex); 348 | StringBuilder victimLocator = new StringBuilder(victim); 349 | StringBuilder backup = new StringBuilder(victim); 350 | adaptable.updateReorder(victimLocator, invertOrder); 351 | // 352 | reference.remove(backup.toString()); 353 | invertOrder.apply(backup); 354 | reference.put(backup.toString(), backup); 355 | } 356 | int index = 0; 357 | for (String element : reference.keySet()) { 358 | Assert.assertEquals("Container contents are identical", element, adaptable.get(index).toString()); 359 | index++; 360 | } 361 | Assert.assertEquals("Container size is identical", reference.size(), adaptable.size()); 362 | } 363 | 364 | private void assertConsonants(Adaptable adaptable) { 365 | char[] charArray = "BCDFGH".toCharArray(); 366 | for (int i = 0; i < charArray.length; i++) { 367 | char c = charArray[i]; 368 | String consonant = Character.toString(c); 369 | Assert.assertEquals(consonant, consonant, adaptable.get(3, i)); 370 | } 371 | } 372 | 373 | private void place(FlexibleAdaptable adaptable, char[] chars) { 374 | for (char c : chars) { 375 | adaptable.add(Character.toString(c)); 376 | Validation.validateIntegrity(adaptable); 377 | } 378 | } 379 | 380 | private char[] getAlphabet() { 381 | StringBuilder stringBuilder = new StringBuilder(); 382 | for (char c = 'A'; c <= 'Z'; ++c) { 383 | stringBuilder.append(c); 384 | } 385 | return stringBuilder.toString().toCharArray(); 386 | } 387 | 388 | private void shuffle(char[] chars, int permutations) { 389 | Random random = new Random(); 390 | char tmp; 391 | int p, q; 392 | for (int i = 0; i < permutations; ++i) { 393 | p = random.nextInt(chars.length); 394 | q = random.nextInt(chars.length); 395 | if (p != q) { 396 | tmp = chars[p]; 397 | chars[p] = chars[q]; 398 | chars[q] = tmp; 399 | } 400 | } 401 | } 402 | 403 | private void assertAlphabet(Adaptable adaptable) { 404 | for (char c = 'A'; c < 'Z'; ++c) { 405 | Assert.assertEquals(String.format("adaptable[%d]", c - 'A'), 406 | Character.toString(c), 407 | adaptable.get(c - 'A')); 408 | } 409 | } 410 | 411 | final Filter isEven = new Filter() { 412 | @Override 413 | public boolean accept(Integer item) { 414 | return item % 2 == 0; 415 | } 416 | }; 417 | final Filter isPowerOf2 = new Filter() { 418 | @Override 419 | public boolean accept(Integer item) { 420 | return Long.bitCount(item) == 1; 421 | } 422 | }; 423 | final Filter isAlpha = new Filter() { 424 | @Override 425 | public boolean accept(Integer item) { 426 | return Character.isLetter(item); 427 | } 428 | }; 429 | 430 | @Test 431 | public void testFilterChange() throws Exception { 432 | // create 433 | FlexibleAdaptable adaptable = createIntegerSkipList(); 434 | // select 435 | int[] evenNumbers = takeSelectionSnapshot(adaptable, 1); 436 | int[] alphaLetters = takeSelectionSnapshot(adaptable, 3); 437 | // modify and verify 438 | ProjectorEditor filterEditor = adaptable.getFilterEditor(); 439 | adaptable.refreshFilters(filterEditor.setFilter(1, isAlpha)); 440 | assertSameSelection(alphaLetters, adaptable, 1); 441 | adaptable.refreshFilters(filterEditor.setFilter(2, isEven)); 442 | assertSameSelection(evenNumbers, adaptable, 2); 443 | assertSameSelection(alphaLetters, adaptable, 1); 444 | } 445 | 446 | @Test 447 | public void testDependencies() throws Exception { 448 | AdaptableFactory builder = createAdaptableFactory(); 449 | builder.addFilter(isEven); 450 | builder.addFilter(isPowerOf2); 451 | builder.addFilter(isAlpha); 452 | Derivative inter = new Derivative() { 453 | @Override 454 | public boolean accept(int... args) { 455 | return args[0] * args[1] != 0; 456 | } 457 | }; 458 | int indexOfInter = builder.addDerivativeOf(inter, 1, 3); 459 | Derivative union = new Derivative() { 460 | @Override 461 | public boolean accept(int... args) { 462 | return (args[0] | args[1]) != 0; 463 | } 464 | }; 465 | int indexOfUnion = builder.addDerivativeOf(union, 1, 3); 466 | Derivative delta = new Derivative() { 467 | @Override 468 | public boolean accept(int... args) { 469 | return args[0] != args[1]; 470 | } 471 | }; 472 | int indexOfDelta = builder.addDerivativeOf(delta, 1, 3); 473 | int indexOfEmpty = builder.addDerivativeOf(inter, inter, delta); 474 | FlexibleAdaptable adaptable = builder.create(); 475 | // now fill 476 | fillWithIntegers(adaptable); 477 | // now check 478 | Assert.assertEquals(0, adaptable.size(indexOfEmpty)); 479 | Assert.assertEquals(adaptable.size(indexOfUnion), 480 | adaptable.size(indexOfInter) + adaptable.size(indexOfDelta)); 481 | adaptable.refreshFilters(adaptable.getFilterEditor().setDerivative(indexOfDelta, inter)); 482 | assertSameSelection(adaptable, indexOfInter, indexOfEmpty); // now empty is inter&inter 483 | // now play with inter itself 484 | // 1 was Even, 3 was Alpha 485 | int[] originalIntersection = takeSelectionSnapshot(adaptable, indexOfInter); 486 | adaptable.refreshFilters(adaptable.getFilterEditor().setFilter(3, isEven)); 487 | assertSameSelection(adaptable, 1, indexOfInter); 488 | adaptable.refreshFilters(adaptable.getFilterEditor().setFilter(1, isAlpha)); 489 | assertSameSelection(originalIntersection, adaptable, indexOfInter); 490 | } 491 | 492 | private static void assertSameSelection(FlexibleAdaptable adaptable, int alice, int bobby) { 493 | final int size = adaptable.size(bobby); 494 | Assert.assertEquals(adaptable.size(alice), size); 495 | for (int index = 0; index < size; ++index) { 496 | Assert.assertEquals(adaptable.get(alice, index), adaptable.get(bobby, index)); 497 | } 498 | } 499 | 500 | private void fillWithIntegers(FlexibleAdaptable adaptable) { 501 | final Random random = new Random(2048); 502 | for (int i = 0; i < 2048; ++i) { 503 | adaptable.add(random.nextInt(1 << 17)); 504 | if (random.nextInt(256) == 0) { 505 | Validation.validateIntegrity(adaptable); 506 | } 507 | } 508 | } 509 | 510 | private FlexibleAdaptable createIntegerSkipList() { 511 | FlexibleAdaptable adaptable = createEmptyIntegerSkipList(); 512 | fillWithIntegers(adaptable); 513 | return adaptable; 514 | } 515 | 516 | private FlexibleAdaptable createEmptyIntegerSkipList() { 517 | AdaptableFactory builder = createAdaptableFactory(); 518 | builder.addFilter(isEven); 519 | builder.addFilter(isPowerOf2); 520 | builder.addFilter(isAlpha); 521 | return builder.create(); 522 | } 523 | 524 | private int[] takeSelectionSnapshot(FlexibleAdaptable adaptable, int filterIndex) { 525 | int[] selection = new int[adaptable.size(filterIndex)]; 526 | for (int elementIndex = 0; elementIndex < selection.length; elementIndex++) { 527 | selection[elementIndex] = adaptable.get(filterIndex, elementIndex); 528 | } 529 | return selection; 530 | } 531 | 532 | private void assertSameSelection(int[] selection, 533 | FlexibleAdaptable adaptable, int newFilterIndex) { 534 | Assert.assertEquals("Selection count", selection.length, adaptable.size(newFilterIndex)); 535 | for (int elementIndex = 0; elementIndex < selection.length; elementIndex++) { 536 | Assert.assertEquals(String.format("selection[%d]", elementIndex), selection[elementIndex], 537 | adaptable.get(newFilterIndex, elementIndex).intValue()); 538 | } 539 | } 540 | 541 | @Test 542 | public void testConvertIndex() throws Exception { 543 | // create 544 | FlexibleAdaptable adaptable = createIntegerSkipList(); 545 | 546 | /// 1. convert from universal to specific 547 | validateIndexConversion(adaptable, 0, 1, isEven); 548 | validateIndexConversion(adaptable, 0, 3, isAlpha); 549 | /// 2. convert from one specific to another specific 550 | validateIndexConversion(adaptable, 3, 1, isEven); 551 | validateIndexConversion(adaptable, 1, 3, isAlpha); 552 | /// 3. make sure conversions to universal exist and match 553 | validateIndexConversion(adaptable, 1, 0, Trivial.universeFilter()); 554 | validateIndexConversion(adaptable, 3, 0, Trivial.universeFilter()); 555 | } 556 | 557 | @Test 558 | public void testIndexOf() throws Exception { 559 | // create 560 | FlexibleAdaptable adaptable = createIntegerSkipList(); 561 | // test hits 562 | for (int filterIndex = 0; filterIndex < adaptable.getFilterCount(); ++filterIndex) { 563 | final int size = adaptable.size(filterIndex); 564 | for (int elementIndex = 0; elementIndex < size; ++elementIndex) { 565 | Integer element = adaptable.get(filterIndex, elementIndex); 566 | int reverseLookup = adaptable.indexOf(filterIndex, element); 567 | if (allowDuplicates) { 568 | // if duplicates are allowed 569 | Integer similar = adaptable.get(filterIndex, reverseLookup); 570 | Assert.assertEquals(String.format("Reverse lookup of %d", element), 571 | element, similar); 572 | } else { 573 | Assert.assertEquals(String.format("Reverse lookup of %d", element), 574 | elementIndex, reverseLookup); 575 | } 576 | } 577 | } 578 | final int size = adaptable.size(0); 579 | for (int elementIndex = 0; elementIndex < size; ++elementIndex) { 580 | Integer element = adaptable.get(0, elementIndex); 581 | // I agree it is inefficient to create the string every time 582 | String missing = String.format("Reverse lookup of missing %d", element); 583 | if (!isEven.accept(element)) { 584 | Assert.assertEquals(missing, -1, adaptable.indexOf(1)); 585 | } 586 | if (!isAlpha.accept(element)) { 587 | Assert.assertEquals(missing, -1, adaptable.indexOf(3)); 588 | } 589 | } 590 | // test misses 591 | final Random random = new Random(2048); 592 | for (int i = 0; i < 2048; ++i) { 593 | int element = random.nextInt(1 << 17); 594 | int filterIndex = random.nextInt(adaptable.getFilterCount()); 595 | int reverseLookup = adaptable.indexOf(filterIndex, element); 596 | int unfilteredSize = adaptable.size(); 597 | int filteredSize = adaptable.size(filterIndex); 598 | if (reverseLookup != -1) { 599 | Assert.assertTrue(String.format("Must be able to remove %d in %d", reverseLookup, filterIndex), 600 | adaptable.remove(filterIndex, reverseLookup)); 601 | Assert.assertTrue("New selection size is 1 less", filteredSize - 1 == adaptable.size(filterIndex)); 602 | Assert.assertTrue("New universe size is 1 less", unfilteredSize - 1 == adaptable.size()); 603 | } 604 | } 605 | } 606 | 607 | private void validateIndexConversion(FlexibleAdaptable adaptable, 608 | int sourceFilterIndex, int targetFilterIndex, 609 | Filter targetFilter) { 610 | boolean sourceIsUniversal = sourceFilterIndex == adaptable.getUniverseFilterIndex(); 611 | int targetSelectionCount = adaptable.size(targetFilterIndex); 612 | BitSet bitSet = null; 613 | if (sourceIsUniversal) { 614 | bitSet = new BitSet(targetSelectionCount); 615 | } 616 | for (int index = 0; index < adaptable.size(sourceFilterIndex); ++index) { 617 | Integer element = adaptable.get(sourceFilterIndex, index); 618 | boolean fits = targetFilter.accept(element); 619 | // DO IT NOW 620 | int convertedIndex = adaptable.convertIndex(index, sourceFilterIndex, targetFilterIndex, false); 621 | // if the resulting index is out of bounds, trim it 622 | Integer elementAtConvertedIndex = convertedIndex < 0 ? Integer.MIN_VALUE : 623 | convertedIndex >= targetSelectionCount ? Integer.MAX_VALUE : 624 | adaptable.get(targetFilterIndex, convertedIndex); 625 | if (fits) { 626 | /// if specific passes, must match universal 627 | Assert.assertEquals("Lookup by converted index matches lookup by original index", 628 | element, elementAtConvertedIndex); 629 | if (sourceIsUniversal) { 630 | bitSet.set(convertedIndex); 631 | } 632 | } else { 633 | /// if specific does not pass, SHOULD fall in between 634 | Assert.assertTrue(String.format("Ceiling of [%d]=%d is [%d]=%d", 635 | index, element, convertedIndex, elementAtConvertedIndex), 636 | element > elementAtConvertedIndex); 637 | if (convertedIndex >= 0 && convertedIndex + 1 < targetSelectionCount) { 638 | Integer elementAtNextIndex = adaptable.get(targetFilterIndex, convertedIndex + 1); 639 | Assert.assertTrue(String.format("Floor of [%d]=%d is [%d]=%d", 640 | index, element, convertedIndex, elementAtNextIndex), 641 | element < elementAtNextIndex); 642 | } 643 | // and test ceiling too 644 | } 645 | } 646 | if (sourceIsUniversal) { 647 | bitSet.flip(0, targetSelectionCount); 648 | Assert.assertTrue("Universe includes selection fully", 649 | bitSet.isEmpty()); 650 | } 651 | } 652 | 653 | @Test 654 | public void testIterators() throws Exception { 655 | AdaptableFactory builder = createAdaptableFactory(); 656 | for (int i = 1, divisor = 0; i <= 10; ++i) { 657 | divisor<<=1; 658 | divisor++; 659 | builder.addFilter(new DivisibleBy(divisor)); 660 | } 661 | FlexibleAdaptable adaptable = builder.create(); 662 | fillWithIntegers(adaptable); 663 | // new Dump(System.err).dump((AdaptableSkipList) adaptable); 664 | Validation.validateIntegrity(adaptable); 665 | Validation.validateIterators((AdaptableSkipList) adaptable); 666 | Validation.validateIterators(adaptable); // factory policy 667 | } 668 | } 669 | -------------------------------------------------------------------------------- /src/main/java/com/skype/research/util/adaptable/AdaptableSkipList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | package com.skype.research.util.adaptable; 8 | 9 | import com.skype.research.util.primitives.Update; 10 | import com.skype.research.util.projection.CompositeProjector; 11 | import com.skype.research.util.projection.CompositeProjectorImpl; 12 | import com.skype.research.util.projection.Projector; 13 | import com.skype.research.util.projection.ProjectorEditor; 14 | 15 | import java.lang.reflect.Array; 16 | import java.util.Arrays; 17 | import java.util.BitSet; 18 | import java.util.Comparator; 19 | import java.util.Iterator; 20 | import java.util.LinkedList; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Random; 24 | 25 | import static com.skype.research.util.adaptable.Distance.isZero; 26 | import static com.skype.research.util.adaptable.Distance.set; 27 | import static com.skype.research.util.adaptable.Distance.sub; 28 | 29 | /** 30 | * {@link Adaptable} implementation with a skip list container with tracked edge lengths. 31 | */ 32 | public class AdaptableSkipList implements FlexibleAdaptable, Projector { 33 | 34 | final int levelCount; 35 | final int orbitLevel; // top, guaranteed to have only one link on it 36 | final int cloudLevel; // highest level allowed for real nodes 37 | // 38 | final int denominator; 39 | 40 | static final BitSet EMPTY = new BitSet(); 41 | final Tracker doNotTrack = new Tracker(); 42 | final Meter doNotMeasure = new Meter(); 43 | 44 | // no use counting indices beyond it 45 | /* package */ int horizon; 46 | 47 | abstract class Locator { 48 | abstract boolean hasNext(Node node, int level); 49 | abstract Node next(int level); 50 | abstract boolean exactMatch(); 51 | } 52 | 53 | final class ExactLocator extends Locator { 54 | final Node boundary; 55 | Node nextNode; 56 | 57 | ExactLocator(Node boundary) { 58 | this.boundary = boundary; 59 | } 60 | 61 | @Override 62 | final boolean hasNext(Node node, int level) { 63 | nextNode = node.nodes[level]; 64 | return boundary != nextNode; 65 | } 66 | 67 | @Override 68 | final Node next(int level) { 69 | return nextNode; 70 | } 71 | 72 | final boolean exactMatch() { 73 | return true; 74 | } 75 | } 76 | 77 | abstract class AimingLocator extends Locator { 78 | int comparison; 79 | 80 | final boolean hasNext(Node node, int level) { 81 | comparison = evaluateNextStep(node, level); 82 | return comparison > 0; 83 | } 84 | 85 | abstract int evaluateNextStep(Node node, int level); 86 | 87 | final boolean exactMatch() { 88 | return comparison == 0; 89 | } 90 | } 91 | 92 | class ValueLocator extends AimingLocator { 93 | final T value; 94 | Node nextNode; 95 | public ValueLocator(T value) { 96 | this.value = value; 97 | } 98 | 99 | @Override 100 | final int evaluateNextStep(Node node, int level) { 101 | nextNode = node.nodes[level]; 102 | return nextNode == null 103 | ? -1 // next is absMax 104 | : comparator.compare(value, nextNode.element); 105 | } 106 | 107 | @Override 108 | final Node next(int level) { 109 | return nextNode; 110 | } 111 | } 112 | 113 | class IndexLocator extends AimingLocator { 114 | final int filterIndex, elementIndex; 115 | int lookupIndex = -1, nextIndex; 116 | Node currentNode; 117 | 118 | IndexLocator(int filterIndex, int elementIndex) { 119 | this.filterIndex = filterIndex; 120 | this.elementIndex = elementIndex; 121 | } 122 | 123 | int evaluateNextStep(Node node, int level) { 124 | currentNode = node; 125 | nextIndex = lookupIndex + node.distances[level][filterIndex]; 126 | return elementIndex - nextIndex; 127 | } 128 | 129 | @Override 130 | public Node next(int level) { 131 | lookupIndex = nextIndex; 132 | return currentNode.nodes[level]; 133 | } 134 | } 135 | 136 | final class Navigator { 137 | Node node = absMinNode; 138 | int level = orbitLevel; 139 | 140 | final Tracker tracker; 141 | final Meter meter; 142 | 143 | public Navigator(Tracker tracker, Meter meter) { 144 | this.tracker = tracker; 145 | this.meter = meter; 146 | } 147 | 148 | private boolean navForward(Locator locator) { 149 | while (locator.hasNext(node, level)) { 150 | meter.addDistance(node, level); 151 | node = locator.next(level); 152 | } 153 | return locator.exactMatch(); 154 | } 155 | 156 | private void markLevel() { 157 | tracker.setNextNode(level, node); 158 | meter.mark(this.tracker, level); 159 | } 160 | 161 | final boolean descend(Locator locator, boolean stopOnExactMatch) { 162 | markLevel(); 163 | while (level > 0) { 164 | --level; 165 | if (navForward(locator) && stopOnExactMatch) { 166 | return true; 167 | } 168 | markLevel(); 169 | } 170 | return false; 171 | } 172 | 173 | /** 174 | * Descend to the node that contains a specific value recording predecessor nodes on each level. 175 | * @param element value to find 176 | * @return container node, or {@link #absMinNode} if the value is not found. 177 | */ 178 | final Node descendTo(T element) { 179 | // alternatively, we could inject Locator every time and decouple the two hierarchies 180 | final ValueLocator locator = new ValueLocator(element); 181 | if (descend(locator, true)) { 182 | // post-descend 183 | final Node found = locator.nextNode; 184 | descend(new ExactLocator(found), false); 185 | return found; 186 | } 187 | return absMinNode; 188 | } 189 | 190 | /** 191 | * 192 | * @param filterIndex 193 | * @param elementIndex 194 | * @return 195 | */ 196 | final Node descendTo(int filterIndex, int elementIndex) { 197 | if (elementIndex >= 0 && elementIndex < size(filterIndex)) { 198 | final IndexLocator locator = new IndexLocator(filterIndex, elementIndex); 199 | descend(locator, false); 200 | return locator.currentNode.nodes[0]; 201 | } 202 | return absMinNode; 203 | } 204 | } 205 | 206 | class Meter { 207 | int[] getPosition() { return zero; } 208 | void addDistance(Node node, int level) {} 209 | void mark(Tracker tracker, int level) {} 210 | } 211 | 212 | class ScalarMeter extends Meter { 213 | final int filterIndex; 214 | int position = -1; 215 | 216 | ScalarMeter(int filterIndex) { 217 | this.filterIndex = filterIndex; 218 | } 219 | 220 | @Override 221 | final void addDistance(Node node, int level) { 222 | position += node.distances[level][filterIndex]; 223 | } 224 | } 225 | 226 | class VectorMeter extends Meter { 227 | final int[] position = newDistance(); 228 | 229 | @Override 230 | final void addDistance(Node node, int level) { 231 | Distance.add(position, node.distances[level], horizon); 232 | } 233 | 234 | @Override 235 | final void mark(Tracker tracker, int level) { 236 | tracker.setDistance(level, position); 237 | } 238 | 239 | @Override 240 | final int[] getPosition() { 241 | return position; 242 | } 243 | } 244 | 245 | class Tracker { 246 | void setNextNode(int level, Node next) {} 247 | void setDistance(int level, int[] value) {} 248 | } 249 | 250 | class Section extends Tracker { 251 | // http://stackoverflow.com/questions/529085/how-to-create-a-generic-array-in-java 252 | final Node[] nodes = Node[].class.cast(Array.newInstance(Node.class, levelCount)); 253 | 254 | @Override 255 | final void setNextNode(int level, Node next) { 256 | nodes[level] = next; 257 | } 258 | } 259 | 260 | class Gap extends Section { 261 | 262 | final int[][] distances = new int[levelCount][filterCount]; 263 | 264 | @Override 265 | final void setDistance(int level, int[] value) { 266 | set(distances[level], value, horizon); 267 | } 268 | } 269 | 270 | protected int[] newDistance() { 271 | return new int[filterCount]; 272 | } 273 | 274 | class Node extends Gap implements Map.Entry { 275 | final T element; 276 | final int level; 277 | 278 | Node(T element, int nodeLevel) { 279 | this.element = element; 280 | this.level = nodeLevel; 281 | } 282 | 283 | @Override 284 | public final T getKey() { 285 | return element; 286 | } 287 | 288 | @Override 289 | public final int[] getValue() { 290 | return distances[0]; 291 | } 292 | 293 | @Override 294 | public final int[] setValue(int[] object) { 295 | throw new UnsupportedOperationException(); // client should not overwrite filters 296 | } 297 | } 298 | 299 | // raw materials 300 | final Random random = new Random(); 301 | 302 | // sub-products 303 | Comparator comparator, pendingComparator; 304 | final int[] zero; 305 | 306 | // structural 307 | Node absMinNode; 308 | final int universeFilter; 309 | boolean allowDuplicates; 310 | boolean broadcastOldValue; 311 | boolean positionUnaware; 312 | boolean autoAdd; 313 | 314 | // multiple representations 315 | final CompositeProjector projector; 316 | final int filterCount; // cached, uninitialized 317 | 318 | // observation 319 | final List> observers = new LinkedList>(); 320 | 321 | public AdaptableSkipList(int levelCount, int denominator) { 322 | //noinspection unchecked 323 | this(levelCount, denominator, Trivial.naturalOrder(), new CompositeProjectorImpl()); 324 | } 325 | 326 | public AdaptableSkipList(int levelCount, int denominator, Comparator comparator, CompositeProjectorImpl projector) { 327 | //noinspection unchecked 328 | this(levelCount, denominator, comparator, 0, projector); 329 | } 330 | 331 | public AdaptableSkipList(int levelCount, int denominator, int universeFilter, CompositeProjectorImpl projector) { 332 | //noinspection unchecked 333 | this(levelCount, denominator, Trivial.naturalOrder(), universeFilter, projector); 334 | } 335 | 336 | public AdaptableSkipList(int levelCount, int denominator, Comparator comparator, int universeFilter, CompositeProjector projector) { 337 | this.levelCount = levelCount; 338 | orbitLevel = levelCount - 1; 339 | cloudLevel = orbitLevel - 1; 340 | this.denominator = denominator; 341 | this.comparator = comparator; 342 | this.pendingComparator = comparator; 343 | this.universeFilter = universeFilter; 344 | this.projector = projector; 345 | this.filterCount = projector.getFilterCount(); 346 | this.horizon = projector.getHorizon(); 347 | zero = newDistance(); 348 | absMinNode = new Node(null, orbitLevel); 349 | } 350 | 351 | /** 352 | * Ensure determinism. Good for unit testing and other repeatable scenarios. 353 | * @param seed internal random number generator seed 354 | */ 355 | public void setSeed(long seed) { 356 | random.setSeed(seed); 357 | } 358 | 359 | public void setAllowDuplicates(boolean allowDuplicates) { 360 | this.allowDuplicates = allowDuplicates; 361 | } 362 | 363 | /** 364 | * If set to true, rank-preserving updates in {@link #updateReorder(Object, Update)} 365 | * broadcast a full remove-insert in order to deliver both the old and the new value 366 | * of the item being edited. 367 | * 368 | * If set to false, a single "updated" is broadcast in this case, with the new value. 369 | * 370 | * @param broadcastOldValue true to guarantee old value broadcasting, 371 | * false to send a single "changed" update. 372 | */ 373 | public void setBroadcastOldValue(boolean broadcastOldValue) { 374 | this.broadcastOldValue = broadcastOldValue; 375 | } 376 | 377 | /** 378 | * If set to true, add/update/delete positions are not computed and are instead set to 0. 379 | * Useful if clients can only update their datasets in whole (as opposed to per-element). 380 | * 381 | * @param positionUnaware suppress delivery of incremental updates and only deliver bulk updates 382 | */ 383 | public void setPositionUnaware(boolean positionUnaware) { 384 | this.positionUnaware = positionUnaware; 385 | } 386 | 387 | protected void onElementUpdated(T element, int[] position, int[] estimate, int deltaSign, int[] deltaCount) { 388 | if (!observers.isEmpty()){ 389 | for (ElementObserver observer : observers) { 390 | observer.onElementUpdated(element, position, estimate, deltaSign, deltaCount); 391 | } 392 | } 393 | } 394 | 395 | /** 396 | * Implements container-specific projection logic. 397 | * By default, condition evaluation is delegated to the {@link Projector} provided upon construction 398 | * or assembled part by part with {@link AdaptableFactory}. 399 | * However, specializations of the container may introduce their own state-dependent logic, 400 | * such as "failing" or "passing" a group item based on whether the group is empty or non-empty. 401 | * 402 | * (Example: allow the alphabet caption "A" in a selection 403 | * if, and only if, names starting on "A" are present in the same selection.) 404 | * 405 | * Direct use by the client is allowed but only makes sense in complicated scenarios 406 | * (for instance, to evaluate the impact of element addition 407 | * or removal before actually adding or removing an element). 408 | * 409 | * @param element element to evaluate. 410 | * @param filterIndex condition index to evaluate. 411 | * @param precomputed pre-evaluated conditions (potential dependencies of the current one) 412 | * @return true if the element passes the condition, false otherwise. 413 | */ 414 | @Override 415 | public boolean accept(T element, int filterIndex, int[] precomputed) { 416 | return projector.accept(element, filterIndex, precomputed); 417 | } 418 | 419 | @Override 420 | public int size() { 421 | return size(universeFilter); 422 | } 423 | 424 | @Override 425 | public int size(int filterIndex) { 426 | return absMinNode.distances[orbitLevel][filterIndex]; 427 | } 428 | 429 | @Override 430 | public int getFilterCount() { 431 | return filterCount; 432 | } 433 | 434 | final int compareWithNextNode(T prev, Node next) { 435 | return prev == null || next == null 436 | ? -1 // prev is absMin or next is absMax 437 | : comparator.compare(prev, next.element); 438 | } 439 | 440 | private int randomLevel() { 441 | int level = 0; 442 | while (level < cloudLevel && random.nextInt(denominator) == 0) { 443 | ++level; 444 | // "fix-up" optimization: never grow up faster than one level at a time. 445 | if (absMinNode.nodes[level] == null) { 446 | break; 447 | } 448 | } 449 | return level; 450 | } 451 | 452 | @Override 453 | public boolean add(T element) { 454 | return addPrecomputedDistance(element, project(element)); 455 | } 456 | 457 | private boolean addPrecomputedDistance(T element, int[] projection) { 458 | // insert sorted 459 | final Locator locator = new ValueLocator(element); 460 | final VectorMeter meter = new VectorMeter(); 461 | 462 | Gap tracker = new Gap(); 463 | final Navigator navigator = new Navigator(tracker, meter); 464 | if (navigator.descend(locator, !allowDuplicates)) { 465 | return false; 466 | } 467 | finishAddition(element, projection, meter, tracker); 468 | return true; 469 | } 470 | 471 | private void finishAddition(T element, int[] projection, VectorMeter meter, Gap tracker) { 472 | int level = 0; 473 | final int[] position = meter.getPosition(); 474 | int[] ceiling = set(newDistance(), position, horizon); 475 | Distance.add(ceiling, projection, horizon); 476 | final int nodeLevel = randomLevel(); 477 | final int[] temp = newDistance(); 478 | Node inserted = new Node(element, nodeLevel); 479 | boolean split = true; 480 | do { 481 | Node prev = tracker.nodes[level]; 482 | Distance.add(prev.distances[level], projection, horizon); 483 | if (split &= level <= nodeLevel) { 484 | // connections 485 | Node next = prev.nodes[level]; 486 | inserted.setNextNode(level, next); 487 | prev.setNextNode(level, inserted); 488 | // edge lengths 489 | sub(set(temp, ceiling, horizon), tracker.distances[level], horizon); 490 | sub(set(inserted.distances[level], prev.distances[level], horizon), temp, horizon); 491 | set(prev.distances[level], temp, horizon); 492 | } 493 | } while (++level <= orbitLevel); 494 | onElementUpdated(element, position, projection, 1, projection); 495 | } 496 | 497 | private boolean removeNodeAtSection(Section section, Node container, int[] position) { 498 | if (container == absMinNode) { 499 | return false; 500 | } 501 | final int[] oldEdge = set(newDistance(), section.nodes[0].distances[0], horizon); 502 | adjustDistance(section, -1, oldEdge); 503 | finishRemoval(section, container); 504 | onElementUpdated(container.element, position, oldEdge, -1, oldEdge); 505 | return true; 506 | } 507 | 508 | private void finishRemoval(Section section, Node container) { 509 | for (int level = orbitLevel; level >= 0; --level) { 510 | Node prev = section.nodes[level]; 511 | if (prev.nodes[level] == container) { 512 | // merge idiom 513 | Distance.add(prev.distances[level], container.distances[level], horizon); 514 | prev.setNextNode(level, container.nodes[level]); 515 | } 516 | } 517 | } 518 | 519 | @Override 520 | public boolean remove(T element) { 521 | Section tracker = new Section(); 522 | Meter meter = allocateMeterForReporting(); 523 | final Navigator navigator = new Navigator(tracker, meter); 524 | Node node = navigator.descendTo(element); 525 | return removeNodeAtSection(tracker, node, meter.getPosition()); 526 | } 527 | 528 | @Override 529 | public boolean remove(int filterIndex, int elementIndex) { 530 | final Section section = new Section(); 531 | final Meter meter = allocateMeterForReporting(); 532 | final Navigator navigator = new Navigator(section, meter); 533 | final Node node = navigator.descendTo(filterIndex, elementIndex); 534 | return removeNodeAtSection(section, node, meter.getPosition()); 535 | } 536 | 537 | @Override 538 | public void clear() { 539 | int[] size = absMinNode.distances[orbitLevel]; 540 | comparator = pendingComparator; 541 | horizon = projector.getHorizon(); 542 | absMinNode = new Node(null, orbitLevel); 543 | onElementUpdated(null, zero, size, -1, size); 544 | } 545 | 546 | @Override 547 | public T get(int filterIndex, int elementIndex) { 548 | final Navigator navigator = new Navigator(doNotTrack, doNotMeasure); 549 | Node node = navigator.descendTo(filterIndex, elementIndex); 550 | return node.element; 551 | } 552 | 553 | @Override 554 | public int indexOf(T item) { 555 | return indexOf(universeFilter, item); 556 | } 557 | 558 | @Override 559 | public int indexOf(int filterIndex, T element) { 560 | final ValueLocator locator = new ValueLocator(element); 561 | final ScalarMeter meter = new ScalarMeter(filterIndex); 562 | final Tracker tracker = doNotTrack; 563 | final Navigator navigator = new Navigator(tracker, meter); 564 | if (navigator.descend(locator, true)) { 565 | final Node found = locator.nextNode; 566 | navigator.descend(new ExactLocator(found), false); 567 | final Node predecessor = navigator.node; 568 | final int projection = predecessor.distances[0][filterIndex]; 569 | return projection == 0 ? -1 : meter.position + projection; 570 | } 571 | return -1; 572 | } 573 | 574 | @Override 575 | public int convertIndex(int sourceElementIndex, int sourceFilterIndex, int targetFilterIndex) { 576 | return convertIndex(sourceElementIndex, sourceFilterIndex, targetFilterIndex, false); 577 | } 578 | 579 | @Override 580 | public int convertIndex(int sourceElementIndex, int sourceFilterIndex, int targetFilterIndex, boolean ceiling) { 581 | int targetSelectionSize = size(targetFilterIndex); 582 | int targetElementIndex; 583 | if (sourceFilterIndex == targetFilterIndex) { 584 | targetElementIndex = sourceElementIndex; // no validation 585 | } else { 586 | if (sourceElementIndex < 0) { 587 | targetElementIndex = -1; 588 | } else if (sourceElementIndex >= size(sourceFilterIndex)) { 589 | targetElementIndex = targetSelectionSize; 590 | } else { 591 | final IndexLocator locator = new IndexLocator(sourceFilterIndex, sourceElementIndex); 592 | final ScalarMeter meter = new ScalarMeter(targetFilterIndex); 593 | final Navigator navigator = new Navigator(doNotTrack, meter); 594 | navigator.descend(locator, false); 595 | final int projection = locator.currentNode.distances[0][targetFilterIndex]; 596 | targetElementIndex = meter.position + (ceiling ? 1 : projection); 597 | } 598 | } 599 | return targetElementIndex; 600 | } 601 | 602 | @Override 603 | public T get(int elementIndex) { 604 | return get(universeFilter, elementIndex); 605 | } 606 | 607 | @Override 608 | public boolean updateInPlace(T oldValue, Update modification) { 609 | Section tracker = autoAdd ? new Gap() : new Section(); 610 | final Meter meter = allocateMeterForAutoAdd(); 611 | final Navigator navigator = new Navigator(tracker, meter); 612 | Node container = navigator.descendTo(oldValue); 613 | if (container == absMinNode) { 614 | if (autoAdd) { 615 | modification.apply(oldValue); 616 | finishAddition(oldValue, project(oldValue), (VectorMeter) meter, (Gap) tracker); 617 | return true; 618 | } 619 | return false; 620 | } 621 | if (modification.apply(container.element)) { 622 | onElementUpdated(container.element, meter.getPosition(), tracker.nodes[0].distances[0], 0, zero); 623 | return true; 624 | } 625 | return false; 626 | } 627 | 628 | @Override 629 | public boolean updateFilters(T oldValue, Update modification) { 630 | Section tracker = autoAdd ? new Gap() : new Section(); 631 | final Meter meter = allocateMeterForAutoAdd(); 632 | final Navigator navigator = new Navigator(tracker, meter); 633 | Node container = navigator.descendTo(oldValue); 634 | if (container == absMinNode) { 635 | if (autoAdd) { 636 | modification.apply(oldValue); 637 | finishAddition(oldValue, project(oldValue), (VectorMeter) meter, (Gap) tracker); 638 | return true; 639 | } 640 | return false; 641 | } 642 | T element = container.element; 643 | int[] oldEdge = tracker.nodes[0].distances[0]; 644 | boolean modified = modification.apply(element); 645 | if (modified) { 646 | int[] changeEstimate = project(element); 647 | int[] deltaCount = sub(set(newDistance(), changeEstimate, horizon), oldEdge, horizon); 648 | Distance.add(changeEstimate, oldEdge, horizon); 649 | adjustDistance(tracker, 1, deltaCount); 650 | onElementUpdated(element, meter.getPosition(), changeEstimate, 1, deltaCount); 651 | } 652 | return modified; 653 | } 654 | 655 | @Override 656 | public boolean updateReorder(T oldValue, Update modification) { 657 | Section tracker = autoAdd ? new Gap() : new Section(); 658 | Meter meter = allocateMeterForAutoAdd(); 659 | final Navigator navigator = new Navigator(tracker, meter); 660 | Node container = navigator.descendTo(oldValue); 661 | if (container == absMinNode) { 662 | if (autoAdd) { 663 | boolean modified = modification.apply(oldValue); 664 | if (modified) { 665 | Node prevNode = tracker.nodes[0]; 666 | if ((prevNode != absMinNode && compareWithNextNode(oldValue, prevNode) < 0) 667 | || compareWithNextNode(oldValue, prevNode.nodes[0]) > 0) { 668 | addPrecomputedDistance(oldValue, project(oldValue)); 669 | return true; 670 | } 671 | } 672 | finishAddition(oldValue, project(oldValue), (VectorMeter) meter, (Gap) tracker); 673 | return true; 674 | } 675 | return false; 676 | } 677 | // optimized position-aware remove 678 | int[] oldEdge = Distance.set(newDistance(), tracker.nodes[0].distances[0], horizon); 679 | adjustDistance(tracker, -1, oldEdge); 680 | T element = container.element; 681 | if (broadcastOldValue) { 682 | onElementUpdated(element, meter.getPosition(), oldEdge, -1, oldEdge); 683 | } 684 | boolean modified = modification.apply(element); 685 | boolean diffRank = modified 686 | && (compareWithNextNode(tracker.nodes[0].element, container) > 0 687 | || compareWithNextNode(element, container.nodes[0]) > 0); 688 | if (diffRank) { 689 | if (!broadcastOldValue) { 690 | onElementUpdated(element, meter.getPosition(), oldEdge, -1, oldEdge); 691 | } 692 | // finish removal 693 | finishRemoval(tracker, container); 694 | add(element); 695 | } else { 696 | // voila, order preserved! 697 | final int[] projection = project(element); 698 | adjustDistance(tracker, 1, projection); 699 | if (!broadcastOldValue) { 700 | sub(projection, oldEdge, horizon); 701 | } 702 | onElementUpdated(element, meter.getPosition(), projection, 1, projection); 703 | } 704 | return modified; 705 | } 706 | 707 | private Meter allocateMeterForReporting() { 708 | return positionUnaware || observers.isEmpty() ? doNotMeasure : new VectorMeter(); 709 | } 710 | 711 | private Meter allocateMeterForAutoAdd() { 712 | return autoAdd ? new VectorMeter() : allocateMeterForReporting(); 713 | } 714 | 715 | private int[] project(T oldValue) { 716 | return Distance.project(newDistance(), oldValue, horizon, this); 717 | } 718 | 719 | @Override 720 | public void setAutoAdd(boolean autoAdd) { 721 | this.autoAdd = autoAdd; 722 | } 723 | 724 | private void adjustDistance(Section previousNodes, int deltaSize, int[] deltaCount) { 725 | if (deltaSize != 0 && !Distance.isZero(deltaCount, horizon)) { 726 | for (int level = orbitLevel; level >= 0; --level) { 727 | Distance.add(previousNodes.nodes[level].distances[level], deltaSize, deltaCount, horizon); 728 | } 729 | } 730 | } 731 | 732 | @Override 733 | public BitSet refreshFilters(BitSet dirtyMask) { 734 | if (dirtyMask.cardinality() == 0) { 735 | return EMPTY; 736 | } 737 | horizon = projector.getHorizon(); 738 | hintBulkOpBegin(); 739 | final BitSet retVal; 740 | if (dirtyMask.cardinality() == 1) { 741 | int affectedElements = doRefreshFilters(dirtyMask.nextSetBit(0)); 742 | retVal = affectedElements == 0 ? EMPTY : dirtyMask; 743 | } else { 744 | retVal = doRefreshFilters(dirtyMask); 745 | } 746 | hintBulkOpCompleted(); 747 | return retVal; 748 | } 749 | 750 | @Override 751 | public void setComparator(Comparator pendingComparator) { 752 | this.pendingComparator = pendingComparator; 753 | } 754 | 755 | @Override 756 | public Comparator getComparator() { 757 | return comparator; 758 | } 759 | 760 | @Override 761 | public Iterator> iterator() { 762 | return new Iterator>() { 763 | Node node = absMinNode; 764 | 765 | @Override 766 | public boolean hasNext() { 767 | return node.nodes[0] != null; 768 | } 769 | 770 | @Override 771 | public Map.Entry next() { 772 | node = node.nodes[0]; 773 | return node; 774 | } 775 | 776 | @Override 777 | public void remove() { 778 | throw new UnsupportedOperationException(); // no use case yet 779 | } 780 | }; 781 | } 782 | 783 | @Override 784 | public Iterator iterator(final int filterIndex) { 785 | int filteredSize = size(filterIndex); 786 | if (filteredSize < levelCount) { 787 | return cherryIterator(filterIndex); 788 | } else { 789 | int universeSize = size(); 790 | // ladder beats walker starting from 1/d factor 791 | if (filteredSize < universeSize / denominator) { 792 | return ladderIterator(filterIndex); 793 | } else { 794 | return walkerIterator(filterIndex); 795 | } 796 | } 797 | } 798 | 799 | // factory methods exposed for unit testing / benchmarking 800 | protected Iterator cherryIterator(int filterIndex) { 801 | return new CherryIterator(filterIndex); 802 | } 803 | 804 | protected Iterator ladderIterator(int filterIndex) { 805 | return new LadderIterator(filterIndex); 806 | } 807 | 808 | protected Iterator walkerIterator(int filterIndex) { 809 | return new WalkerIterator(filterIndex); 810 | } 811 | 812 | @Override 813 | public void setAll(Adaptable source) { 814 | doAddAll(source, true); 815 | } 816 | 817 | @Override 818 | public void addAll(Adaptable source) { 819 | doAddAll(source, source == this); 820 | } 821 | 822 | private void doAddAll(Adaptable source, boolean dropExisting) { 823 | hintBulkOpBegin(); 824 | if (source.getFilterCount() != getFilterCount()) { 825 | throw new IllegalArgumentException("Incompatible source!"); 826 | } 827 | Iterator> iterator = source.iterator(); 828 | if (dropExisting) { 829 | // clear silently, preserving iterator. 830 | clear(); 831 | } 832 | while (iterator.hasNext()) { 833 | Map.Entry node = iterator.next(); 834 | addPrecomputedDistance(node.getKey(), node.getValue()); 835 | } 836 | hintBulkOpCompleted(); 837 | } 838 | 839 | private BitSet doRefreshFilters(BitSet mask) { 840 | if (size() == 0 || mask.cardinality() == 0) { 841 | // skip for empty containers 842 | return EMPTY; 843 | } 844 | Gap tracker = new Gap(); 845 | Node node = absMinNode; 846 | Node nextNode; 847 | Node prevNode; 848 | int[] selectionIndex = newDistance(); 849 | int[] projection = newDistance(); 850 | for (int level = 0; level < levelCount; ++level) { 851 | tracker.setNextNode(level, node); 852 | } 853 | boolean moreData; 854 | final int[] indices = Distance.toArray(mask); 855 | final int[] deltaCount = newDistance(); 856 | T element; 857 | do { 858 | set(projection, node.distances[0], horizon); 859 | nextNode = node.nodes[0]; 860 | moreData = nextNode != null; 861 | if (moreData) { 862 | element = nextNode.element; 863 | Distance.set(deltaCount, projection, indices); 864 | Distance.project(projection, element, indices, this); 865 | Distance.sub(deltaCount, projection, indices); 866 | if (!isZero(deltaCount, horizon)) { 867 | onElementUpdated(element, selectionIndex, deltaCount, -1, deltaCount); 868 | } 869 | Distance.add(selectionIndex, projection, horizon); 870 | } 871 | for (int level = 0; level < levelCount; ++level) { 872 | prevNode = tracker.nodes[level]; 873 | if (prevNode.nodes[level] == nextNode) { 874 | sub(set(projection, selectionIndex, indices), tracker.distances[level], horizon); 875 | set(prevNode.distances[level], projection, indices); 876 | set(tracker.distances[level], selectionIndex, indices); 877 | tracker.setNextNode(level, nextNode); 878 | } 879 | } 880 | node = nextNode; 881 | } while (moreData); 882 | return mask; 883 | } 884 | 885 | // optimized single-filter version 886 | // 887 | // the traversal logic is similar to WalkerIterator, and can be further optimized 888 | // if both selections are narrow enough. 889 | // however, updateFilters(...) targeted by filterIndex should be considered first. 890 | // 891 | private int doRefreshFilters(int filterIndex) { 892 | if (size() == 0) { 893 | return 0; 894 | } 895 | // we don't use a Gap because we only recompute indices of a single selection 896 | Section section = new Section(); 897 | int[] lastIndex = new int[levelCount]; 898 | Node node = absMinNode; 899 | Node nextNode; 900 | Node prevNode; 901 | int selectionIndex = 0; 902 | for (int level = 0; level < levelCount; ++level) { 903 | section.setNextNode(level, node); 904 | } 905 | boolean moreData; 906 | final int[] vPosition = newDistance(); 907 | final int[] deltaCount = newDistance(); 908 | T element; 909 | int[] distance; 910 | int delta; 911 | do { 912 | nextNode = node.nodes[0]; 913 | moreData = nextNode != null; 914 | if (moreData) { 915 | element = nextNode.element; 916 | distance = node.distances[0]; 917 | delta = -distance[filterIndex]; 918 | if (accept(element, filterIndex, distance)) { 919 | ++delta; 920 | ++selectionIndex; 921 | } 922 | if (delta != 0) { 923 | deltaCount[filterIndex] = delta; 924 | vPosition[filterIndex] = selectionIndex; 925 | onElementUpdated(element, vPosition, deltaCount, 1, deltaCount); 926 | } 927 | } 928 | for (int level = 0; level < levelCount; ++level) { 929 | prevNode = section.nodes[level]; 930 | if (prevNode.nodes[level] == nextNode) { 931 | prevNode.distances[level][filterIndex] = selectionIndex - lastIndex[level]; 932 | lastIndex[level] = selectionIndex; 933 | section.setNextNode(level, nextNode); 934 | } 935 | } 936 | node = nextNode; 937 | } while (moreData); 938 | return selectionIndex; 939 | } 940 | 941 | @SuppressWarnings("UnusedDeclaration") 942 | void validateIntegrity() { 943 | Node node = absMinNode; 944 | Gap accumulated = new Gap(); 945 | for (int level = 0; level < orbitLevel; ++level) { 946 | accumulated.setNextNode(level, absMinNode); 947 | } 948 | Node nextNode; 949 | do { 950 | int[] walkSlow = accumulated.distances[0]; 951 | Distance.add(walkSlow, node.distances[0]); 952 | nextNode = node.nodes[0]; 953 | int nextGoodLevel = 0; 954 | for (int level = 1; level < orbitLevel; ++level) { 955 | Node prev = accumulated.nodes[level]; 956 | if (prev.nodes[level] == nextNode) { 957 | int[] flyDelta = accumulated.distances[level]; 958 | Distance.add(flyDelta, prev.distances[level]); 959 | accumulated.setNextNode(level, prev.nodes[level]); 960 | if (!Arrays.equals(flyDelta, walkSlow)) { 961 | throw new IllegalStateException("Inconsistent distance to element " 962 | + (nextNode == null ? null : nextNode.element) 963 | + " at level " + level); 964 | } 965 | if (level - nextGoodLevel > 1) { 966 | throw new IllegalStateException("Connected at " + level + " but unconnected at " + (level - 1)); 967 | } 968 | nextGoodLevel = level; 969 | } 970 | } 971 | node = nextNode; 972 | } while (node != null); 973 | } 974 | 975 | @Override 976 | public void hintBulkOpBegin() { 977 | // no-op 978 | } 979 | 980 | @Override 981 | public void hintBulkOpCompleted() { 982 | // no-op 983 | } 984 | 985 | @Override 986 | public int getUniverseFilterIndex() { 987 | return universeFilter; 988 | } 989 | 990 | @Override 991 | public ProjectorEditor getFilterEditor() { 992 | return projector; 993 | } 994 | 995 | @Override 996 | public void addElementObserver(ElementObserver observer) { 997 | observers.add(observer); 998 | } 999 | 1000 | @Override 1001 | public void removeElementObserver(ElementObserver observer) { 1002 | observers.remove(observer); 1003 | } 1004 | 1005 | abstract class SimpleIterator implements Iterator { 1006 | final int filterIndex; 1007 | final int returnedSize; 1008 | Node node = absMinNode; 1009 | int returnedCount = 0; 1010 | 1011 | public SimpleIterator(int filterIndex) { 1012 | this.filterIndex = filterIndex; 1013 | returnedSize = size(filterIndex); 1014 | } 1015 | 1016 | @Override 1017 | public boolean hasNext() { 1018 | return returnedCount < returnedSize; 1019 | } 1020 | 1021 | @Override 1022 | public void remove() { 1023 | throw new UnsupportedOperationException(); // no use case yet 1024 | } 1025 | 1026 | /** 1027 | * Actually return the found element, preparing iterator state 1028 | * for subsequent {@link #hasNext()} and {@link #next()} calls. 1029 | * The name is adopted from the Generator Function pattern. 1030 | * @return the element found. 1031 | */ 1032 | protected final T yield() { 1033 | returnedCount ++; 1034 | return node.element; 1035 | } 1036 | } 1037 | 1038 | class WalkerIterator extends SimpleIterator { 1039 | public WalkerIterator(int filterIndex) { 1040 | super(filterIndex); 1041 | } 1042 | 1043 | @Override 1044 | public T next() { 1045 | int stepDistance; 1046 | do { 1047 | stepDistance = node.distances[0][filterIndex]; 1048 | node = node.nodes[0]; 1049 | } while (stepDistance == 0); 1050 | return yield(); 1051 | } 1052 | } 1053 | 1054 | class LadderIterator extends SimpleIterator { 1055 | int level, bestLevel; 1056 | int[][] distances; 1057 | 1058 | public LadderIterator(int filterIndex) { 1059 | super(filterIndex); 1060 | bestLevel = 0; 1061 | final int totalSize = size(); 1062 | int selectionSize = returnedSize; 1063 | while (bestLevel < node.level && (selectionSize *= denominator) < totalSize) { 1064 | ++bestLevel; 1065 | } 1066 | level = bestLevel; 1067 | distances = node.distances; 1068 | } 1069 | 1070 | @Override 1071 | public T next() { 1072 | // zero, may escalate -> escalate 1073 | // zero, nowhere to escalate -> jump 1074 | // nonzero, may descend -> descend 1075 | // nonzero, nowhere to descend -> yield 1076 | while (distances[0][filterIndex] == 0) { 1077 | while (level > 0 && distances[level][filterIndex] != 0) { 1078 | level--; 1079 | } 1080 | node = node.nodes[level]; 1081 | distances = node.distances; 1082 | } 1083 | node = node.nodes[0]; 1084 | distances = node.distances; 1085 | level = Math.min(bestLevel, node.level); 1086 | return yield(); 1087 | } 1088 | 1089 | } 1090 | 1091 | class CherryIterator extends SimpleIterator { 1092 | public CherryIterator(int filterIndex) { 1093 | super(filterIndex); 1094 | } 1095 | 1096 | @Override 1097 | public T next() { 1098 | return get(filterIndex, returnedCount++); 1099 | } 1100 | } 1101 | } 1102 | --------------------------------------------------------------------------------