Author: desruisseaux Date: Fri Feb 6 21:48:57 2015 New Revision: 1657973 URL: http://svn.apache.org/r1657973 Log: Ported OperationMethodSet, to be needed by DefaultMathTransformFactory. Added tests (this is new code).
Added: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/OperationMethodSet.java (with props) sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/OperationMethodSetTest.java (with props) Modified: sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java Added: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/OperationMethodSet.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/OperationMethodSet.java?rev=1657973&view=auto ============================================================================== --- sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/OperationMethodSet.java (added) +++ sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/OperationMethodSet.java [UTF-8] Fri Feb 6 21:48:57 2015 @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.referencing.operation.transform; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.ServiceLoader; +import org.opengis.referencing.operation.SingleOperation; +import org.opengis.referencing.operation.OperationMethod; +import org.apache.sis.referencing.operation.DefaultOperationMethod; + + +/** + * An immutable and thread-safe set containing the operation methods given by an {@link Iterable}. + * Initial iteration is synchronized on the given {@code Iterable} and the result is cached. + * + * {@section Rational} + * We use this class instead than copying the {@link OperationMethod} instances in a {@link java.util.HashSet} + * in order to allow deferred {@code OperationMethod} instantiation, for example in the usual case where the + * iterable is a {@link java.util.ServiceLoader}: we do not invoke {@link Iterator#next()} before needed. + * + * {@section Limitations} + * The usual {@link Set} methods like {@code contains(Object)} are inefficient as they may require a traversal + * of all elements in this set. + * + * @author Martin Desruisseaux (Geomatys) + * @since 0.6 + * @version 0.6 + * @module + */ +final class OperationMethodSet extends AbstractSet<OperationMethod> { + /** + * The operation type we are looking for. + */ + private final Class<? extends SingleOperation> type; + + /** + * The {@link DefaultMathTransformFactory#methods} used for fetching the initial methods. + * We need this reference for locking purpose. + */ + private final Iterable<? extends OperationMethod> methods; + + /** + * Iterator over {@link #methods} elements. All usage of this iterator must be synchronized on {@link #methods}. + * Will be set to {@code null} when the iteration is over. + */ + private Iterator<? extends OperationMethod> methodIterator; + + /** + * The methods returned by the first iteration. + */ + private final List<OperationMethod> cachedMethods; + + /** + * Constructs a set wrapping the given iterable. + * The caller musts holds the lock on {@code methods} when invoking this constructor. + * + * @param type The type of coordinate operation for which to retain methods. + * @param methods The {@link DefaultMathTransformFactory#methods} used for fetching the initial methods. + */ + OperationMethodSet(final Class<? extends SingleOperation> type, + final Iterable<? extends OperationMethod> methods) + { + this.type = type; + this.methods = methods; + cachedMethods = new ArrayList<>(); + reset(); + } + + /** + * Invoked on construction time, or when the service loader has been reloaded. + * The caller musts holds the lock on {@code methods} when invoking this method. + */ + final synchronized void reset() { + assert Thread.holdsLock(methods); + cachedMethods.clear(); + methodIterator = methods.iterator(); + if (!methodIterator.hasNext()) { + methodIterator = null; + } + } + + /** + * Transfers the next element from {@link #methodIterator} to {@link #cachedMethods}. + * + * @return {@code true} if the transfer has been done, or {@code false} if the next + * method has been skipped because its operation type is not the expected one + * or because the element has already been added in a previous transfer. + */ + private boolean transfer() { + assert Thread.holdsLock(this); + final OperationMethod method; + synchronized (methods) { + method = methodIterator.next(); + if (!methodIterator.hasNext()) { + methodIterator = null; + } + } + if (method instanceof DefaultOperationMethod) { + if (!type.isAssignableFrom(((DefaultOperationMethod) method).getOperationType())) { + return false; + } + } + /* + * ServiceLoader guarantees that the iteration does not contain duplicated elements. + * The Set contract provides similar guarantee. For other types (which should be very + * uncommon), we check for duplicated elements as a safety. + * + * Note that in the vast majority of cases, 'methods' is an instance of ServiceLoader + * and its "instanceof" check should be very fast since ServiceLoader is a final class. + */ + if (!(methods instanceof ServiceLoader || methods instanceof Set<?>)) { + if (cachedMethods.contains(method)) { + return false; + } + } + return cachedMethods.add(method); + } + + /** + * Returns {@code true} if this set is empty. + */ + @Override + public synchronized boolean isEmpty() { + if (!cachedMethods.isEmpty()) { + return false; + } + while (methodIterator != null) { + if (transfer()) return false; + } + return true; + } + + /** + * Returns the number of elements in this set. + */ + @Override + public synchronized int size() { + while (methodIterator != null) { + transfer(); + } + return cachedMethods.size(); + } + + /** + * Returns {@code true} if {@link #next(int)} can return an operation method at the given index. + */ + final synchronized boolean hasNext(final int index) { + if (index >= cachedMethods.size()) { + do if (methodIterator == null) { + return false; + } while (!transfer()); + } + return true; + } + + /** + * Returns the operation method at the given index. In case of index out of bounds, this method throws a + * {@link NoSuchElementException} instead than an {@link IndexOutOfBoundsException} because this method + * is designed for being invoked by {@link Iter#next()}. + */ + final synchronized OperationMethod next(final int index) { + if (index >= cachedMethods.size()) { + do if (methodIterator == null) { + throw new NoSuchElementException(); + } while (!transfer()); + } + return cachedMethods.get(index); + } + + /** + * Returns an iterator over the elements contained in this set. + */ + @Override + public Iterator<OperationMethod> iterator() { + return new Iterator<OperationMethod>() { + /** Index of the next element to be returned. */ + private int cursor; + + @Override + public boolean hasNext() { + return OperationMethodSet.this.hasNext(cursor); + } + + @Override + public OperationMethod next() { + return OperationMethodSet.this.next(cursor++); + } + }; + } +} Propchange: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/OperationMethodSet.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/OperationMethodSet.java ------------------------------------------------------------------------------ svn:mime-type = text/plain;charset=UTF-8 Added: sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/OperationMethodSetTest.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/OperationMethodSetTest.java?rev=1657973&view=auto ============================================================================== --- sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/OperationMethodSetTest.java (added) +++ sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/OperationMethodSetTest.java [UTF-8] Fri Feb 6 21:48:57 2015 @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.referencing.operation.transform; + +import java.util.Map; +import java.util.Iterator; +import java.util.Collections; +import java.util.NoSuchElementException; +import org.opengis.parameter.ParameterDescriptorGroup; +import org.opengis.referencing.operation.Projection; +import org.opengis.referencing.operation.ConicProjection; +import org.opengis.referencing.operation.PlanarProjection; +import org.opengis.referencing.operation.CylindricalProjection; +import org.opengis.referencing.operation.OperationMethod; +import org.apache.sis.referencing.operation.DefaultOperationMethod; +import org.apache.sis.parameter.DefaultParameterDescriptorGroup; +import org.apache.sis.internal.util.UnmodifiableArrayList; +import org.apache.sis.test.DependsOnMethod; +import org.apache.sis.test.DependsOn; +import org.apache.sis.test.TestCase; +import org.junit.Test; + +import static org.junit.Assert.*; + + +/** + * Tests {@link OperationMethodSet}. + * + * @author Martin Desruisseaux (Geomatys) + * @since 0.6 + * @version 0.6 + * @module + */ +@DependsOn({ + org.apache.sis.referencing.operation.DefaultOperationMethodTest.class, +}) +public final strictfp class OperationMethodSetTest extends TestCase { + /** + * Creates a new two-dimensional operation method for an operation of the given name. + * + * @param type The value to be returned by {@link DefaultOperationMethod#getOperationType()}. + * @param method The operation name (example: "Mercator (variant A)"). + * @return The operation method. + */ + @SuppressWarnings("serial") + private static DefaultOperationMethod createMethod(final Class<? extends Projection> type, final String method) { + Map<String,?> properties = Collections.singletonMap(DefaultOperationMethod.NAME_KEY, method); + final ParameterDescriptorGroup parameters = new DefaultParameterDescriptorGroup(properties, 1, 1); + /* + * Recycle the ParameterDescriptorGroup name for DefaultOperationMethod. + * This save us one object creation, and is often the same name anyway. + */ + properties = Collections.singletonMap(DefaultOperationMethod.NAME_KEY, parameters.getName()); + return new DefaultOperationMethod(properties, 2, 2, parameters) { + @Override public Class<? extends Projection> getOperationType() { + return type; + } + }; + } + + /** + * Creates an {@code OperationMethodSet} from the given methods, using an iterable + * which will guarantee that the iteration is performed at most once. + * + * @param type The type of coordinate operation for which to retain methods. + * @param methods The {@link DefaultMathTransformFactory#methods} used for fetching the initial methods. + */ + private static OperationMethodSet create(final Class<? extends Projection> type, final DefaultOperationMethod... methods) { + @SuppressWarnings("serial") + final Iterable<DefaultOperationMethod> asList = new UnmodifiableArrayList<DefaultOperationMethod>(methods) { + private boolean isIterationDone; + + @Override + public Iterator<DefaultOperationMethod> iterator() { + assertFalse("Expected no more than one iteration.", isIterationDone); + isIterationDone = true; + return super.iterator(); + } + }; + synchronized (asList) { // Needed for avoiding assertion error in OperationMethodSet. + return new OperationMethodSet(type, asList); + } + } + + /** + * Tests construction from an empty list. + */ + @Test + public void testEmpty() { + assertEmpty(create(Projection.class)); + } + + /** + * Asserts that the given {@link OperationMethodSet} is empty. + */ + private static void assertEmpty(final OperationMethodSet set) { + assertTrue ("isEmpty", set.isEmpty()); + assertEquals("size", 0, set.size()); + final Iterator<OperationMethod> iterator = set.iterator(); + assertFalse(iterator.hasNext()); + try { + iterator.next(); + fail("Expected NoSuchElementException"); + } catch (NoSuchElementException e) { + // This is the expected exception. + } + } + + /** + * Tests a non-empty set. + */ + @Test + @DependsOnMethod("testEmpty") + public void testMixedCases() { + final DefaultOperationMethod merA = createMethod(CylindricalProjection.class, "Mercator (variant A)"); + final DefaultOperationMethod merB = createMethod(CylindricalProjection.class, "Mercator (variant B)"); + final DefaultOperationMethod merC = createMethod(CylindricalProjection.class, "Mercator (variant C)"); + final DefaultOperationMethod dup = createMethod(CylindricalProjection.class, "Mercator (variant B)"); + final DefaultOperationMethod lamb = createMethod(ConicProjection.class, "Lambert"); + final DefaultOperationMethod[] methods = new DefaultOperationMethod[] {merA, merB, merC, dup, lamb}; + final OperationMethodSet mercators = create(CylindricalProjection.class, methods); + final OperationMethodSet lambert = create( ConicProjection.class, methods); + final OperationMethodSet all = create( Projection.class, methods); + /* + * Mercator case. + * - Intentionally start the iteration without checking 'hasNext()' - the iterator shall be robust to that. + * - Intentionally start an other iteration (indirectly) in the middle of the first one. + */ + final Iterator<OperationMethod> iterator = mercators.iterator(); + assertSame(merA, iterator.next()); + assertSame(merB, iterator.next()); + assertArrayEquals("toArray", new DefaultOperationMethod[] {merA, merB, merC}, mercators.toArray()); + assertSame(merC, iterator.next()); + assertFalse (iterator.hasNext()); + assertFalse ("isEmpty", mercators.isEmpty()); + assertEquals("size", 3, mercators.size()); + /* + * Lambert case. Test twice since the two excecutions will take different code paths. + */ + assertEquals(Collections.singleton(lamb), lambert); + assertEquals(Collections.singleton(lamb), lambert); + /* + * Test filtering: the test should not contain any conic projection. + */ + assertEmpty(create(PlanarProjection.class, methods)); + /* + * Opportunist tests. + */ + assertFalse(lambert.containsAll(all)); + assertTrue(all.containsAll(lambert)); + assertTrue(all.containsAll(mercators)); + } +} Propchange: sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/OperationMethodSetTest.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/OperationMethodSetTest.java ------------------------------------------------------------------------------ svn:mime-type = text/plain;charset=UTF-8 Modified: sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java?rev=1657973&r1=1657972&r2=1657973&view=diff ============================================================================== --- sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java [UTF-8] (original) +++ sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java [UTF-8] Fri Feb 6 21:48:57 2015 @@ -26,7 +26,7 @@ import org.junit.BeforeClass; * * @author Martin Desruisseaux (Geomatys) * @since 0.3 - * @version 0.5 + * @version 0.6 * @module */ @Suite.SuiteClasses({ @@ -79,6 +79,7 @@ import org.junit.BeforeClass; org.apache.sis.referencing.operation.DefaultFormulaTest.class, org.apache.sis.referencing.operation.DefaultOperationMethodTest.class, + org.apache.sis.referencing.operation.transform.OperationMethodSetTest.class, org.apache.sis.internal.referencing.OperationMethodsTest.class, org.apache.sis.referencing.datum.BursaWolfParametersTest.class,