[BLEEDING] Prepare internal registry lists overhaul to support sorting.

* Add RegistrationOrder for order.
* Add sorting functionality with some tests. (See
RegistrationOrder.AbstractRegisTrationOrderSort.)
* (Except for test cases, all this is not yet in use.)

(Perhaps the sorting should be changed to use an array for output
instead of iterators, this just represents a quick way in.)

Further direction:
* Replace all the registry lists like for INotifyReload and
IDisableListener and so on within registries by a generic storage that
uses this for sorting. This way every such thing will support both
priority-based and tag-based order at the same time.
* To support different order definitions depending on what type an
interface like IRegisterWithOrder might be supported by a registry.
* Later generic event listeners might also use this, simply.
This commit is contained in:
asofold 2017-04-24 18:54:13 +02:00
parent 97cdbe8046
commit d0c42ab061
8 changed files with 1035 additions and 4 deletions

View File

@ -0,0 +1,37 @@
package fr.neatmonster.nocheatplus.components.registry.exception;
/**
* A registry item has already been registered (for a given context), and the
* registry does not support overriding via register(...) - it might still
* support unregister(...) and register(...), in case what to unregister is
* known.
*
* @author asofold
*
*/
public class AlreadyRegisteredException extends RegistryException {
private static final long serialVersionUID = -72557863263954102L;
public AlreadyRegisteredException() {
super();
}
public AlreadyRegisteredException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public AlreadyRegisteredException(String message, Throwable cause) {
super(message, cause);
}
public AlreadyRegisteredException(String message) {
super(message);
}
public AlreadyRegisteredException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,23 @@
package fr.neatmonster.nocheatplus.components.registry.order;
/**
* Just provide a neutral getter, independent of what context it is used in.
* There will be confusion potential, so this remains subject to an overhaul
* later on.
* <hr>
* Typical uses:
* <ul>
* <li>IRegisterWithOrder might just extend this one, renaming and adding more
* methods pending there.</li>
* <li>IGetRegistrationOrder can be implemented to enable use of sorting
* methods.</li>
* </ul>
*
* @author asofold
*
*/
public interface IGetRegistrationOrder {
public RegistrationOrder getRegistrationOrder();
}

View File

@ -0,0 +1,7 @@
package fr.neatmonster.nocheatplus.components.registry.order;
public interface IRegisterWithOrder {
public RegistrationOrder getRegistrationOrder(Class<?> registerForType);
// TODO: getRegistrationOrder(Class<?> registerForType, Class<?> registryType) ?
}

View File

@ -17,7 +17,9 @@ package fr.neatmonster.nocheatplus.components.registry.order;
import java.util.Comparator;
/**
* Utilities for sorting out order.
* Utilities for sorting out order. <br>
* TODO: DEPRECATE and later remove.
*
* @author asofold
*
*/

View File

@ -0,0 +1,23 @@
package fr.neatmonster.nocheatplus.components.registry.order;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Empty string counts as null.
* @author asofold
*
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RegisterWithOrder {
/** Crude workaround for an Integer that may be null. */
public String basePriority() default "";
public String tag() default "";
public String beforeTag() default "";
public String afterTag() default "";
}

View File

@ -0,0 +1,577 @@
package fr.neatmonster.nocheatplus.components.registry.order;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import fr.neatmonster.nocheatplus.utilities.ds.map.CoordHash;
/**
* The order of registration. These objects can't be sorted reliably, due to
* tags allowing virtually everything, thus specific (greedy) rules are needed.
*
* @author asofold
*
*/
public class RegistrationOrder {
/**
* Compare on base of basePriority. Entries with null priority are sorted to
* the front.
*/
public static Comparator<RegistrationOrder> cmpBasePriority = new Comparator<RegistrationOrder>() {
@Override
public int compare(final RegistrationOrder o1, final RegistrationOrder o2) {
final Integer p1 = o1.getBasePriority();
final Integer p2 = o2.getBasePriority();
if (p1 == null) {
return p2 == null ? 0 : -1; // o1 to front.
} else if (p2 == null) {
return 1; // o2 to front.
} else {
return p1.compareTo(p2);
}
}
};
/**
* Auxiliary class to contain the basic sorting algorithm, using an abstract
* method to fetch the RegistrationOrder from the input type. The sorting
* isn't necessarily stable, and tag-specific order can vary wildly
* depending on the order of input.
* <hr>
* The basePriority comes first on sorting, beforeTag set means being put to
* front of a priority level, afterTag means being sorted to the end (with
* beforeTag still in front of the priority level, without to the end). With
* null priority entries are sorted to the very start/end of the list,
* unless both of beforeTag and afterTag are null, in which case they're
* sorted to/around the level of basePriority being 0. Tag-based comparison
* uses regular expressions via tagA.matchhes(beforeTagB|afterTagB). The
* element sorted into an existing (sub-) list has their beforeTag/afterTag
* checked first (greedy).<br>
* TODO: Describe the sorting algorithm in more detail, if needed.
*
* @author asofold
*
* @param <F>
*/
public static abstract class AbstractRegistrationOrderSort<F> {
// TODO: Signature with passing IFetchRegistrationOrder<F> to the sorting?
// TODO: Back to generic static methods?
private final Comparator<F> cmp = new Comparator<F>() {
@Override
public int compare(final F o1, final F o2) {
return RegistrationOrder.cmpBasePriority.compare(
fetchRegistrationOrder(o1), fetchRegistrationOrder(o2));
}
};
protected abstract RegistrationOrder fetchRegistrationOrder(F item);
public void sort(final Collection<F> input) {
final LinkedList<F> sorted = getSortedLinkedList(input);
input.clear();
input.addAll(sorted);
}
@SuppressWarnings("unchecked")
public F[] getSortedArray(Collection<F> input) {
// TODO: Make the default sorting thing use an array (!).
return (F[]) getSortedLinkedList(input).toArray();
}
public final <O extends Collection<F>> O sort(final Collection<F> input, final O output) {
output.addAll(getSortedLinkedList(input));
return output;
}
public LinkedList<F> getSortedLinkedList(final Collection<F> input) {
/*
* (Due to the need for a ListIterator, you can't just use a given
* type that merely extends Collection.)
*/
// TODO: Implement using an array for output with insertionIndex (less iterators etc).
final LinkedList<F> output = new LinkedList<F>();
if (input == null || input.isEmpty()) {
return output;
}
else if (input.size() == 1) {
output.addAll(input);
return output;
}
// Sort into rough groups.
final LinkedList<F> belowZeroPriority = new LinkedList<F>();
final LinkedList<F> zeroPriority = new LinkedList<F>();
final LinkedList<F> aboveZeroPriority = new LinkedList<F>();
for (final F item : input) {
final RegistrationOrder order = fetchRegistrationOrder(item);
final Integer basePriority = order.getBasePriority();
if (basePriority == null) {
// Distinguish cases, note the sorting order of cmpBasePriority.
if (order.getBeforeTag() != null) {
/*
* These will be sorted to front, processed last within
* this list, resulting in optimal order.
*/
belowZeroPriority.add(item);
}
else if (order.getAfterTag() != null) {
// These will be last in the end, thus add to tempOut directly.
sortInFromStart(output, item);
}
else {
/*
* These are somehow added to 0-priority - could end up
* anywhere, if 0-basePriority items have an afterTag
* set that matches the tag. Consequently items with
* null base priority are processed last.
*/
zeroPriority.add(item);
}
}
else if (basePriority < 0) {
belowZeroPriority.add(item);
}
else if (basePriority > 0) {
aboveZeroPriority.add(item);
}
else {
// Ensure to process items with 0 basePriority set first.
zeroPriority.add(0, item);
}
}
// Combine lists into output.
// TODO: Perhaps could use optimized methods for sorting in later on.
if (!aboveZeroPriority.isEmpty()) {
addSortedSubList(aboveZeroPriority, output);
}
if (!zeroPriority.isEmpty()) {
// Still need to sort the roughly pre-grouped items, to match tags.
for (final F item : zeroPriority) {
sortInFromStart(output, item);
}
}
if (!belowZeroPriority.isEmpty()) {
addSortedSubList(belowZeroPriority, output);
}
return output;
}
/**
* Sort, reverse and then add the subList to the output via
* sortInFromStart.
*
* @param subList
* @param output
*/
private void addSortedSubList(final List<F> subList, final List<F> output) {
Collections.sort(subList, cmp);
Collections.reverse(subList);
for (final F item : subList) {
sortInFromStart(output, item);
}
}
private void sortInFromStart(final List<F> list, final F item) {
// Attempt to get the fast thing first.
if (list.isEmpty()) {
list.add(item);
return;
}
final RegistrationOrder order = fetchRegistrationOrder(item);
if (shouldSortBefore(order, fetchRegistrationOrder(list.get(0)))) {
list.add(0, item);
return;
}
else {
sortInFromSecond(list, item, order);
}
}
private void sortInFromSecond(final List<F> list, final F item, final RegistrationOrder order) {
final ListIterator<F> it = list.listIterator(1);
while (it.hasNext()) {
final F otherItem = it.next();
final RegistrationOrder other = fetchRegistrationOrder(otherItem);
if (shouldSortBefore(order, other)) {
it.previous();
it.add(item);
return;
}
}
// To the end.
it.add(item);
}
/**
* The heart of all the sorting. See {@link AbstractRegistrationOrderSort} for a description.
* @param order1
* @param order2
* @return
*/
public static boolean shouldSortBefore(final RegistrationOrder order1, final RegistrationOrder order2) {
// TODO: Javadocs - where / how.
final Integer basePriority1 = order1.getBasePriority();
final String tag1 = order1.getTag();
final String beforeTag1 = order1.getBeforeTag();
final String afterTag1 = order1.getAfterTag();
final Integer basePriority2 = order2.getBasePriority();
final String tag2 = order2.getTag();
final String beforeTag2 = order2.getBeforeTag();
final String afterTag2 = order2.getAfterTag();
// TODO: Can the tag comparison be unified (simplification)?
// TODO: Final decision: first all tags of the first, or first 'before' each then 'after' each?
if (basePriority1 == null) {
if (basePriority2 == null) {
if (beforeTag1 == null) {
if (beforeTag2 == null) {
if (afterTag1 == null) {
// Safe to abort here.
return true;
}
else {
if (
// order1 might be set to come after order2.
tag2 != null && tag2.matches(afterTag1)
// Must proceed to check other entries with afterTag set.
|| afterTag2 == null
|| tag1 == null
// The other entry is not set to come after this one.
|| !tag1.matches(afterTag2)) {
return false;
}
else {
return true;
}
}
}
else {
/*
* beforeTag2 is not null - the only way to let
* order1 come first, is to have tag1 match
* afterTag2. Otherwise
*/
return tag1 != null && afterTag2 != null && tag1.matches(afterTag2);
}
}
else {
/*
* beforeTag1 is not null - the only way to let
* order2 come first, is to have tag2 match
* afterTag1. Otherwise
*/
return tag2 == null || afterTag1 == null || !tag2.matches(afterTag1);
}
}
else {
// order2 has basePriority set, proceed depending on tags.
return
// Pre-any-basePriority region.
beforeTag1 != null
// To center region (simple).
|| afterTag1 == null && basePriority2 > 0
// To center region (with comparison).
|| afterTag1 == null && basePriority2 == 0
&& (
// Sort to the end of 0-basePriority, where neither beforeTag nor afterTag is set.
tag1 == null && (beforeTag2 == null && afterTag2 != null)
// The other is explicitly set to be later.
|| tag1 != null && afterTag2 != null && tag1.matches(afterTag2)
// Before anything that only has the afterTag set, otherwise.
|| beforeTag2 == null && afterTag2 != null
)
;
}
}
else if (basePriority2 == null) {
// Determine region to fit in (...).
if (beforeTag2 == null) {
if (afterTag2 == null) {
// order2 is within the 0-basePriority region.
if (basePriority1 < 0) {
return false;
}
else if (basePriority1 > 0) {
return true;
}
else {
// Both are within the same region.
if (tag2 != null && afterTag1 != null && tag2.matches(afterTag1)) {
// order1 is set to come after order2.
return false;
}
else if (beforeTag1 != null) {
// Order1 comes first.
return true;
}
else if (afterTag1 != null) {
// Order1 is sorted to the end of the region rather.
return false;
}
else {
/*
* Prefer to have have set basePriority 0 first,
* assuming null basePriority to be interpreted
* as 'later registered', keeping the order of
* registration rather.
*/
return true;
}
}
}
else {
// order2 comes later.
return true;
}
}
else {
// order2 comes first.
return false;
}
}
else {
// Both not null.
if (basePriority1 < basePriority2) {
return true;
}
else if (basePriority1 > basePriority2) {
return false;
}
else {
// Same priority set, distinguish by tag settings.
if (beforeTag1 == null) {
if (beforeTag2 == null) {
// Only distinction could be afterTagS.
if (afterTag1 == null) {
// Either can't distinguish, or order2 should come afterwards.
return true;
}
else {
if (afterTag2 == null) {
// order1 should be passed on.
return false;
}
else {
// Both afterTagS are not null.
if (tag2 != null && tag2.matches(afterTag1)) {
// Greedy (could override afterTag2).
return false;
}
else if (tag1 != null && tag1.matches(afterTag2)) {
// order2 is set to come after order1.
return true;
}
else {
// order1 may be set to come after another entry that follows.
return false;
}
}
}
}
else {
// beforeTag1 is null, beforeTag2 not.
if (afterTag2 != null && tag1 != null && tag1.matches(afterTag2)) {
// order2 is set to come after order1,
return true;
}
else {
// beforeTag set rules otherwise.
return false;
}
}
}
else {
// beforeTag1 is null.
if (beforeTag2 == null) {
if (afterTag1 != null && tag2 != null && tag2.matches(afterTag1)) {
// order1 is explicitly set to come after order2.
return false;
}
else {
// TODO: Very complicated - explain.
return afterTag2 == null && afterTag1 == null;
}
}
else {
// Both beforeTag1 and beforeTag2 are not so null.
if (tag2 != null && tag2.matches(beforeTag1)) {
// order1 is set to come before order2.
return true;
}
else if (tag1 != null && tag1.matches(beforeTag2)) {
// order2 is set to come before order1.
return false;
}
else if (afterTag1 == null) {
// Either can't distinguish, or afterTag2 is set, thus sort in before.
return true;
}
else {
// TODO: If correct, simplify (one condition returns true).
// afterTag1 is not null.
if (tag2 != null && tag2.matches(afterTag1)) {
// order1 is set to come after order2.
return false;
}
else if (afterTag2 != null){
if (tag1 != null && tag1.matches(afterTag2)) {
// order2 is set to come after order1.
return true;
}
else {
// Rather keep checking.
return false;
}
}
else {
// afterTag1 is null, otherwise not matching, thus order1 comes behind.
return false;
}
}
}
}
}
}
// (No catch all.)
}
}
public static class SortRegistrationOrder extends AbstractRegistrationOrderSort<RegistrationOrder> {
@Override
protected RegistrationOrder fetchRegistrationOrder(RegistrationOrder item) {
return item;
}
}
public static class SortIGetRegistrationOrder extends AbstractRegistrationOrderSort<IGetRegistrationOrder> {
@Override
protected RegistrationOrder fetchRegistrationOrder(IGetRegistrationOrder item) {
return item.getRegistrationOrder();
}
}
/** The default order (no preference, middle). */
public static final RegistrationOrder DEFAULT_ORDER = new RegistrationOrder();
/**
* Sort collections of RegistrationOrder instances.
*/
public static final SortRegistrationOrder sortRegistrationOrder = new SortRegistrationOrder();
/**
* Sort collections of IGetRegistrationOrder instances.
*/
public static final SortIGetRegistrationOrder sortIGetRegistrationOrder = new SortIGetRegistrationOrder();
/*
* TODO: Consider a registration id / count (meant unique, increasing with
* the next registration). When both priorities are 0, this id could be used
* to guarantee order.
*/
private final Integer basePriority;
private final String tag;
private final String beforeTag; // TODO: Set ?
private final String afterTag; // TODO: Set ?
/**
*
* @param bluePrint
* @throws NumberFormatException, if basePriority can't be parsed.
*/
public RegistrationOrder(RegisterWithOrder bluePrint) {
// TODO: InvalidOrderException via static method for parsing.
this(bluePrint.basePriority().isEmpty() ? null : Integer.parseInt(bluePrint.basePriority()),
bluePrint.tag().isEmpty() ? null : bluePrint.tag(),
bluePrint.beforeTag().isEmpty() ? null : bluePrint.beforeTag(),
bluePrint.afterTag().isEmpty() ? null : bluePrint.afterTag());
}
public RegistrationOrder(RegistrationOrder bluePrint) {
this(bluePrint.getBasePriority(), bluePrint.getTag(), bluePrint.getBeforeTag(), bluePrint.getAfterTag());
}
public RegistrationOrder() {
this(null, null, null, null);
}
public RegistrationOrder(Integer basePriority) {
this(basePriority, null, null, null);
}
public RegistrationOrder(Integer basePriority, String tag) {
this(basePriority, tag, null, null);
}
public RegistrationOrder(String tag) {
this(null, tag, null, null);
}
public RegistrationOrder(String tag, String beforeTag, String afterTag) {
this(null, tag, beforeTag, afterTag);
}
/**
*
* @param basePriority
* Basic priority for sorting (first priority). If this is not
* set (i.e. is null), beforeTag and afterTag allow sorting
* independently of the basePriority of other instances, however
* the sorting is greedy/special.
* @param tag
* @param beforeTag
* @param afterTag
*/
public RegistrationOrder(Integer basePriority, String tag, String beforeTag, String afterTag) {
this.basePriority = basePriority;
this.tag = tag;
this.beforeTag = beforeTag;
this.afterTag = afterTag;
}
public Integer getBasePriority() {
return basePriority;
}
public String getTag() {
return tag;
}
public String getBeforeTag() {
return beforeTag;
}
public String getAfterTag() {
return afterTag;
}
@Override
public int hashCode() {
// (Special value for null.)
return (basePriority == null ? CoordHash.hashCode3DPrimes(-1, -1, -1) : basePriority.hashCode())
^ (tag == null ? CoordHash.hashCode3DPrimes(1, 0, 0) : tag.hashCode())
^ (beforeTag == null ? CoordHash.hashCode3DPrimes(0, 1, 0) : beforeTag.hashCode())
^ (afterTag == null ? CoordHash.hashCode3DPrimes(0, 0, 1) : afterTag.hashCode()) ;
}
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof RegistrationOrder)) {
return false;
}
final RegistrationOrder other = (RegistrationOrder) obj;
return (basePriority == null ? other.getBasePriority() == null : basePriority.equals(other.getBasePriority()))
&& (tag == null ? other.getTag() == null : tag.equals(other.getTag()))
&& (beforeTag == null ? other.getBeforeTag() == null : beforeTag.equals(other.getBeforeTag()))
&& (afterTag == null ? other.getAfterTag() == null : afterTag.equals(other.getAfterTag()));
}
}

