Values.java
/* *******************************************************************
* Copyright (c) 2003 Contributors.
* All rights reserved.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License v 2.0
* which accompanies this distribution and is available at
* https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
*
* Contributors:
* Wes Isberg initial implementation
* ******************************************************************/
package org.aspectj.testing.util.options;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import org.aspectj.testing.util.options.Option.Value;
import org.aspectj.util.LangUtil;
/**
* Wrapper for Value[] that handles search boilerplate.
*/
public class Values {
public static final Values EMPTY;
/** used by methods taking Selector to halt processing early */
private static final boolean VERIFYING = true;
private static final boolean FIND_ALL = true;
private static final String NO_ERROR = "no error";
static {
EMPTY = new Values(new Value[0]);
}
public static Values wrapValues(Value[] values) {
if ((null == values) || (0 == values.length)) {
return EMPTY;
}
return new Values(values);
}
public static Values wrapValues(Values[] values) {
if ((null == values) || (0 == values.length)) {
return EMPTY;
}
Value[] input = null;
if (values.length == 1) {
input = values[0].asArray();
LangUtil.throwIaxIfNull(input, "values");
} else {
int length = 0;
for (int i = 0; i < values.length; i++) {
if (values[i] == null) {
LangUtil.throwIaxIfNull(null, "null value[" + i + "]");
}
length += values[i].length();
}
input = new Value[length];
length = 0;
Value[] temp;
for (Values value : values) {
temp = value.asArray();
System.arraycopy(temp, 0, input, length, temp.length);
length += temp.length;
}
}
return new Values(input);
}
static int[] invert(int[] missed, int length) {
final int MAX = length;
final int len = MAX - missed.length;
final int[] result = new int[len];
int missedIndex = 0;
int resultIndex = 0;
for (int counter = 0; counter < MAX; counter++) {
// catch result up to missed
while (((missedIndex >= missed.length)
|| (missed[missedIndex] > counter))
&& (counter < MAX)) {
result[resultIndex++] = counter++;
}
// absorb missed up to counter
while ((missedIndex < missed.length)
&& (missed[missedIndex] <= counter)
&& (counter < MAX)) {
missedIndex++;
}
}
return result;
}
private static Option.Value[] toArray(ArrayList list) {
return (Option.Value[]) list.toArray(new Option.Value[0]);
}
/**
* Resolve input to remove any values matching the same options,
* where option conflicts are handled by option forcing.
* First, for any force-off value, all matching set-on and
* the force-off itself are removed. At this time, if there
* is a matching force-on, then this will return an error.
* Next, for any force-on value, it is converted to set-on,
* and any other matching set-on value is removed.
* Finally, this signals a collision if two values share
* the same option family and the family reports that this is
* a collision.
* In all cases, only the first error detected is reported.
* @param input the Option.Value[] matched from the input,
* (forced/duplicate options will be set to null)
* @return String error during resolution, or null if no error
*/
private static String resolve(Option.Value[] input) {
String err = null;
if (LangUtil.isEmpty(input)) {
return null;
}
Map familyToMatches = new TreeMap();
for (int i = 0;(null == err) && (i < input.length); i++) {
if (null != input[i]) {
Option.Family family = input[i].option.getFamily();
int[] matches = (int[]) familyToMatches.get(family);
if (null == matches) {
matches = match(input, i);
familyToMatches.put(family, matches);
}
}
}
familyToMatches = Collections.unmodifiableMap(familyToMatches);
for (Iterator iter = familyToMatches.entrySet().iterator();
(null == err) && iter.hasNext();
) {
Map.Entry entry = (Map.Entry) iter.next();
int[] matches = (int[]) entry.getValue();
err = resolve(input, matches);
}
return err;
}
/**
* Resolve all related options into one
* by nullifying or modifying the values.
*
* First, for any force-off value,
* remove all identical set-on
* and the force-off itself,
* and alert on any identical force-on.
*
* Next, for any force-on value,
* convert to set-on,
* throw Error on any same-family force-off value,
* remove any identical force-on or set-on value,
* alert on any other non-identical same-family force-on value,
* remove any same-family set-on value,
* and alert on any same-family set-off value.
*
* Finally, alert if any two remaining values share
* the same option family, unless the option is marked
* as permitting multiple values.
*
* @param input the Option.Value[] matching the input
* @param matches the int[] list of indexes into input for
* values for related by option
* (all such values must have option matched by family)
* @return String error, if any, or null if no error
* @see #match(Option.Value[], int)
*/
private static String resolve(Option.Value[] input, int[] matches) {
String err = null;
// seek force-off
// Option.Value forceOff = null;
Option option = null;
// find and remove any force-off
for (int i = 0;(null == err) && (i < matches.length); i++) {
Option.Value value = input[matches[i]];
if (null != value) {
// verify that matches are in the same family
if (VERIFYING) {
if (null == option) {
option = value.option;
} else if (!(option.sameOptionFamily(value.option))) {
String s =
value.option
+ " has different family from "
+ option;
throw new IllegalArgumentException(s);
}
}
if (value.prefix.forceOff()) {
err = removeForceOff(input, value, matches);
}
}
}
// find and set any force-on, removing others
for (int i = 0;(null == err) && (i < matches.length); i++) {
Option.Value value = input[matches[i]];
if (null != value) {
if (value.prefix.forceOn()) {
err = convertForceOn(input, i, matches);
}
}
}
// remove any exact duplicates
for (int i = 0;(null == err) && (i < matches.length); i++) {
Option.Value value = input[matches[i]];
if (null != value) {
for (int j = i + 1; j < matches.length; j++) {
if (value.sameValueIdentifier(input[matches[j]])) {
input[matches[j]] = null;
}
}
}
}
// signal error if two left unless permitMultipleFamilyValues
Option.Value first = null;
for (int i = 0;(null == err) && (i < matches.length); i++) {
Option.Value value = input[matches[i]];
if (null != value) {
if (null == first) {
first = value;
if (first
.option
.getFamily()
.permitMultipleFamilyValues()) {
break;
}
} else {
err = "collision between " + first + " and " + value;
}
}
}
return err;
}
/**
* For any force-off value,
* remove all set-on or force-off with same value
* (including the force-off itself),
* and alert on any identical force-on.
* @param input the Option.Value[] matching the input
* @param value the force-off Option.Value to remove
* @param matches the int[] list of indexes into input for
* values for related by option
* (all such values must have matching option)
* @return String error if any
*/
private static String removeForceOff(
Option.Value[] input,
Option.Value value,
int[] matches) {
if (!value.prefix.forceOff()) {
throw new IllegalArgumentException(
"expecting force-off: " + value);
}
for (int j : matches) {
Value match = input[j];
if ((null != match) && value.sameValueIdentifier(match)) {
if (match.prefix.forceOn()) {
return "force conflict between "
+ value
+ " and "
+ match;
} else {
input[j] = null; // unset matches[i]?
}
}
}
return null;
}
/**
* For this force-on value, convert to set-on,
* throw Error on any same-family force-off value,
* remove any identical force-on or set-on value,
* alert on any other non-identical same-family force-on value,
* remove any same-family set-on value,
* and alert on any same-family set-off value.
* This must be called after <code>removeForceOff(..)</code>.
* @param input the Option.Value[] to modify
* @param valueIndex the int index in matches to find the force-on
* and to start after
* @param matches the int[] map into input entries with matching options
* @return
* @throw Error if any matching force-off found
*/
private static String convertForceOn(
Option.Value[] input,
int valueIndex,
int[] matches) {
Option.Value value = input[matches[valueIndex]];
if (!value.prefix.forceOn()) {
throw new IllegalArgumentException(
"expecting force-on: " + value);
}
input[matches[valueIndex]] = value.convert(Option.ON);
for (int i = 0; i < matches.length; i++) {
if (i == valueIndex) {
continue;
}
Option.Value match = input[matches[i]];
if (null != match) {
// assert match.sameOptionFamily(value);
if (match.prefix.forceOff()) {
throw new Error(
"unexpected force-off:"
+ match
+ " when processing "
+ value);
}
if (value.option.sameOptionIdentifier(match.option)) {
input[matches[i]] = null;
// remove any identical force-on or set
} else if (match.prefix.forceOn()) {
return "conflict between " + match + " and " + value;
} else if (match.prefix.isSet()) {
input[matches[i]] = null;
// remove any same-value set-on value
} else { // same family, force-off
return "collision between " + match + " and " + value;
}
}
}
return null;
}
/**
* Get a list of input matching the option in the initial value,
* rendered as indexes into the input array.
* @param input the Option.Value[] to seek in
* @param start the int index of the starting position
* @return int[] of indexes into input with the same option
* as index[start] - never null, but can be empty
*/
private static int[] match(Option.Value[] input, int start) {
IntList result = new IntList();
Option.Family key = null;
Option.Family nextKey = null;
for (int i = start; i < input.length; i++) {
if (null != input[i]) {
nextKey = input[i].option.getFamily();
if (null == key) {
key = nextKey;
result.add(i);
} else if (key.equals(nextKey)) {
result.add(i);
}
}
}
return result.getList();
}
static int nullify(Option.Value[] values, Selector selector) {
LangUtil.throwIaxIfNull(selector, "selector");
int changed = 0;
for (int i = 0; i < values.length; i++) {
final boolean accepted;
try {
accepted = selector.accept(values[i]);
} catch (Error e) {
if (e != Selector.STOP) {
throw e;
}
break;
}
if (accepted) {
if (null != values[i]) {
values[i] = null;
changed++;
}
}
}
return changed;
}
/**
* Render set values as String using associated prefix.
* @param values the Value[] to render
* @return String[] of values rendered for output
* (never null or longer than values, but might be shorter)
*/
private static String[] render(Value[] values) {
ArrayList list = new ArrayList();
for (Value value : values) {
if (null != value) {
String[] output = value.unflatten();
if (LangUtil.isEmpty(output)) {
throw new Error("no output for " + value);
}
String s = value.prefix.render(output[0]);
if (null != s) { // this means the prefix is set
list.add(s);
list.addAll(Arrays.asList(output).subList(1, output.length));
}
}
}
return (String[]) list.toArray(new String[0]);
}
private final Option.Value[] values;
private Option.Value[] valuesNotNull;
private String resolveError;
private Values(Value[] values) {
this.values = new Value[values.length];
System.arraycopy(values, 0, this.values, 0, values.length);
}
public int length() {
return values.length;
}
public Option.Value[] asArray() {
Option.Value[] result = new Option.Value[values.length];
System.arraycopy(values, 0, result, 0, result.length);
return result;
}
/**
* Emit as String[] the non-null values.
* @return String[] of matched entries (never null, elements not null)
*/
public String[] render() {
return Values.render(valuesNotNull());
}
public String toString() {
return Arrays.asList(values).toString();
}
/**
* Create index into values of those that were matched,
* including the options and their arguments.
* @return int[] of elements in values that are not null (options)
* or that represent option arguments
*/
public int[] indexMatches() {
// must be in order, low to high
final int[] missed = indexMissedMatches();
return invert(missed, length());
}
/**
* Create index into values of missed input,
* taking into account that matched arguments are
* represented as null.
* @return int[] of elements in values that are null
* or optionally represent option arguments
*/
public int[] indexMissedMatches() {
MissedSelector selector = new MissedSelector();
find(selector, FIND_ALL);
String errors = selector.getErrors();
if (null != errors) {
throw new Error(errors);
}
return selector.getResult();
}
public Value firstInFamily(Option.Family family) {
return findFirst(new ValueSelector(family));
}
public Value[] allInFamily(Option.Family family) {
return find(new ValueSelector(family), FIND_ALL);
}
public Value firstOption(Option option) {
return findFirst(new ValueSelector(option));
}
public Value[] allOption(Option option) {
return find(new ValueSelector(option), FIND_ALL);
}
public Value firstValue(Option option, String value) {
LangUtil.throwIaxIfNull(value, "value");
return findFirst(new ValueSelector(option, value));
}
public Value[] allValues(Option option, String value) {
LangUtil.throwIaxIfNull(value, "value");
return find(new ValueSelector(option, value), FIND_ALL);
}
public boolean isResolved() {
return ((this != EMPTY) && (null != resolveError));
}
/**
*
* @param selector the Selector to pick out entries to nullify
* (should throw STOP to halt processing)
* @return Values resulting from nullifying entries,
* or this if none were changed
*/
public Values nullify(Selector selector) {
if (null == selector) {
return this;
}
Value[] temp = asArray();
int changed = nullify(temp, selector);
if (0 == changed) {
return this;
}
return new Values(temp);
}
/**
* Resolve options, removing duplicates by force if necessary.
* If any error is returned, then the values are left unchanged.
* @return String error, if any
* @throws IllegalStateException if <code>isResolved()</code>
*/
public String resolve() {
if (isResolved()) {
throw new IllegalStateException("already resolved");
}
Option.Value[] temp = asArray();
resolveError = resolve(temp);
if (null == resolveError) {
System.arraycopy(temp, 0, values, 0, temp.length);
valuesNotNull = null;
resolveError = NO_ERROR;
return null;
}
return resolveError;
}
protected Option.Value findFirst(Selector filter) {
Option.Value[] result = find(filter, !FIND_ALL);
return (0 == result.length ? null : result[0]);
}
protected Option.Value[] find(Selector filter, boolean findAll) {
LangUtil.throwIaxIfNull(filter, "filter");
ArrayList result = new ArrayList();
for (Value value : values) {
final boolean accepted;
try {
accepted = filter.accept(value);
} catch (Error e) {
if (Selector.STOP != e) {
throw e;
}
break;
}
if (accepted) {
result.add(value);
if (findAll != FIND_ALL) {
break;
}
}
}
return toArray(result);
}
private Option.Value[] valuesNotNull() {
if (null == valuesNotNull) {
ArrayList list = new ArrayList();
for (Value value : this.values) {
if (null != value) {
list.add(value);
}
}
valuesNotNull = toArray(list);
}
return valuesNotNull;
}
public static class Selector {
public static final Error STOP = new Error("stop invoking Selector");
protected Selector() {
}
protected boolean accept(Value value) {
return false;
}
}
protected static class ValueSelector extends Selector {
private final Option option;
private final Option.Family family;
private final String value;
ValueSelector(Option.Family family) {
LangUtil.throwIaxIfNull(family, "family");
this.family = family;
option = null;
value = null;
}
ValueSelector(Option option) {
this(option, (String) null);
}
ValueSelector(Option option, String value) {
LangUtil.throwIaxIfNull(option, "option");
this.option = option;
family = null;
this.value = value;
}
protected boolean accept(Value value) {
if (null == value) {
return false;
}
if (null != family) {
return family.sameFamily(value.option.getFamily());
} else if (!option.sameOptionIdentifier(value.option)) {
return false;
} else {
return ((null == this.value)
|| (this.value.equals(value.value)));
}
}
}
/** pick all null entries (except for args), return as int[] */
protected static class MissedSelector extends Selector {
public static final String DELIM = "; ";
final IntList result = new IntList();
int index;
final StringBuffer errors = new StringBuffer();
int argsExpected;
Option argsExpectedFor;
MissedSelector() {
}
int[] getResult() {
return result.getList();
}
/**
* add index if value is null
* unless skipArguments
*/
protected boolean accept(Value value) {
index++;
if (null != value) {
if (0 < argsExpected) { // expected more (null) args
missedArgsFor(argsExpectedFor, argsExpected);
}
argsExpected = value.option.numArguments();
argsExpectedFor = value.option;
} else if (0 < argsExpected) { // ignore null in arg position
argsExpected--;
if (0 == argsExpected) {
argsExpectedFor = null;
}
} else { // null, not expecting arg, so missing
result.add(index - 1);
return true;
}
return false;
}
private void missedArgsFor(Option option, int numArgs) {
errors.append("missed ");
errors.append(numArgs + " args for ");
errors.append(option + DELIM);
}
String getErrors() {
if (0 < argsExpected) {
}
if (0 == errors.length()) {
return null;
}
return errors.toString();
}
}
static class IntList {
// not synchronized - used only in one thread
static String render(int[] input) {
if (null == input) {
return "null";
}
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < input.length; i++) {
if (i > 0) {
sb.append(", " + input[i]);
} else {
sb.append("" + input[i]);
}
}
sb.append("]");
return sb.toString();
}
private int[] input = new int[256];
private int insert;
private void add(int i) {
if (insert >= input.length) {
int[] temp = new int[insert + 256];
System.arraycopy(input, 0, temp, 0, input.length);
input = temp;
}
input[insert++] = i;
}
private int[] getList() {
int[] result = new int[insert];
if (result.length >= 0) System.arraycopy(input, 0, result, 0, result.length);
return result;
}
}
}