http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/NumberRange.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/NumberRange.java b/src/main/groovy/groovy/lang/NumberRange.java new file mode 100644 index 0000000..52ef856 --- /dev/null +++ b/src/main/groovy/groovy/lang/NumberRange.java @@ -0,0 +1,629 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package groovy.lang; + +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.runtime.IteratorClosureAdapter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.AbstractList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareEqual; +import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareGreaterThan; +import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareGreaterThanEqual; +import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareLessThan; +import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareLessThanEqual; +import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareNotEqual; +import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareTo; +import static org.codehaus.groovy.runtime.dgmimpl.NumberNumberMinus.minus; +import static org.codehaus.groovy.runtime.dgmimpl.NumberNumberMultiply.multiply; +import static org.codehaus.groovy.runtime.dgmimpl.NumberNumberPlus.plus; + +/** + * Represents an immutable list of Numbers from a value to a value with a particular step size. + * + * In general, it isn't recommended using a NumberRange as a key to a map. The range + * 0..3 is deemed to be equal to 0.0..3.0 but they have different hashCode values, + * so storing a value using one of these ranges couldn't be retrieved using the other. + * + * @since 2.5.0 + */ +public class NumberRange extends AbstractList<Comparable> implements Range<Comparable> { + + /** + * The first value in the range. + */ + private final Comparable from; + + /** + * The last value in the range. + */ + private final Comparable to; + + /** + * The step size in the range. + */ + private final Number stepSize; + + /** + * The cached size, or -1 if not yet computed + */ + private int size = -1; + + /** + * The cached hashCode (once calculated) + */ + private Integer hashCodeCache = null; + + /** + * <code>true</code> if the range counts backwards from <code>to</code> to <code>from</code>. + */ + private final boolean reverse; + + /** + * <code>true</code> if the range includes the upper bound. + */ + private final boolean inclusive; + + /** + * Creates an inclusive {@link NumberRange} with step size 1. + * Creates a reversed range if <code>from</code> < <code>to</code>. + * + * @param from the first value in the range + * @param to the last value in the range + */ + public <T extends Number & Comparable, U extends Number & Comparable> + NumberRange(T from, U to) { + this(from, to, null, true); + } + + /** + * Creates a new {@link NumberRange} with step size 1. + * Creates a reversed range if <code>from</code> < <code>to</code>. + * + * @param from start of the range + * @param to end of the range + * @param inclusive whether the range is inclusive + */ + public <T extends Number & Comparable, U extends Number & Comparable> + NumberRange(T from, U to, boolean inclusive) { + this(from, to, null, inclusive); + } + + /** + * Creates an inclusive {@link NumberRange}. + * Creates a reversed range if <code>from</code> < <code>to</code>. + * + * @param from start of the range + * @param to end of the range + * @param stepSize the gap between discrete elements in the range + */ + public <T extends Number & Comparable, U extends Number & Comparable, V extends + Number & Comparable<? super Number>> + NumberRange(T from, U to, V stepSize) { + this(from, to, stepSize, true); + } + + /** + * Creates a {@link NumberRange}. + * Creates a reversed range if <code>from</code> < <code>to</code>. + * + * @param from start of the range + * @param to end of the range + * @param stepSize the gap between discrete elements in the range + * @param inclusive whether the range is inclusive + */ + public <T extends Number & Comparable, U extends Number & Comparable, V extends + Number & Comparable> + NumberRange(T from, U to, V stepSize, boolean inclusive) { + if (from == null) { + throw new IllegalArgumentException("Must specify a non-null value for the 'from' index in a Range"); + } + if (to == null) { + throw new IllegalArgumentException("Must specify a non-null value for the 'to' index in a Range"); + } + reverse = areReversed(from, to); + Number tempFrom; + Number tempTo; + if (reverse) { + tempFrom = to; + tempTo = from; + } else { + tempFrom = from; + tempTo = to; + } + if (tempFrom instanceof Short) { + tempFrom = tempFrom.intValue(); + } else if (tempFrom instanceof Float) { + tempFrom = tempFrom.doubleValue(); + } + if (tempTo instanceof Short) { + tempTo = tempTo.intValue(); + } else if (tempTo instanceof Float) { + tempTo = tempTo.doubleValue(); + } + + if (tempFrom instanceof Integer && tempTo instanceof Long) { + tempFrom = tempFrom.longValue(); + } else if (tempTo instanceof Integer && tempFrom instanceof Long) { + tempTo = tempTo.longValue(); + } + + this.from = (Comparable) tempFrom; + this.to = (Comparable) tempTo; + this.stepSize = stepSize == null ? 1 : stepSize; + this.inclusive = inclusive; + } + + /** + * For a NumberRange with step size 1, creates a new NumberRange with the same + * <code>from</code> and <code>to</code> as this NumberRange + * but with a step size of <code>stepSize</code>. + * + * @param stepSize the desired step size + * @return a new NumberRange + */ + public <T extends Number & Comparable> NumberRange by(T stepSize) { + if (!Integer.valueOf(1).equals(this.stepSize)) { + throw new IllegalStateException("by only allowed on ranges with original stepSize = 1 but found " + this.stepSize); + } + return new NumberRange(comparableNumber(from), comparableNumber(to), stepSize, inclusive); + } + + @SuppressWarnings("unchecked") + /* package private */ static <T extends Number & Comparable> T comparableNumber(Comparable c) { + return (T) c; + } + + @SuppressWarnings("unchecked") + /* package private */ static <T extends Number & Comparable> T comparableNumber(Number n) { + return (T) n; + } + + private static boolean areReversed(Number from, Number to) { + try { + return compareGreaterThan(from, to); + } catch (ClassCastException cce) { + throw new IllegalArgumentException("Unable to create range due to incompatible types: " + from.getClass().getSimpleName() + ".." + to.getClass().getSimpleName() + " (possible missing brackets around range?)", cce); + } + } + + /** + * An object is deemed equal to this NumberRange if it represents a List of items and + * those items equal the list of discrete items represented by this NumberRange. + * + * @param that the object to be compared for equality with this NumberRange + * @return {@code true} if the specified object is equal to this NumberRange + * @see #fastEquals(NumberRange) + */ + @Override + public boolean equals(Object that) { + return super.equals(that); + } + + /** + * A NumberRange's hashCode is based on hashCode values of the discrete items it represents. + * + * @return the hashCode value + */ + @Override + public int hashCode() { + if (hashCodeCache == null) { + hashCodeCache = super.hashCode(); + } + return hashCodeCache; + } + + /* + * NOTE: as per the class javadoc, this class doesn't obey the normal equals/hashCode contract. + * The following field and method could assist some scenarios which required a similar sort of contract + * (but between equals and the custom canonicalHashCode). Currently commented out since we haven't + * found a real need. We will likely remove this commented out code if no usage is identified soon. + */ + + /* + * The cached canonical hashCode (once calculated) + */ +// private Integer canonicalHashCodeCache = null; + + /* + * A NumberRange's canonicalHashCode is based on hashCode values of the discrete items it represents. + * When two NumberRange's are equal they will have the same canonicalHashCode value. + * Numerical values which Groovy deems equal have the same hashCode during this calculation. + * So currently (0..3).equals(0.0..3.0) yet they have different hashCode values. This breaks + * the normal equals/hashCode contract which is a weakness in Groovy's '==' operator. However + * the contract isn't broken between equals and canonicalHashCode. + * + * @return the hashCode value + */ +// public int canonicalHashCode() { +// if (canonicalHashCodeCache == null) { +// int hashCode = 1; +// for (Comparable e : this) { +// int value; +// if (e == null) { +// value = 0; +// } else { +// BigDecimal next = new BigDecimal(e.toString()); +// if (next.compareTo(BigDecimal.ZERO) == 0) { +// // workaround on pre-Java8 for http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6480539 +// value = BigDecimal.ZERO.hashCode(); +// } else { +// value = next.stripTrailingZeros().hashCode(); +// } +// } +// hashCode = 31 * hashCode + value; +// } +// canonicalHashCodeCache = hashCode; +// } +// return canonicalHashCodeCache; +// } + + /** + * Compares a {@link NumberRange} to another {@link NumberRange} using only a strict comparison + * of the NumberRange properties. This won't return true for some ranges which represent the same + * discrete items, use equals instead for that but will be much faster for large lists. + * + * @param that the NumberRange to check equality with + * @return <code>true</code> if the ranges are equal + */ + public boolean fastEquals(NumberRange that) { + return that != null + && reverse == that.reverse + && inclusive == that.inclusive + && compareEqual(from, that.from) + && compareEqual(to, that.to) + && compareEqual(stepSize, that.stepSize); + } + + /* + * NOTE: as per the class javadoc, this class doesn't obey the normal equals/hashCode contract. + * The following field and method could assist some scenarios which required a similar sort of contract + * (but between fastEquals and the custom fastHashCode). Currently commented out since we haven't + * found a real need. We will likely remove this commented out code if no usage is identified soon. + */ + + /* + * The cached fast hashCode (once calculated) + */ +// private Integer fastHashCodeCache = null; + + /* + * A hashCode function that pairs with fastEquals, following the normal equals/hashCode contract. + * + * @return the calculated hash code + */ +// public int fastHashCode() { +// if (fastHashCodeCache == null) { +// int result = 17; +// result = result * 31 + (reverse ? 1 : 0); +// result = result * 31 + (inclusive ? 1 : 0); +// result = result * 31 + new BigDecimal(from.toString()).stripTrailingZeros().hashCode(); +// result = result * 31 + new BigDecimal(to.toString()).stripTrailingZeros().hashCode(); +// result = result * 31 + new BigDecimal(stepSize.toString()).stripTrailingZeros().hashCode(); +// fastHashCodeCache = result; +// } +// return fastHashCodeCache; +// } + + @Override + public Comparable getFrom() { + return from; + } + + @Override + public Comparable getTo() { + return to; + } + + public Comparable getStepSize() { + return (Comparable) stepSize; + } + + @Override + public boolean isReverse() { + return reverse; + } + + @Override + public Comparable get(int index) { + if (index < 0) { + throw new IndexOutOfBoundsException("Index: " + index + " should not be negative"); + } + final Iterator<Comparable> iter = new StepIterator(this, stepSize); + + Comparable value = iter.next(); + for (int i = 0; i < index; i++) { + if (!iter.hasNext()) { + throw new IndexOutOfBoundsException("Index: " + index + " is too big for range: " + this); + } + value = iter.next(); + } + return value; + } + + /** + * Checks whether a value is between the from and to values of a Range + * + * @param value the value of interest + * @return true if the value is within the bounds + */ + @Override + public boolean containsWithinBounds(Object value) { + final int result = compareTo(from, value); + return result == 0 || result < 0 && compareTo(to, value) >= 0; + } + + /** + * protection against calls from Groovy + */ + @SuppressWarnings("unused") + private void setSize(int size) { + throw new UnsupportedOperationException("size must not be changed"); + } + + @Override + public int size() { + if (size == -1) { + calcSize(from, to, stepSize); + } + return size; + } + + void calcSize(Comparable from, Comparable to, Number stepSize) { + int tempsize = 0; + boolean shortcut = false; + if (isIntegral(stepSize)) { + if ((from instanceof Integer || from instanceof Long) + && (to instanceof Integer || to instanceof Long)) { + // let's fast calculate the size + final BigInteger fromNum = new BigInteger(from.toString()); + final BigInteger toTemp = new BigInteger(to.toString()); + final BigInteger toNum = inclusive ? toTemp : toTemp.subtract(BigInteger.ONE); + final BigInteger sizeNum = new BigDecimal(toNum.subtract(fromNum)).divide(new BigDecimal(stepSize.longValue()), BigDecimal.ROUND_DOWN).toBigInteger().add(BigInteger.ONE); + tempsize = sizeNum.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) == -1 ? sizeNum.intValue() : Integer.MAX_VALUE; + shortcut = true; + } else if (((from instanceof BigDecimal || from instanceof BigInteger) && to instanceof Number) || + ((to instanceof BigDecimal || to instanceof BigInteger) && from instanceof Number)) { + // let's fast calculate the size + final BigDecimal fromNum = new BigDecimal(from.toString()); + final BigDecimal toTemp = new BigDecimal(to.toString()); + final BigDecimal toNum = inclusive ? toTemp : toTemp.subtract(new BigDecimal("1.0")); + final BigInteger sizeNum = toNum.subtract(fromNum).divide(new BigDecimal(stepSize.longValue()), BigDecimal.ROUND_DOWN).toBigInteger().add(BigInteger.ONE); + tempsize = sizeNum.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) == -1 ? sizeNum.intValue() : Integer.MAX_VALUE; + shortcut = true; + } + } + if (!shortcut) { + // let's brute-force calculate the size by iterating start to end + final Iterator iter = new StepIterator(this, stepSize); + while (iter.hasNext()) { + tempsize++; + // integer overflow + if (tempsize < 0) { + break; + } + iter.next(); + } + // integer overflow + if (tempsize < 0) { + tempsize = Integer.MAX_VALUE; + } + } + size = tempsize; + } + + private boolean isIntegral(Number stepSize) { + BigDecimal tempStepSize = new BigDecimal(stepSize.toString()); + return tempStepSize.equals(new BigDecimal(tempStepSize.toBigInteger())); + } + + @Override + public List<Comparable> subList(int fromIndex, int toIndex) { + if (fromIndex < 0) { + throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); + } + if (fromIndex > toIndex) { + throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); + } + if (fromIndex == toIndex) { + return new EmptyRange<Comparable>(from); + } + + // Performance detail: + // not using get(fromIndex), get(toIndex) in the following to avoid stepping over elements twice + final Iterator<Comparable> iter = new StepIterator(this, stepSize); + + Comparable value = iter.next(); + int i = 0; + for (; i < fromIndex; i++) { + if (!iter.hasNext()) { + throw new IndexOutOfBoundsException("Index: " + i + " is too big for range: " + this); + } + value = iter.next(); + } + final Comparable fromValue = value; + for (; i < toIndex - 1; i++) { + if (!iter.hasNext()) { + throw new IndexOutOfBoundsException("Index: " + i + " is too big for range: " + this); + } + value = iter.next(); + } + final Comparable toValue = value; + + return new NumberRange(comparableNumber(fromValue), comparableNumber(toValue), comparableNumber(stepSize), true); + } + + @Override + public String toString() { + return getToString(to.toString(), from.toString()); + } + + @Override + public String inspect() { + return getToString(InvokerHelper.inspect(to), InvokerHelper.inspect(from)); + } + + private String getToString(String toText, String fromText) { + String sep = inclusive ? ".." : "..<"; + String base = reverse ? "" + toText + sep + fromText : "" + fromText + sep + toText; + return Integer.valueOf(1).equals(stepSize) ? base : base + ".by(" + stepSize + ")"; + } + + /** + * iterates over all values and returns true if one value matches. + * Also see containsWithinBounds. + */ + @Override + public boolean contains(Object value) { + if (value == null) { + return false; + } + final Iterator it = new StepIterator(this, stepSize); + while (it.hasNext()) { + if (compareEqual(value, it.next())) { + return true; + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void step(int numSteps, Closure closure) { + if (numSteps == 0 && compareTo(from, to) == 0) { + return; // from == to and step == 0, nothing to do, so return + } + final StepIterator iter = new StepIterator(this, multiply(numSteps, stepSize)); + while (iter.hasNext()) { + closure.call(iter.next()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator<Comparable> iterator() { + return new StepIterator(this, stepSize); + } + + /** + * convenience class to serve in other methods. + * It's not thread-safe, and lazily produces the next element only on calls of hasNext() or next() + */ + private class StepIterator implements Iterator<Comparable> { + private final NumberRange range; + private final Number step; + private final boolean isAscending; + + private boolean isNextFetched = false; + private Comparable next = null; + + StepIterator(NumberRange range, Number step) { + if (compareEqual(step, 0) && compareNotEqual(range.getFrom(), range.getTo())) { + throw new GroovyRuntimeException("Infinite loop detected due to step size of 0"); + } + + this.range = range; + if (compareLessThan(step, 0)) { + this.step = multiply(step, -1); + isAscending = range.isReverse(); + } else { + this.step = step; + isAscending = !range.isReverse(); + } + } + + @Override + public boolean hasNext() { + fetchNextIfNeeded(); + return (next != null) && (isAscending + ? (range.inclusive ? compareLessThanEqual(next, range.getTo()) : compareLessThan(next, range.getTo())) + : (range.inclusive ? compareGreaterThanEqual(next, range.getFrom()) : compareGreaterThan(next, range.getFrom()))); + } + + @Override + public Comparable next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + fetchNextIfNeeded(); + isNextFetched = false; + return next; + } + + private void fetchNextIfNeeded() { + if (!isNextFetched) { + isNextFetched = true; + + if (next == null) { + // make the first fetch lazy too + next = isAscending ? range.getFrom() : range.getTo(); + } else { + next = isAscending ? increment(next, step) : decrement(next, step); + } + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public List<Comparable> step(int numSteps) { + final IteratorClosureAdapter<Comparable> adapter = new IteratorClosureAdapter<Comparable>(this); + step(numSteps, adapter); + return adapter.asList(); + } + + /** + * Increments by given step + * + * @param value the value to increment + * @param step the amount to increment + * @return the incremented value + */ + @SuppressWarnings("unchecked") + private Comparable increment(Object value, Number step) { + return (Comparable) plus((Number) value, step); + } + + /** + * Decrements by given step + * + * @param value the value to decrement + * @param step the amount to decrement + * @return the decremented value + */ + @SuppressWarnings("unchecked") + private Comparable decrement(Object value, Number step) { + return (Comparable) minus((Number) value, step); + } +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/ObjectRange.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/ObjectRange.java b/src/main/groovy/groovy/lang/ObjectRange.java new file mode 100644 index 0000000..a7e2b05 --- /dev/null +++ b/src/main/groovy/groovy/lang/ObjectRange.java @@ -0,0 +1,539 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.runtime.IteratorClosureAdapter; +import org.codehaus.groovy.runtime.ScriptBytecodeAdapter; +import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.AbstractList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Represents an inclusive list of objects from a value to a value using + * comparators. + * <p> + * Note: This class is similar to {@link IntRange}. If you make any changes to this + * class, you might consider making parallel changes to {@link IntRange}. + */ +public class ObjectRange extends AbstractList<Comparable> implements Range<Comparable> { + /** + * The first value in the range. + */ + private final Comparable from; + + /** + * The last value in the range. + */ + private final Comparable to; + + /** + * The cached size, or -1 if not yet computed + */ + private int size = -1; + + /** + * <code>true</code> if the range counts backwards from <code>to</code> to <code>from</code>. + */ + private final boolean reverse; + + /** + * Creates a new {@link ObjectRange}. Creates a reversed range if + * <code>from</code> < <code>to</code>. + * + * @param from the first value in the range. + * @param to the last value in the range. + */ + public ObjectRange(Comparable from, Comparable to) { + this(from, to, null); + } + + /** + * Creates a new {@link ObjectRange} assumes smaller <= larger, else behavior is undefined. + * Caution: Prefer the other constructor when in doubt. + * <p> + * Optimized Constructor avoiding initial computation of comparison. + */ + public ObjectRange(Comparable smaller, Comparable larger, boolean reverse) { + this(smaller, larger, (Boolean) reverse); + } + + /** + * Constructs a Range, computing reverse if not provided. When providing reverse, + * 'smaller' must not be larger than 'larger'. + * + * @param smaller start of the range, must no be larger than to when reverse != null + * @param larger end of the range, must be larger than from when reverse != null + * @param reverse direction of the range. If null, causes direction to be computed (can be expensive). + */ + private ObjectRange(Comparable smaller, Comparable larger, Boolean reverse) { + if (smaller == null) { + throw new IllegalArgumentException("Must specify a non-null value for the 'from' index in a Range"); + } + if (larger == null) { + throw new IllegalArgumentException("Must specify a non-null value for the 'to' index in a Range"); + } + if (reverse == null) { + final boolean computedReverse = areReversed(smaller, larger); + // ensure invariant from <= to + if (computedReverse) { + final Comparable temp = larger; + larger = smaller; + smaller = temp; + } + this.reverse = computedReverse; + } else { + this.reverse = reverse; + } + + if (smaller instanceof Short) { + smaller = ((Short) smaller).intValue(); + } else if (smaller instanceof Float) { + smaller = ((Float) smaller).doubleValue(); + } + if (larger instanceof Short) { + larger = ((Short) larger).intValue(); + } else if (larger instanceof Float) { + larger = ((Float) larger).doubleValue(); + } + + if (smaller instanceof Integer && larger instanceof Long) { + smaller = ((Integer) smaller).longValue(); + } else if (larger instanceof Integer && smaller instanceof Long) { + larger = ((Integer) larger).longValue(); + } + + /* + areReversed() already does an implicit type compatibility check + based on DefaultTypeTransformation.compareToWithEqualityCheck() for mixed classes + but it is only invoked if reverse == null. + So Object Range has to perform those type checks for consistency even when not calling + compareToWithEqualityCheck(), and ObjectRange has + to use the normalized value used in a successful comparison in + compareToWithEqualityCheck(). Currently that means Chars and single-char Strings + are evaluated as the char's charValue (an integer) when compared to numbers. + So '7'..'9' should produce ['7', '8', '9'], whereas ['7'..9] and [7..'9'] should produce [55, 56, 57]. + if classes match, or both numerical, no checks possible / necessary + */ + if (smaller.getClass() == larger.getClass() || + (smaller instanceof Number && larger instanceof Number)) { + this.from = smaller; + this.to = larger; + } else { + // Convenience hack: try convert single-char strings to ints + final Comparable tempfrom = normaliseStringType(smaller); + final Comparable tempto = normaliseStringType(larger); + // if after normalizing both are numbers, assume intended range was numbers + if (tempfrom instanceof Number && tempto instanceof Number) { + this.from = tempfrom; + this.to = tempto; + } else { + // if convenience hack did not make classes match, + // throw exception when starting with known class, and thus "from" cannot be advanced over "to". + // Note if start is an unusual Object, it could have a next() method + // that yields a Number or String to close the range + final Comparable start = this.reverse ? larger : smaller; + if (start instanceof String || start instanceof Number) { + // starting with number will never reach a non-number, same for string + throw new IllegalArgumentException("Incompatible Argument classes for ObjectRange " + smaller.getClass() + ", " + larger.getClass()); + } + // Since normalizing did not help, use original values at user's risk + this.from = smaller; + this.to = larger; + } + } + checkBoundaryCompatibility(); + } + + /** + * throws IllegalArgumentException if to and from are incompatible, meaning they e.g. (likely) produce infinite sequences. + * Called at construction time, subclasses may override cautiously (using only members to and from). + */ + protected void checkBoundaryCompatibility() { + if (from instanceof String && to instanceof String) { + // this test depends deeply on the String.next implementation + // 009.next is 00:, not 010 + final String start = from.toString(); + final String end = to.toString(); + if (start.length() != end.length()) { + throw new IllegalArgumentException("Incompatible Strings for Range: different length"); + } + final int length = start.length(); + int i; + for (i = 0; i < length; i++) { + if (start.charAt(i) != end.charAt(i)) { + break; + } + } + // strings must be equal except for the last character + if (i < length - 1) { + throw new IllegalArgumentException("Incompatible Strings for Range: String#next() will not reach the expected value"); + } + } + } + + private static boolean areReversed(Comparable from, Comparable to) { + try { + return ScriptBytecodeAdapter.compareGreaterThan(from, to); + } catch (IllegalArgumentException iae) { + throw new IllegalArgumentException("Unable to create range due to incompatible types: " + from.getClass().getSimpleName() + ".." + to.getClass().getSimpleName() + " (possible missing brackets around range?)", iae); + } + } + + public boolean equals(Object that) { + return (that instanceof ObjectRange) ? equals((ObjectRange) that) : super.equals(that); + } + + /** + * Compares an {@link ObjectRange} to another {@link ObjectRange}. + * + * @param that the object to check equality with + * @return <code>true</code> if the ranges are equal + */ + public boolean equals(ObjectRange that) { + return that != null + && reverse == that.reverse + && DefaultTypeTransformation.compareEqual(from, that.from) + && DefaultTypeTransformation.compareEqual(to, that.to); + } + + @Override + public Comparable getFrom() { + return from; + } + + @Override + public Comparable getTo() { + return to; + } + + @Override + public boolean isReverse() { + return reverse; + } + + @Override + public Comparable get(int index) { + if (index < 0) { + throw new IndexOutOfBoundsException("Index: " + index + " should not be negative"); + } + final StepIterator iter = new StepIterator(this, 1); + + Comparable value = iter.next(); + for (int i = 0; i < index; i++) { + if (!iter.hasNext()) { + throw new IndexOutOfBoundsException("Index: " + index + " is too big for range: " + this); + } + value = iter.next(); + } + return value; + } + + /** + * Checks whether a value is between the from and to values of a Range + * + * @param value the value of interest + * @return true if the value is within the bounds + */ + @Override + public boolean containsWithinBounds(Object value) { + if (value instanceof Comparable) { + final int result = compareTo(from, (Comparable) value); + return result == 0 || result < 0 && compareTo(to, (Comparable) value) >= 0; + } + return contains(value); + } + + protected int compareTo(Comparable first, Comparable second) { + return DefaultGroovyMethods.numberAwareCompareTo(first, second); + } + + /** + * protection against calls from Groovy + */ + @SuppressWarnings("unused") + private void setSize(int size) { + throw new UnsupportedOperationException("size must not be changed"); + } + + @Override + public int size() { + if (size == -1) { + int tempsize = 0; + if ((from instanceof Integer || from instanceof Long) + && (to instanceof Integer || to instanceof Long)) { + // let's fast calculate the size + final BigInteger fromNum = new BigInteger(from.toString()); + final BigInteger toNum = new BigInteger(to.toString()); + final BigInteger sizeNum = toNum.subtract(fromNum).add(new BigInteger("1")); + tempsize = sizeNum.intValue(); + if (!BigInteger.valueOf(tempsize).equals(sizeNum)) { + tempsize = Integer.MAX_VALUE; + } + } else if (from instanceof Character && to instanceof Character) { + // let's fast calculate the size + final char fromNum = (Character) from; + final char toNum = (Character) to; + tempsize = toNum - fromNum + 1; + } else if (((from instanceof BigDecimal || from instanceof BigInteger) && to instanceof Number) || + ((to instanceof BigDecimal || to instanceof BigInteger) && from instanceof Number)) { + // let's fast calculate the size + final BigDecimal fromNum = new BigDecimal(from.toString()); + final BigDecimal toNum = new BigDecimal(to.toString()); + final BigInteger sizeNum = toNum.subtract(fromNum).add(new BigDecimal(1.0)).toBigInteger(); + tempsize = sizeNum.intValue(); + if (!BigInteger.valueOf(tempsize).equals(sizeNum)) { + tempsize = Integer.MAX_VALUE; + } + } else { + // let's brute-force calculate the size by iterating start to end + final Iterator<Comparable> iter = new StepIterator(this, 1); + while (iter.hasNext()) { + tempsize++; + // integer overflow + if (tempsize < 0) { + break; + } + iter.next(); + } + } + // integer overflow + if (tempsize < 0) { + tempsize = Integer.MAX_VALUE; + } + size = tempsize; + } + return size; + } + + @Override + public List<Comparable> subList(int fromIndex, int toIndex) { + if (fromIndex < 0) { + throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); + } + if (fromIndex > toIndex) { + throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); + } + if (fromIndex == toIndex) { + return new EmptyRange<Comparable>(from); + } + + // Performance detail: + // not using get(fromIndex), get(toIndex) in the following to avoid stepping over elements twice + final Iterator<Comparable> iter = new StepIterator(this, 1); + + Comparable toValue = iter.next(); + int i = 0; + for (; i < fromIndex; i++) { + if (!iter.hasNext()) { + throw new IndexOutOfBoundsException("Index: " + i + " is too big for range: " + this); + } + toValue = iter.next(); + } + final Comparable fromValue = toValue; + for (; i < toIndex - 1; i++) { + if (!iter.hasNext()) { + throw new IndexOutOfBoundsException("Index: " + i + " is too big for range: " + this); + } + toValue = iter.next(); + } + + return new ObjectRange(fromValue, toValue, reverse); + } + + public String toString() { + return reverse ? "" + to + ".." + from : "" + from + ".." + to; + } + + @Override + public String inspect() { + final String toText = InvokerHelper.inspect(to); + final String fromText = InvokerHelper.inspect(from); + return reverse ? "" + toText + ".." + fromText : "" + fromText + ".." + toText; + } + + /** + * Iterates over all values and returns true if one value matches. + * + * @see #containsWithinBounds(Object) + */ + @Override + public boolean contains(Object value) { + final Iterator<Comparable> iter = new StepIterator(this, 1); + if (value == null) { + return false; + } + while (iter.hasNext()) { + if (DefaultTypeTransformation.compareEqual(value, iter.next())) return true; + } + return false; + } + + @Override + public void step(int step, Closure closure) { + if (step == 0 && compareTo(from, to) == 0) { + return; // from == to and step == 0, nothing to do, so return + } + final Iterator<Comparable> iter = new StepIterator(this, step); + while (iter.hasNext()) { + closure.call(iter.next()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator<Comparable> iterator() { + // non thread-safe iterator + return new StepIterator(this, 1); + } + + /** + * Non-thread-safe iterator which lazily produces the next element only on calls of hasNext() or next() + */ + private static final class StepIterator implements Iterator<Comparable> { + // actual step, can be +1 when desired step is -1 and direction is from high to low + private final int step; + private final ObjectRange range; + private int index = -1; + private Comparable value; + private boolean nextFetched = true; + + private StepIterator(ObjectRange range, final int desiredStep) { + if (desiredStep == 0 && range.compareTo(range.getFrom(), range.getTo()) != 0) { + throw new GroovyRuntimeException("Infinite loop detected due to step size of 0"); + } + this.range = range; + if (range.isReverse()) { + step = -desiredStep; + } else { + step = desiredStep; + } + if (step > 0) { + value = range.getFrom(); + } else { + value = range.getTo(); + } + } + + @Override + public void remove() { + range.remove(index); + } + + @Override + public Comparable next() { + // not thread safe + if (!hasNext()) { + throw new NoSuchElementException(); + } + nextFetched = false; + index++; + return value; + } + + @Override + public boolean hasNext() { + // not thread safe + if (!nextFetched) { + value = peek(); + nextFetched = true; + } + return value != null; + } + + private Comparable peek() { + if (step > 0) { + Comparable peekValue = value; + for (int i = 0; i < step; i++) { + peekValue = (Comparable) range.increment(peekValue); + // handle back to beginning due to modulo incrementing + if (range.compareTo(peekValue, range.from) <= 0) return null; + } + if (range.compareTo(peekValue, range.to) <= 0) { + return peekValue; + } + } else { + final int positiveStep = -step; + Comparable peekValue = value; + for (int i = 0; i < positiveStep; i++) { + peekValue = (Comparable) range.decrement(peekValue); + // handle back to beginning due to modulo decrementing + if (range.compareTo(peekValue, range.to) >= 0) return null; + } + if (range.compareTo(peekValue, range.from) >= 0) { + return peekValue; + } + } + return null; + } + } + + @Override + public List<Comparable> step(int step) { + final IteratorClosureAdapter<Comparable> adapter = new IteratorClosureAdapter<Comparable>(this); + step(step, adapter); + return adapter.asList(); + } + + /** + * Increments by one + * + * @param value the value to increment + * @return the incremented value + */ + protected Object increment(Object value) { + return InvokerHelper.invokeMethod(value, "next", null); + } + + /** + * Decrements by one + * + * @param value the value to decrement + * @return the decremented value + */ + protected Object decrement(Object value) { + return InvokerHelper.invokeMethod(value, "previous", null); + } + + /** + * if operand is a Character or a String with one character, return that character's int value. + */ + private static Comparable normaliseStringType(final Comparable operand) { + if (operand instanceof Character) { + return (int) (Character) operand; + } + if (operand instanceof String) { + final String string = (String) operand; + + if (string.length() == 1) { + return (int) string.charAt(0); + } + return string; + } + return operand; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/ParameterArray.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/ParameterArray.java b/src/main/groovy/groovy/lang/ParameterArray.java new file mode 100644 index 0000000..d3a4163 --- /dev/null +++ b/src/main/groovy/groovy/lang/ParameterArray.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +/** + * Distinguish a parameter array from Object[]. + * + * @author Pilho Kim + */ +public class ParameterArray { + + private final Object parameters; + + public ParameterArray(Object data) { + parameters = packArray(data); + } + + private static Object packArray(Object object) { + if (object instanceof Object[]) + return (Object[]) object; + else + return object; + } + + public Object get() { + return parameters; + } + + public String toString() { + if (parameters == null) + return "<null parameter>"; + return parameters.toString(); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/PropertyAccessInterceptor.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/PropertyAccessInterceptor.java b/src/main/groovy/groovy/lang/PropertyAccessInterceptor.java new file mode 100644 index 0000000..6d2906e --- /dev/null +++ b/src/main/groovy/groovy/lang/PropertyAccessInterceptor.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +/** + * <p>An interface that adds the ability to intercept + * property getters/setters + * + * @author Graeme Rocher + * @since Oct 24, 2005 + */ +public interface PropertyAccessInterceptor extends Interceptor { + + /** + * Intercepts a getXXX call and returns a result. The result is replaced by the + * real value if doGet() return false + * + * @param object The target object + * @param property The property to get + * @return A value supplied by the interceptor + */ + Object beforeGet(Object object, String property); + + /** + * Intercepts a setXXX call + * + * @param object The target object + * @param property The property to set + * @param newValue The new value + */ + void beforeSet(Object object, String property, Object newValue); + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/PropertyValue.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/PropertyValue.java b/src/main/groovy/groovy/lang/PropertyValue.java new file mode 100644 index 0000000..ac77b54 --- /dev/null +++ b/src/main/groovy/groovy/lang/PropertyValue.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +public class PropertyValue { + // the owner of the property + private final Object bean; + + // the description of the property + private final MetaProperty mp; + + public PropertyValue(Object bean, MetaProperty mp) { + this.bean = bean; + this.mp = mp; + } + + public String getName() { + return mp.getName(); + } + + public Class getType() { + return mp.getType(); + } + + public Object getValue() { + return mp.getProperty(bean); + } + + public void setValue(Object value) { + mp.setProperty(bean, value); + } +} + http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/ProxyMetaClass.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/ProxyMetaClass.java b/src/main/groovy/groovy/lang/ProxyMetaClass.java new file mode 100644 index 0000000..8c41f7d --- /dev/null +++ b/src/main/groovy/groovy/lang/ProxyMetaClass.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +/** + * As subclass of MetaClass, ProxyMetaClass manages calls from Groovy Objects to POJOs. + * It enriches MetaClass with the feature of making method invocations interceptable by + * an Interceptor. To this end, it acts as a decorator (decorator pattern) allowing + * to add or withdraw this feature at runtime. + * See groovy/lang/InterceptorTest.groovy for details. + * <p> + * WARNING: This implementation of ProxyMetaClass is NOT thread-safe and hence should only be used for + * as a per-instance MetaClass running in a single thread. Do not place this MetaClass in the MetaClassRegistry + * as it will result in unpredictable behaviour + * + * @author Dierk Koenig + * @author Graeme Rocher + * @see groovy.lang.MetaClassRegistry + */ +public class ProxyMetaClass extends MetaClassImpl implements AdaptingMetaClass { + + protected MetaClass adaptee = null; + protected Interceptor interceptor = null; + + + /** + * convenience factory method for the most usual case. + */ + public static ProxyMetaClass getInstance(Class theClass) { + MetaClassRegistry metaRegistry = GroovySystem.getMetaClassRegistry(); + MetaClass meta = metaRegistry.getMetaClass(theClass); + return new ProxyMetaClass(metaRegistry, theClass, meta); + } + + /** + * @param adaptee the MetaClass to decorate with interceptability + */ + public ProxyMetaClass(MetaClassRegistry registry, Class theClass, MetaClass adaptee) { + super(registry, theClass); + this.adaptee = adaptee; + if (null == adaptee) throw new IllegalArgumentException("adaptee must not be null"); + super.initialize(); + } + + public synchronized void initialize() { + this.adaptee.initialize(); + } + + /** + * Use the ProxyMetaClass for the given Closure. + * Cares for balanced register/unregister. + * + * @param closure piece of code to be executed with registered ProxyMetaClass + */ + public Object use(Closure closure) { + // grab existing meta (usually adaptee but we may have nested use calls) + MetaClass origMetaClass = registry.getMetaClass(theClass); + registry.setMetaClass(theClass, this); + try { + return closure.call(); + } finally { + registry.setMetaClass(theClass, origMetaClass); + } + } + + /** + * Use the ProxyMetaClass for the given Closure. + * Cares for balanced setting/unsetting ProxyMetaClass. + * + * @param closure piece of code to be executed with ProxyMetaClass + */ + public Object use(GroovyObject object, Closure closure) { + // grab existing meta (usually adaptee but we may have nested use calls) + MetaClass origMetaClass = object.getMetaClass(); + object.setMetaClass(this); + try { + return closure.call(); + } finally { + object.setMetaClass(origMetaClass); + } + } + + /** + * @return the interceptor in use or null if no interceptor is used + */ + public Interceptor getInterceptor() { + return interceptor; + } + + /** + * @param interceptor may be null to reset any interception + */ + public void setInterceptor(Interceptor interceptor) { + this.interceptor = interceptor; + } + + /** + * Call invokeMethod on adaptee with logic like in MetaClass unless we have an Interceptor. + * With Interceptor the call is nested in its beforeInvoke and afterInvoke methods. + * The method call is suppressed if Interceptor.doInvoke() returns false. + * See Interceptor for details. + */ + public Object invokeMethod(final Object object, final String methodName, final Object[] arguments) { + return doCall(object, methodName, arguments, interceptor, new Callable() { + public Object call() { + return adaptee.invokeMethod(object, methodName, arguments); + } + }); + } + + /** + * Call invokeMethod on adaptee with logic like in MetaClass unless we have an Interceptor. + * With Interceptor the call is nested in its beforeInvoke and afterInvoke methods. + * The method call is suppressed if Interceptor.doInvoke() returns false. + * See Interceptor for details. + */ + @Override + public Object invokeMethod(final Class sender, final Object object, final String methodName, final Object[] arguments, final boolean isCallToSuper, final boolean fromInsideClass) { + return doCall(object, methodName, arguments, interceptor, new Callable() { + public Object call() { + return adaptee.invokeMethod(sender, object, methodName, arguments, isCallToSuper, fromInsideClass); + } + }); + } + + /** + * Call invokeStaticMethod on adaptee with logic like in MetaClass unless we have an Interceptor. + * With Interceptor the call is nested in its beforeInvoke and afterInvoke methods. + * The method call is suppressed if Interceptor.doInvoke() returns false. + * See Interceptor for details. + */ + public Object invokeStaticMethod(final Object object, final String methodName, final Object[] arguments) { + return doCall(object, methodName, arguments, interceptor, new Callable() { + public Object call() { + return adaptee.invokeStaticMethod(object, methodName, arguments); + } + }); + } + + /** + * Call invokeConstructor on adaptee with logic like in MetaClass unless we have an Interceptor. + * With Interceptor the call is nested in its beforeInvoke and afterInvoke methods. + * The method call is suppressed if Interceptor.doInvoke() returns false. + * See Interceptor for details. + */ + public Object invokeConstructor(final Object[] arguments) { + return doCall(theClass, "ctor", arguments, interceptor, new Callable() { + public Object call() { + return adaptee.invokeConstructor(arguments); + } + }); + } + + /** + * Interceptors the call to getProperty if a PropertyAccessInterceptor is + * available + * + * @param object the object to invoke the getter on + * @param property the property name + * @return the value of the property + */ + public Object getProperty(Class aClass, Object object, String property, boolean b, boolean b1) { + if (null == interceptor) { + return super.getProperty(aClass, object, property, b, b1); + } + if (interceptor instanceof PropertyAccessInterceptor) { + PropertyAccessInterceptor pae = (PropertyAccessInterceptor) interceptor; + + Object result = pae.beforeGet(object, property); + if (interceptor.doInvoke()) { + result = super.getProperty(aClass, object, property, b, b1); + } + return result; + } + return super.getProperty(aClass, object, property, b, b1); + } + + /** + * Interceptors the call to a property setter if a PropertyAccessInterceptor + * is available + * + * @param object The object to invoke the setter on + * @param property The property name to set + * @param newValue The new value of the property + */ + public void setProperty(Class aClass, Object object, String property, Object newValue, boolean b, boolean b1) { + if (null == interceptor) { + super.setProperty(aClass, object, property, newValue, b, b1); + } + if (interceptor instanceof PropertyAccessInterceptor) { + PropertyAccessInterceptor pae = (PropertyAccessInterceptor) interceptor; + + pae.beforeSet(object, property, newValue); + if (interceptor.doInvoke()) { + super.setProperty(aClass, object, property, newValue, b, b1); + } + } else { + super.setProperty(aClass, object, property, newValue, b, b1); + } + } + + public MetaClass getAdaptee() { + return this.adaptee; + } + + public void setAdaptee(MetaClass metaClass) { + this.adaptee = metaClass; + } + + // since Java has no Closures... + private interface Callable { + Object call(); + } + + private Object doCall(Object object, String methodName, Object[] arguments, Interceptor interceptor, Callable howToInvoke) { + if (null == interceptor) { + return howToInvoke.call(); + } + Object result = interceptor.beforeInvoke(object, methodName, arguments); + if (interceptor.doInvoke()) { + result = howToInvoke.call(); + } + result = interceptor.afterInvoke(object, methodName, arguments, result); + return result; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/Range.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/Range.java b/src/main/groovy/groovy/lang/Range.java new file mode 100644 index 0000000..99f30de --- /dev/null +++ b/src/main/groovy/groovy/lang/Range.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +import java.util.List; + +/** + * A Range represents the list of discrete items between some starting (or <code>from</code>) + * value and <em>working up</em> towards some ending (or <code>to</code>) value. + * For a reverse range, the list is obtained by starting at the <code>to</code> value and + * <em>working down</em> towards the <code>from</code> value. + * + * The concept of <em>working up</em> and <em>working down</em> is dependent on the range implementation. + * In the general case, working up involves successive calls to the first item's <code>next()</code> + * method while working down involves calling the <code>previous()</code> method. Optimized + * numerical ranges may apply numeric addition or subtraction of some numerical step size. + * + * Particular range implementations may also support the notion of inclusivity + * and exclusivity with respect to the ending value in the range. + * E.g. <code>1..3 == [1, 2, 3]</code>; but <code>1..<3 == [1, 2]</code>. + * + * In general, the second boundary may not be contained in the range, + * and <code>a..b</code> may produce a different set of elements than <code>(b..a).reversed()</code>. + * E.g. <code>1..2.5 == [1, 2]</code>; but <code>2.5..1 == [2.5, 1.5]</code>. + * + * Implementations can be memory efficient by storing just the <code>from</code> and <code>to</code> boundary + * values rather than eagerly creating all discrete items in the conceptual list. The actual discrete items + * can be lazily calculated on an as needed basis (e.g. when calling methods from the <code>java.util.List</code> + * interface or the additional <code>step</code> methods in the <code>Range</code> interface). + * + * In addition to the methods related to a Range's "discrete items" abstraction, there is a method, + * <code>containsWithinBounds</code> which, for numerical ranges, allows checking within the continuous + * interval between the Range's boundary values. + */ +public interface Range<T extends Comparable> extends List<T> { + /** + * The lower value in the range. + * + * @return the lower value in the range. + */ + T getFrom(); + + /** + * The upper value in the range. + * + * @return the upper value in the range + */ + T getTo(); + + /** + * Indicates whether this is a reverse range which iterates backwards + * starting from the to value and ending on the from value + * + * @return <code>true</code> if this is a reverse range + */ + boolean isReverse(); + + /** + * Indicates whether an object is greater than or equal to the <code>from</code> + * value for the range and less than or equal to the <code>to</code> value. + * <p> + * This may be true even for values not contained in the range. + * + * Example: from = 1.5, to = 3, next() increments by 1 + * containsWithinBounds(2) == true + * contains(2) == false + * + * @param o the object to check against the boundaries of the range + * @return <code>true</code> if the object is between the from and to values + */ + boolean containsWithinBounds(Object o); + + /** + * Steps through the range, calling a closure for each item. + * + * @param step the amount by which to step. If negative, steps through the range backwards. + * @param closure the {@link Closure} to call + */ + void step(int step, Closure closure); + + /** + * Forms a list by stepping through the range by the indicated interval. + * + * @param step the amount by which to step. If negative, steps through the range backwards. + * @return the list formed by stepping through the range by the indicated interval. + */ + List<T> step(int step); + + /** + * @return the verbose {@link String} representation of this {@link Range} as would be typed into a console to create the {@link Range} instance + */ + String inspect(); +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/ReadOnlyPropertyException.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/ReadOnlyPropertyException.java b/src/main/groovy/groovy/lang/ReadOnlyPropertyException.java new file mode 100644 index 0000000..342a182 --- /dev/null +++ b/src/main/groovy/groovy/lang/ReadOnlyPropertyException.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + + +/** + * This exception is thrown if an attempt is made to set a read only property + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public class ReadOnlyPropertyException extends MissingPropertyException { + + public ReadOnlyPropertyException(final String property, final Class type) { + super("Cannot set readonly property: " + property + " for class: " + type.getName(), property, type); + } + + public ReadOnlyPropertyException(final String property, final String classname) { + super("Cannot set readonly property: " + property + " for class: " + classname); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/Reference.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/Reference.java b/src/main/groovy/groovy/lang/Reference.java new file mode 100644 index 0000000..c4bf21d --- /dev/null +++ b/src/main/groovy/groovy/lang/Reference.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +import org.codehaus.groovy.runtime.InvokerHelper; + +import java.io.Serializable; + +/** + * Represents a reference to a value + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public class Reference<T> extends GroovyObjectSupport implements Serializable { + + private static final long serialVersionUID = 4963704631487573488L; + private T value; + + public Reference() { + } + + public Reference(T value) { + this.value = value; + } + + public Object getProperty(String property) { + Object value = get(); + if (value != null) { + return InvokerHelper.getProperty(value, property); + } + return super.getProperty(property); + } + + public void setProperty(String property, Object newValue) { + Object value = get(); + if (value != null) { + InvokerHelper.setProperty(value, property, newValue); + } + else { + super.setProperty(property, newValue); + } + } + + public Object invokeMethod(String name, Object args) { + Object value = get(); + if (value != null) { + try { + return InvokerHelper.invokeMethod(value, name, args); + } + catch (Exception e) { + return super.invokeMethod(name, args); + } + } + else { + return super.invokeMethod(name, args); + } + } + + public T get() { + return value; + } + + public void set(T value) { + this.value = value; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/Script.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/Script.java b/src/main/groovy/groovy/lang/Script.java new file mode 100644 index 0000000..196c74c --- /dev/null +++ b/src/main/groovy/groovy/lang/Script.java @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.runtime.InvokerHelper; + +import java.io.File; +import java.io.IOException; + +/** + * This object represents a Groovy script + * + * @author <a href="mailto:[email protected]">James Strachan</a> + * @author Guillaume Laforge + */ +public abstract class Script extends GroovyObjectSupport { + private Binding binding; + + protected Script() { + this(new Binding()); + } + + protected Script(Binding binding) { + this.binding = binding; + } + + public Binding getBinding() { + return binding; + } + + public void setBinding(Binding binding) { + this.binding = binding; + } + + public Object getProperty(String property) { + try { + return binding.getVariable(property); + } catch (MissingPropertyException e) { + return super.getProperty(property); + } + } + + public void setProperty(String property, Object newValue) { + if ("binding".equals(property)) + setBinding((Binding) newValue); + else if("metaClass".equals(property)) + setMetaClass((MetaClass)newValue); + else + binding.setVariable(property, newValue); + } + + /** + * Invoke a method (or closure in the binding) defined. + * + * @param name method to call + * @param args arguments to pass to the method + * @return value + */ + public Object invokeMethod(String name, Object args) { + try { + return super.invokeMethod(name, args); + } + // if the method was not found in the current scope (the script's methods) + // let's try to see if there's a method closure with the same name in the binding + catch (MissingMethodException mme) { + try { + if (name.equals(mme.getMethod())) { + Object boundClosure = getProperty(name); + if (boundClosure != null && boundClosure instanceof Closure) { + return ((Closure) boundClosure).call((Object[])args); + } else { + throw mme; + } + } else { + throw mme; + } + } catch (MissingPropertyException mpe) { + throw mme; + } + } + } + + /** + * The main instance method of a script which has variables in scope + * as defined by the current {@link Binding} instance. + */ + public abstract Object run(); + + // println helper methods + + /** + * Prints a newline to the current 'out' variable which should be a PrintWriter + * or at least have a println() method defined on it. + * If there is no 'out' property then print to standard out. + */ + public void println() { + Object object; + + try { + object = getProperty("out"); + } catch (MissingPropertyException e) { + System.out.println(); + return; + } + + InvokerHelper.invokeMethod(object, "println", ArgumentListExpression.EMPTY_ARRAY); + } + + /** + * Prints the value to the current 'out' variable which should be a PrintWriter + * or at least have a print() method defined on it. + * If there is no 'out' property then print to standard out. + */ + public void print(Object value) { + Object object; + + try { + object = getProperty("out"); + } catch (MissingPropertyException e) { + DefaultGroovyMethods.print(System.out,value); + return; + } + + InvokerHelper.invokeMethod(object, "print", new Object[]{value}); + } + + /** + * Prints the value and a newline to the current 'out' variable which should be a PrintWriter + * or at least have a println() method defined on it. + * If there is no 'out' property then print to standard out. + */ + public void println(Object value) { + Object object; + + try { + object = getProperty("out"); + } catch (MissingPropertyException e) { + DefaultGroovyMethods.println(System.out,value); + return; + } + + InvokerHelper.invokeMethod(object, "println", new Object[]{value}); + } + + /** + * Prints a formatted string using the specified format string and argument. + * + * @param format the format to follow + * @param value the value to be formatted + */ + public void printf(String format, Object value) { + Object object; + + try { + object = getProperty("out"); + } catch (MissingPropertyException e) { + DefaultGroovyMethods.printf(System.out, format, value); + return; + } + + InvokerHelper.invokeMethod(object, "printf", new Object[] { format, value }); + } + + /** + * Prints a formatted string using the specified format string and arguments. + * + * @param format the format to follow + * @param values an array of values to be formatted + */ + public void printf(String format, Object[] values) { + Object object; + + try { + object = getProperty("out"); + } catch (MissingPropertyException e) { + DefaultGroovyMethods.printf(System.out, format, values); + return; + } + + InvokerHelper.invokeMethod(object, "printf", new Object[] { format, values }); + } + + /** + * A helper method to allow the dynamic evaluation of groovy expressions using this + * scripts binding as the variable scope + * + * @param expression is the Groovy script expression to evaluate + */ + public Object evaluate(String expression) throws CompilationFailedException { + GroovyShell shell = new GroovyShell(getClass().getClassLoader(), binding); + return shell.evaluate(expression); + } + + /** + * A helper method to allow the dynamic evaluation of groovy expressions using this + * scripts binding as the variable scope + * + * @param file is the Groovy script to evaluate + */ + public Object evaluate(File file) throws CompilationFailedException, IOException { + GroovyShell shell = new GroovyShell(getClass().getClassLoader(), binding); + return shell.evaluate(file); + } + + /** + * A helper method to allow scripts to be run taking command line arguments + */ + public void run(File file, String[] arguments) throws CompilationFailedException, IOException { + GroovyShell shell = new GroovyShell(getClass().getClassLoader(), binding); + shell.run(file, arguments); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/Sequence.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/Sequence.java b/src/main/groovy/groovy/lang/Sequence.java new file mode 100644 index 0000000..2b4316c --- /dev/null +++ b/src/main/groovy/groovy/lang/Sequence.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * Represents a sequence of objects which represents zero or many instances of + * of objects of a given type. The type can be omitted in which case any type of + * object can be added. + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public class Sequence extends ArrayList implements GroovyObject { + + private MetaClass metaClass = InvokerHelper.getMetaClass(getClass()); + private final Class type; + private int hashCode; + + public Sequence() { + this(null); + } + + public Sequence(Class type) { + this.type = type; + } + + public Sequence(Class type, List content) { + super(content.size()); + this.type = type; + addAll(content); + } + + /** + * Sets the contents of this sequence to that + * of the given collection. + */ + public void set(Collection collection) { + checkCollectionType(collection); + clear(); + addAll(collection); + } + + public boolean equals(Object that) { + if (that instanceof Sequence) { + return equals((Sequence) that); + } + return false; + } + + public boolean equals(Sequence that) { + if (size() == that.size()) { + for (int i = 0; i < size(); i++) { + if (!DefaultTypeTransformation.compareEqual(this.get(i), that.get(i))) { + return false; + } + } + return true; + } + return false; + } + + public int hashCode() { + if (hashCode == 0) { + for (int i = 0; i < size(); i++) { + Object value = get(i); + int hash = (value != null) ? value.hashCode() : 0xbabe; + hashCode ^= hash; + } + if (hashCode == 0) { + hashCode = 0xbabe; + } + } + return hashCode; + } + + public int minimumSize() { + return 0; + } + + /** + * @return the type of the elements in the sequence or null if there is no + * type constraint on this sequence + */ + public Class type() { + return type; + } + + public void add(int index, Object element) { + checkType(element); + hashCode = 0; + super.add(index, element); + } + + public boolean add(Object element) { + checkType(element); + hashCode = 0; + return super.add(element); + } + + public boolean addAll(Collection c) { + checkCollectionType(c); + hashCode = 0; + return super.addAll(c); + } + + public boolean addAll(int index, Collection c) { + checkCollectionType(c); + hashCode = 0; + return super.addAll(index, c); + } + + public void clear() { + hashCode = 0; + super.clear(); + } + + public Object remove(int index) { + hashCode = 0; + return super.remove(index); + } + + protected void removeRange(int fromIndex, int toIndex) { + hashCode = 0; + super.removeRange(fromIndex, toIndex); + } + + public Object set(int index, Object element) { + hashCode = 0; + return super.set(index, element); + } + + // GroovyObject interface + //------------------------------------------------------------------------- + public Object invokeMethod(String name, Object args) { + try { + return getMetaClass().invokeMethod(this, name, args); + } + catch (MissingMethodException e) { + // lets apply the method to each item in the collection + List answer = new ArrayList(size()); + for (Iterator iter = iterator(); iter.hasNext(); ) { + Object element = iter.next(); + Object value = InvokerHelper.invokeMethod(element, name, args); + answer.add(value); + } + return answer; + } + } + + public Object getProperty(String property) { + return getMetaClass().getProperty(this, property); + } + + public void setProperty(String property, Object newValue) { + getMetaClass().setProperty(this, property, newValue); + } + + public MetaClass getMetaClass() { + return metaClass; + } + + public void setMetaClass(MetaClass metaClass) { + this.metaClass = metaClass; + } + + // Implementation methods + //------------------------------------------------------------------------- + + /** + * Checks that each member of the given collection are of the correct + * type + */ + protected void checkCollectionType(Collection c) { + if (type != null) { + for (Iterator iter = c.iterator(); iter.hasNext(); ) { + Object element = iter.next(); + checkType(element); + } + } + } + + + /** + * Checks that the given object instance is of the correct type + * otherwise a runtime exception is thrown + */ + protected void checkType(Object object) { + if (object == null) { + throw new NullPointerException("Sequences cannot contain null, use a List instead"); + } + if (type != null) { + if (!type.isInstance(object)) { + throw new IllegalArgumentException( + "Invalid type of argument for sequence of type: " + + type.getName() + + " cannot add object: " + + object); + } + } + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/Singleton.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/Singleton.java b/src/main/groovy/groovy/lang/Singleton.java new file mode 100644 index 0000000..9b86c8b --- /dev/null +++ b/src/main/groovy/groovy/lang/Singleton.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.lang; + +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Class annotation to make a singleton class. The singleton is obtained through normal property access using the singleton property (defaults to "instance"). + * + * Such classes can be initialized during normal static initialization of the class or lazily (on first access). + * To make the singleton lazy use {@code @Singleton(lazy=true)}. + * Lazy singletons are implemented with double-checked locking and a volatile backing field. + * By default, no explicit constructors are allowed. To create one or more explicit constructors + * use {@code @Singleton(strict=false)}. + * This could be used to: + * <ul> + * <li>provide your own custom initialization logic in your own no-arg constructor - you + * will be responsible for the entire code (the {@code @Singleton} annotation becomes merely documentation)</li> + * <li>provide one or more constructors with arguments for a quasi-singleton - these constructors will be used + * to create instances that are independent of the singleton instance returned by the singleton property</li> + * </ul> + * + * @author Alex Tkachman + * @author Paul King + */ [email protected] +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.SingletonASTTransformation") +public @interface Singleton { + /** + * @return if this singleton should be lazy + */ + boolean lazy() default false; + + /** + * @return if this singleton should have strict semantics + */ + boolean strict() default true; + + /** + * @return the singleton property name + */ + String property() default "instance"; +}