View File

@ -21,13 +21,15 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Priority for order during setup/initialization of something.
* @author mc_dev
* Priority for order during setup/initialization of something. <br>
* TODO: DEPRECATE, later remove.
*
* @author asofold
*
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SetupOrder {
public int priority() default 0;
public int priority() default 0;
}

View File

@ -0,0 +1,360 @@
package fr.neatmonster.nocheatplus.test;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.junit.Test;
import fr.neatmonster.nocheatplus.components.registry.order.IGetRegistrationOrder;
import fr.neatmonster.nocheatplus.components.registry.order.RegistrationOrder;
import fr.neatmonster.nocheatplus.utilities.StringUtil;
import fr.neatmonster.nocheatplus.utilities.build.BuildParameters;
public class TestRegistrationOrder {
public static interface IAccessSort<F> {
public LinkedList<F> getSortedLinkedList(Collection<F> items);
public RegistrationOrder getRegistrationOrder(F item);
}
public static final IAccessSort<RegistrationOrder> accessRegistrationOrder = new IAccessSort<RegistrationOrder>() {
@Override
public LinkedList<RegistrationOrder> getSortedLinkedList(Collection<RegistrationOrder> items) {
return RegistrationOrder.sortRegistrationOrder.getSortedLinkedList(items);
}
@Override
public RegistrationOrder getRegistrationOrder(RegistrationOrder item) {
return item;
}
};
public static final class AccessIGetRegistrationOrder implements IAccessSort<IGetRegistrationOrder> {
@Override
public LinkedList<IGetRegistrationOrder> getSortedLinkedList(Collection<IGetRegistrationOrder> items) {
return RegistrationOrder.sortIGetRegistrationOrder.getSortedLinkedList(items);
}
@Override
public RegistrationOrder getRegistrationOrder(IGetRegistrationOrder item) {
return item.getRegistrationOrder();
}
}
public static final IAccessSort<IGetRegistrationOrder> accessIGetRegistrationOrder = new AccessIGetRegistrationOrder();
private static String[] tags = new String[]{null, "foo", "bar", "low", "high", "ledge", "bravo", "bravissimo"};
private final Random random = new Random(System.currentTimeMillis() ^ System.nanoTime());
@Test
public void testRegistrationOrder() {
int repeatShuffle = 15;
int rand1samples = BuildParameters.testLevel > 0 ? 500 : 50;
final List<List<RegistrationOrder>> samples = new ArrayList<List<RegistrationOrder>>();
// TODO: Hand crafted, random, part-random (defined tags, random order).
for (int i = 0; i < rand1samples; i++) {
samples.add(getSample(5, 12)); // Random-ish tests to have something in place.
}
// Iterate ...
for (List<RegistrationOrder> sample : samples) {
List<IGetRegistrationOrder> sample2 = new ArrayList<IGetRegistrationOrder>();
for (final RegistrationOrder order : sample) {
sample2.add(new IGetRegistrationOrder() {
@Override
public RegistrationOrder getRegistrationOrder() {
return order;
}
});
}
// Test sorting each.
testSorting(sample, accessRegistrationOrder, repeatShuffle);
testSorting(sample2, accessIGetRegistrationOrder, repeatShuffle);
// Test equality.
testEquality(sample, accessRegistrationOrder, sample2, accessIGetRegistrationOrder, repeatShuffle);
}
// TODO: Create RegistrationOrder and IGetRegistrationOrder inputs equally.
// TODO: Run sorting tests per input, and testEquality.
}
/**
* Run sorting test with the given items (!). Inputs are copied to an ArrayList.
*
* @param items
* @param fetcher
*/
private <F> void testSorting(List<F> items, IAccessSort<F> fetcher, int repeatShuffle) {
// (1) Sorting can't be stable / result in the same in general.
List<F> originalItems = items;
items = new ArrayList<F>(items);
LinkedList<F> sorted;
for (int i = 0; i < 1 + repeatShuffle; i++) {
sorted = fetcher.getSortedLinkedList(items);
testIfContained(originalItems, sorted);
testIfSorted(sorted, fetcher);
//testEquality(sorted, fetcher, fetcher.getSortedLinkedList(originalItems), fetcher); // (1)
Collections.reverse(items);
// testEquality(sorted, fetcher, fetcher.getSortedLinkedList(items), fetcher); // (1)
sorted = fetcher.getSortedLinkedList(items);
testIfContained(originalItems, sorted);
testIfSorted(sorted, fetcher);
//testEquality(sorted, fetcher, fetcher.getSortedLinkedList(originalItems), fetcher); // (1)
// Finally shuffle.
shuffle2s(items);
}
}
private <F> void testIfSorted(List<F> items, IAccessSort<F> fetcher) {
// Test priority ordering and rough region order.
Integer lastPriority = null;
Integer maxPriority = null;
boolean priorityContained = false;
boolean nullShouldFollow = false;
for (final F item : items) {
RegistrationOrder order = fetcher.getRegistrationOrder(item);
Integer basePriority = order.getBasePriority();
if (basePriority == null) {
if (maxPriority == null) {
if (order.getBeforeTag() == null && order.getAfterTag() != null) {
nullShouldFollow = true;
// Assume the pair-comparison would catch wrongly sorted null entries on occasion.
}
}
else {
if (nullShouldFollow && order.getBeforeTag() != null) {
fail("Invalid null-basePriority entry after basePriority>0 region: beforeTag is set.");
}
//
if (maxPriority > 0) {
nullShouldFollow = true;
}
else if (maxPriority == 0) {
if (order.getBeforeTag() != null) {
fail("Invalid null-basePriority entry within 0-basePriority region: beforeTag is set.");
}
}
}
}
else {
if (nullShouldFollow) {
fail("Invalid mixture of priority null/set after 0-basePriority region.");
}
else if (priorityContained && basePriority < 0 && lastPriority == null) {
fail("Invalid mixture of priority null/set before 0-basePriority region.");
}
if (maxPriority == null) {
maxPriority = basePriority;
}
else if (basePriority < maxPriority) {
fail("Order by priority broken.");
}
else {
maxPriority = basePriority;
}
priorityContained = true; // Remember to be able to exclude cases.
}
lastPriority = basePriority;
}
// Compare pairs, for being wrongly ordered.
for (int i = 1; i <items.size(); i++) {
// (Careful testing: greedy could apply both ways round.)
if (!RegistrationOrder.AbstractRegistrationOrderSort.shouldSortBefore(
fetcher.getRegistrationOrder(items.get(i - 1)),
fetcher.getRegistrationOrder(items.get(i)))) {
// Still test the other way round, to see if it's really demanded.
if (RegistrationOrder.AbstractRegistrationOrderSort.shouldSortBefore(
fetcher.getRegistrationOrder(items.get(i)),
fetcher.getRegistrationOrder(items.get(i - 1)))) {
// The following object is explicitly set to come before.
fail("Pair is wrongly ordered.");
}
// TODO: Could still consider checking i-1 versus following items, until explicit stop/end.
}
}
}
private <F> void testIfContained(List<F> originalItems, List<F> items) {
if (!new HashSet<F>(items).containsAll(originalItems)) {
fail("Sorted list does not contain all original items.");
}
}
/**
* Inputs are copied to an ArrayList.
*
* @param items1
* @param fetcher1
* @param items2
* @param fetcher2
*/
private <F1, F2> void testEquality(List<F1> items1, IAccessSort<F1> fetcher1,
List<F2> items2, IAccessSort<F2> fetcher2,
int repeatShuffle) {
// Test if sorting remains the same: original, and n times shuffled - input + reversed.
items1 = new ArrayList<F1>(items1);
items2 = new ArrayList<F2>(items2);
List<F1> sorted1;
List<F2> sorted2;
for (int i = 0; i < repeatShuffle + 1; i++) {
sorted1 = fetcher1.getSortedLinkedList(items1);
sorted2 = fetcher2.getSortedLinkedList(items2);
testEquality(sorted1, fetcher1, sorted2, fetcher2);
Collections.reverse(items1);
Collections.reverse(items2);
sorted1 = fetcher1.getSortedLinkedList(items1);
sorted2 = fetcher2.getSortedLinkedList(items2);
testEquality(sorted1, fetcher1, sorted2, fetcher2);
// In the end "shuffle" both inputs parallel.
int[][] swap = getSwapIndices(items1.size(), items1.size() * 2);
shuffle(items1, swap);
shuffle(items2, swap);
}
}
/**
* Create a somewhat regular randomized sample, having itemsPerLevel times
* items per priority level plus, plus a similar amount of null entries.<br>
* TODO: A version that guarantees every type is in?
*
* @param priorities
* @param itemsPerLevel
* @return
*/
private List<RegistrationOrder> getSample(int priorities, int itemsPerLevel) {
ArrayList<RegistrationOrder> out = new ArrayList<RegistrationOrder>();
int[] prioArr;
if (priorities <= 0) {
prioArr = new int[0];
}
else {
Set<Integer> prios = new LinkedHashSet<Integer>();
if (random.nextBoolean()) {
prios.add(0);
}
while (prios.size() < priorities) {
prios.add(random.nextInt(2 * priorities + 1) - priorities);
}
prioArr = new int[priorities];
int index = 0;
for (Integer x : prios) {
prioArr[index] = x;
index ++;
}
}
for (int i = 0; i < prioArr.length; i++) {
for (int x = 0; x < itemsPerLevel; x++) {
out.add(getRegistrationOrder(prioArr[i]));
}
}
for (int i = 0; i < itemsPerLevel; i++) {
out.add(getRegistrationOrder(null));
}
shuffle2s(out); // Extra shuffle to potentially unlink.
return out;
}
/**
* Get one using the default tags, beforeTag and afterTag get set to null, 1 2 or 3 others.
* @param priority
* @return
*/
private RegistrationOrder getRegistrationOrder(Integer basePriority) {
String tag = tags[random.nextInt(tags.length)];
String afterTag = random.nextBoolean() ? null : getTagRegex(random.nextInt(3) + 1);
String beforeTag = random.nextBoolean() ? null : getTagRegex(random.nextInt(3) + 1);
// StaticLog.logInfo("RegistrationOrder(b " + basePriority + " t " + tag + " bt " + beforeTag + " at " + afterTag + ")");
return new RegistrationOrder(basePriority, tag, beforeTag, afterTag);
}
/**
* Get a non null regex using default tags.
* @param combinations
*/
private String getTagRegex(int combinations) {
Set<String> indices = new HashSet<String>();
while (indices.size() < combinations) {
indices.add(tags[1 + random.nextInt(tags.length - 1)]); // Avoid null here.
}
// Combine tags to a regex (simple).
return"(" + StringUtil.join(indices, "|") + ")";
}
private <F1, F2> void testEquality(List<F1> items1, IAccessSort<F1> fetcher1,
List<F2> items2, IAccessSort<F2> fetcher2) {
for (int i = 0; i < items1.size(); i++) {
if (!fetcher1.getRegistrationOrder(items1.get(i)).equals(fetcher2.getRegistrationOrder(items2.get(i)))) {
// TODO: Return / log outside to give context.
doFail("Lists are not the same");
}
}
}
/**
* Shuffle by swapping items according to the give indices.
*
* @param items
* @param swap
* int[N][2], containing valid indices for items.
*/
private <F> void shuffle(List<F> items, int[][] swap) {
for (int x = 0; x < swap.length; x++) {
F temp1 = items.get(swap[x][1]);
items.set(swap[x][1], items.get(swap[x][0]));
items.set(swap[x][0], temp1);
}
}
/**
* Shuffle by swapping 2 times the list size items.
*
* @param items
*/
private <F> void shuffle2s(List<F> items) {
shuffle(items, 2 * items.size());
}
private <F> void shuffle(List<F> items, int n) {
int[][] swap = getSwapIndices(items.size(), n);
for (int x = 0; x < swap.length; x++) {
F temp1 = items.get(swap[x][1]);
items.set(swap[x][1], items.get(swap[x][0]));
items.set(swap[x][0], temp1);
}
}
/**
* Used for "shuffling".
* @param upperBound Excluded.
* @param n
* @return
*/
private int[][] getSwapIndices(int upperBound, int n) {
int[][] out = new int[n][2];
for (int i = 0; i < n; i++) {
out[i][0] = random.nextInt(upperBound);
out[i][1] = random.nextInt(upperBound);
}
return out;
}
private void doFail(String message) {
// TODO: Args.
fail(message);
}
}