Skip to content

GSIP 69 API Proposal

Jody Garnett edited this page Jul 12, 2017 · 1 revision

GSIP 69 - API Proposal

Summary

In essence, the proposal lays down to a two method addition to the Catalog interface: one to obtain the count and another one to obtain a stream of Catalog configuration objects, both allowing to specify a filtering criteria through an OGC Filter predicate, as well as the ability to do paging and sorting.

  • Iterable or Iterator : Use java.util.Iterator instead of java.lang.Iterable as query return type. It is better suited to represent a single stream of contents and helps keep the API changes to a minimum.

CloseableIterator

interface CloseableIterator<T> extends Iterator<T>, Closeable {
    @Override
    public void close();
}
  • Full Text Search : Use a special property named AnyText to indicate a query predicate is to be performed against all text properties of a given object type. The concept and approach is taken from the OGC CSW specification.

  • OGC Filter : leverage the well known GeoTools OGC Filter constructs as GeoServer catalog subsystem’s query model.

  • Predicates utility factory methods : Static factory methods utility to build well known types of Filter instances, that Catalog backends can easily identify and translate to their native query language, and provide a convenient way of creating the most common filters through static imports.

    package org.geoserver.catalog; public class Predicates {

      public static Filter acceptAll() {...}
    
      public static Filter acceptNone() {...}
    
      public static Filter equal(String property, Object expected) {...}
    
      public static Filter contains(String property, String subsequence) {...}
    
      public static Filter and(Filter... operands) {...}
    
      public static Filter or(Filter... operands) {...}
    
      public static Filter fullTextSearch(final String subsequence) {...}
    
      public static Filter isNull(String propertyName) {...}
    
      public static SortBy sortBy(String propertyName, boolean ascending) {...}
    

    }

  • Catalog Extensions : augment the Catalog interface with the following four methods, two of which are pure convenience ones, to enable counting the number, and obtaining a stream of Catalog objects for a given query predicate, and to enable paged queries.

    public interface Catalog {

      .... previous methods ...
    
      public <T extends CatalogInfo> int count(
                  Class<T> of, Filter filter);
    
      /** Convenience method to retrieve a single object, provided the filter is known to
          return at most one object **/
      public <T extends CatalogInfo> T get(
                  Class<T> type, Filter filter)
                  throws IllegalArgumentException;
    
      public <T extends CatalogInfo> CloseableIterator<T> list(
                   Class<T> of, Filter filter);
    
      public <T extends CatalogInfo> CloseableIterator<T> list(
                   Class<T> of, 
                   Filter filter, 
                   @Nullable Integer offset, 
                   @Nullable Integer count,
                   @Nullable SortBy sortOrder);
    

    }

  • CatalogFacade Extensions : augment the CatalogFacade interface with three methods in order to cope up with the three main interface use cases in a generic way: a) get a single object, b) get a filtered and possibly paged list of objects, and c) compute the number of objects that satisfy a query criteria.

    public interface CatalogFacade { ....

      public <T extends CatalogInfo> int count(Class<T> of, Filter filter);
    
      public boolean canSort(Class<? extends CatalogInfo> type, String propertyName);
    
      public <T extends CatalogInfo> CloseableIterator<T> list(final Class<T> of,
              final Filter filter, @Nullable Integer offset, @Nullable Integer count,
              @Nullable SortBy sortOrder);
    

    }

.

Proposed API additions

Iterator or Iterable

In order to gracefully scale to hundreds of thousands of catalog resources, we need to facilitate streaming. Both the java.util.Iterator and java.lang.Iterable interfaces are well suited to streaming idioms at the code level. Although it is practical to use the Java enhanced for loop with Iterable objects, the extra burden in using Iterator.hasNext () and Iterator.next () is very small. We’ll chose to return an Iterator for the Catalog query results, since it better represents a single stream of contents, and it’ll make things easier to work with and implement due to:

  • The Iterable interface represents an unbounded collection of objects to which an Iterator over its contents can be requested multiple times. As we’ll be returning the results of a specific query, returning Iterable would mean an extra burden for implementors if results are to be streamed, as somehow the returned Iterable should contain state to perform the originating query every time an Iterator is requested. Iterable also assumes it is cheap to request a new Iterator, while keeping our query return type as Iterator makes the API more clear in that you’re performing a potentially expensive query and that it could fail.
  • As we’ll use a specific subclass of Iterator - as we’ll see in the next section - returning Iterable would also mean we need to return an extra specialization of the Iterable with it’s iterator () method type narrowed, adding unnecessary cluttering to the API.

CloseableIterator

Streaming comes at a cost: Catalog backends (i.e. CatalogFacade implementations} might need to make some concessions to provide such a service, like keeping some sort of connection open to the actual storage system for the life time of the returned Iterator. Hence the introduction of the CloseableIterator extends Closeable, Iterator interface serves (at least until Java 7 and its AutoCloseable wonders are adopted as a minimum platform requirement) as a measure for the Catalog client code to be conscious those those resources may need to be closed.

package org.geoserver.platform;

import java.io.Closeable;
import java.util.Iterator;

public interface CloseableIterator<T> extends Iterator<T>, Closeable {

    /**
     * Closes this stream and releases any system resources associated with it.
     * This method is idempotent, if the stream is already closed then invoking
     * this method has no effect.
     * 
     * @throws RuntimeException
     *             if an I/O error occurs
     */
    @Override
    public void close();

}

Full Text Search

I found out that it would be very convenient if the API supported also some sort of “full text search” over catalog resources. The most direct example is the UI search boxes. By one side, one would like the search to hit more object metadata than the attributes presented on the list of layers, for example. So that the search is also performed on attributes such as title, abstract, SRS, etc. Another benefit of full text search support may come at the implementation side of the fence. The current approach at searching on the UI is so that the same criteria is applied to a short handful of attributes as an OR filter (e.g. name like ‘%some text%’ OR resource.store.workspace.name like ‘%some text%’ OR resource.store.name like ‘%some text%’), but it’s likely that an implementation can make a more efficient search with a shorter filter such as AnyText like ‘%some text%’. Actually, that’s the approach (Filter API wise) adopted by the CSW specification: to use a special property name, named AnyText, that’s not part of the object model, but serves as a placeholder for an OGC Filter to express that the criteria matches any textual property on the queried objects.

OGC Filter as predicate object model

The GeoTools OGC Filter interfaces and implementation are general purpose enough and so widely adopted that provide for an (almost) perfect fit to our querying criteria needs. A handful of utility classes that work with them are already in place, such as filter simplifiers, encoders to SQL, splitters into back-end’s supported and unsupported bits, and so on.

The one thing where Filter falls a bit short is in support for run-time defined criteria, such as through anonymous inner classes, for the cases where a needed criteria is too constrained to code logic that it can’t be expressed through the pre-existing and closed set of Filter interfaces, nor any of the available functions.

Now, as org.opengis.filter.expression.Function is the only “extension point” provided by the API, and creating a special function that registers itself through the SPI lookup mechanism would not only be a bit overkill, but would also bloat the list of available functions with domain specific ones, whilst they’re supposed to be generic, we’ve proposed a new Function subinterface to the GeoTools Filter API, namely InternalFunction, whose purpose is to instruct collaborators (such as the various types of visitors), that such a function deviates from the usual SPI lookup mechanism, and hence an attempt to create it through FilterFactory would fail.

Custom PropertyAccessor

A custom PropertyAccessor is in place, specific to org.geoserver.catalog.\*Info objects, so that we can use the desired property name syntax at runtime and it works with both POJO properties as well as MetadataMap entries, and resolves nested collection properties properly.

package org.geoserver.catalog.impl;
/**
 * Property accessor for GeoServer {@link Info} configuration objects.
 * 
 */
public class CatalogPropertyAccessorFactory implements PropertyAccessorFactory {

    private static final CatalogPropertyAccessor INSTANCE = new CatalogPropertyAccessor();

    @Override
    public PropertyAccessor createPropertyAccessor(Class<?> type, String xpath, Class<?> target,
            Hints hints) {
        if (Info.class.isAssignableFrom(type)) {
            return INSTANCE;
        }
        return null;
    }
}

/**
 * Extracts a property from a {@link Info} object.
 * <p>
 * The property can be nested (p1.p2.p3), indexed (p1[3]), collection (colProp),
 * or a combination (colProp1.nonColProp.colProp2[1]).
 * <p>
 * In the later case, indicates {@code colProp1} is a collection property and a
 * list of all the id values from all the objects in the p1 property shall be
 * returned.
 */
public class CatalogPropertyAccessor implements PropertyAccessor {

    @Override
    public boolean canHandle(Object object, String xpath, Class<?> target) {
        return object instanceof Info;
    }

    /**
     * Not supported.
     */
    @Override
    public <T> void set(Object object, String xpath, T value, Class<T> target)
            throws IllegalArgumentException {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T get(Object object, String xpath, Class<T> target)
            throws IllegalArgumentException {
        Object value = getProperty((Info) object, xpath);
        T result;
        if (null != target && null != value) {
            result = Converters.convert(value, target);
        } else {
            result = (T) value;
        }
        return result;
    }

    /**
     * @param input the object to extract the (possibly nested,indexed, or
     *        collection) property from
     * @param propertyName the property to extract from {@code input}
     * @return the evaluated value of the given property, or {@code null} if a
     *         prior nested property in the path is null;
     * @throws IllegalArgumentException if no such property exists for the given
     *         object
     */
    public Object getProperty(final Object input, final String propertyName)
            throws IllegalArgumentException {

        if (input instanceof Info
                && Predicates.ANY_TEXT.getPropertyName().equals(propertyName)) {
            return getAnyText((Info) input);
        }
        String[] propertyNames = propertyName.split("\\.");
        return getProperty(input, propertyNames, 0);
    }

    /**
     * @param input
     * @return
     */
    @SuppressWarnings("unchecked")
    private List<String> getAnyText(final Info input) {

        final Set<String> propNames = fullTextProperties(input);
        List<String> textProps = new ArrayList<String>(propNames.size());
        for (String propName : propNames) {
            Object property = getProperty(input, propName);
            if (property instanceof Collection) {
                textProps.addAll(((Collection<String>) property));
            } else if (property != null) {
                textProps.add(String.valueOf(property));
            }
        }
        return textProps;
    }

    public Object getProperty(final Object input, final String[] propertyNames,
            final int offset) throws IllegalArgumentException {

        if (offset < 0 || offset > propertyNames.length) {
            throw new ArrayIndexOutOfBoundsException("offset: " + offset
                    + ", properties: " + propertyNames.length);
        }

        if (offset == propertyNames.length) {
            return input;
        }

        final String propName = propertyNames[offset];

        if (null == input) {
            throw new IllegalArgumentException("Property not found: "
                    + Joiner.on('.').join(
                            Arrays.copyOf(propertyNames, offset + 1)));
        }

        // indexed property?
        if (propName.indexOf('[') > 0 && propName.endsWith("]")) {
            return getIndexedProperty(input, propertyNames, offset);
        }
        if (input instanceof Collection) {
            @SuppressWarnings("unchecked")
            Collection<Object> col = (Collection<Object>) input;
            List<Object> result = new ArrayList<Object>(col.size());
            for (Object o : col) {
                Object value = getProperty(o, propName);
                result.add(value);
            }
            return result;
        }

        Object value;
        if (input instanceof Map) {
            if (!((Map<?, ?>) input).containsKey(propName)) {
                throw new IllegalArgumentException("Property " + propName
                        + " does not exist in Map property "
                        + (offset > 0 ? propertyNames[offset - 1] : ""));
            }
            value = ((Map<?, ?>) input).get(propName);
        } else {
            try {
                value = OwsUtils.get(input, propName);
            } catch (IllegalArgumentException noSuchProperty) {
                throw noSuchProperty;
            }
        }

        return getProperty(value, propertyNames, offset + 1);
    }

    private Object getIndexedProperty(Object input,
            final String[] propertyNames, final int offset) {

        final String indexedPropName = propertyNames[offset];

        final String colPropName = indexedPropName.substring(0,
                indexedPropName.indexOf('['));

        final int index;
        {
            final int beginIndex = indexedPropName.indexOf('[') + 1;
            final int endIndex = indexedPropName.length() - 1;
            final String indexStr = indexedPropName.substring(beginIndex,
                    endIndex);
            index = Integer.parseInt(indexStr);
            Preconditions.checkArgument(index > 0,
                    "Illegal indexed property, index shall be > 0: "
                            + indexedPropName);
        }

        Collection<Object> col = getCollectionProperty(input, colPropName);
        if (col == null) {
            return false;
        }
        if (!(col instanceof List)) {
            throw new RuntimeException(
                    "Indexed property access is not valid for property "
                            + colPropName);
        }
        List<Object> list = (List<Object>) col;
        if (index > list.size()) {
            return null;
        }
        Object indexedValue = list.get(index - 1);
        return getProperty(indexedValue, propertyNames, offset + 1);
    }

    private Collection<Object> getCollectionProperty(Object input,
            String colPropName) {

        Object colProp = OwsUtils.get(input, colPropName);
        if (null == colProp) {
            return null;
        }
        if (colProp.getClass().isArray()) {
            int length = Array.getLength(colProp);
            List<Object> array = new ArrayList<Object>(length);
            for (int j = 0; j < length; j++) {
                array.add(Array.get(colProp, j));
            }
            colProp = array;
        }
        if (!(colProp instanceof Collection)) {
            throw new IllegalArgumentException("Specified property "
                    + colPropName + " is not a collection or array: " + colProp);
        }
        @SuppressWarnings("unchecked")
        Collection<Object> col = (Collection<Object>) colProp;
        return col;
    }

    private static Map<Class<?>, Set<String>> FULL_TEXT_PROPERTIES = Maps
            .newHashMap();

    private static Set<String> fullTextProperties(Info obj) {
        Set<String> props = ImmutableSet.of();
        if (obj != null) {
            Class<?> clazz = ModificationProxy.unwrap(obj).getClass();
            ClassMappings classMappings = ClassMappings.fromImpl(clazz);
            checkState(classMappings != null,
                    "No class mappings found for class " + clazz.getName());
            Class<?> interf = classMappings.getInterface();
            props = fullTextProperties(interf);
        }
        return props;
    }

    public static Set<String> fullTextProperties(Class<?> type) {
        if (FULL_TEXT_PROPERTIES.isEmpty()) {
            loadFullTextProperties();
        }
        Set<String> props = FULL_TEXT_PROPERTIES.get(type);
        if (props == null) {
            props = ImmutableSet.of();
        }
        return props;
    }

    /**
     * 
     */
    private static synchronized void loadFullTextProperties() {
        if (!FULL_TEXT_PROPERTIES.isEmpty()) {
            return;
        }
        final String resource = "CatalogPropertyAccessor_FullTextProperties.properties";
        Properties properties = new Properties();
        InputStream stream = CatalogPropertyAccessor.class
                .getResourceAsStream(resource);
        try {
            properties.load(stream);
        } catch (IOException e) {
            throw Throwables.propagate(e);
        } finally {
            Closeables.closeQuietly(stream);
        }
        Map<String, String> map = Maps.fromProperties(properties);
        for (Map.Entry<String, String> e : map.entrySet()) {
            Class<?> key;
            try {
                key = Class.forName(e.getKey());
            } catch (ClassNotFoundException e1) {
                throw propagate(e1);
            }
            String[] split = e.getValue().split(",");
            Set<String> set = Sets.newHashSet();
            for (String s : split) {
                set.add(s.trim());
            }

            FULL_TEXT_PROPERTIES.put(key, ImmutableSet.copyOf(set));
        }
    }
}

Predicates utility factory methods

package org.geoserver.catalog;
/**
 * Static factory method utility to build well known types of {@link Filter}
 * instances.
 * <p>
 * Although {@code Catalog} client code is allowed to use any (well behaving)
 * {@code Filter}, the factory methods in this utility class construct predicate
 * instances of well-known types, in order to aid catalog backend
 * implementations in transforming the predicates to their native query
 * languages.
 * <p>
 * The factory methods in this utility also allow for a more compact code by
 * using static imports, so that, for example:
 * 
 * <pre>
 * <code>
 * FilterFactory ff = CommonFactoryFinder.getFilterFactory();
 * Filter f1 = ff.equals(ff.propertyName('name'), ff.literal('roads'));
 * Filter f2 = ff.equals(ff.propertyName('name'), ff.literal('streams'));
 * Filter f3 = ff.equals(ff.propertyName('enabled'), ff.literal(Boolean.TRUE));
 * Filter filter = ff.and(ff.or(f1, f2), f3);
 * </code>
 * </pre>
 * 
 * becomes:
 * 
 * <pre>
 * <code>
 * Filter filter = and(
 *              or(equal('name', 'roads'),equal('name', 'streams')), 
 *              equal('enabled', Boolean.TRUE));
 * </code>
 * </pre>
 */
public class Predicates {

    private Predicates() {
        //
    }

    public static final FilterFactory factory = CommonFactoryFinder
            .getFilterFactory();

    public static final PropertyName ANY_TEXT = factory.property("AnyText");

    private Predicates() {
        //
    }

    /**
     * @return the "no-filter" predicate.
     */
    public static Filter acceptAll() {
        return Filter.INCLUDE;
    }

    /**
     * @return the "filter-all" predicate.
     */
    public static Filter acceptNone() {
        return Filter.EXCLUDE;
    }

    /**
     * Returns a predicate that checks a CatalogInfo object's property for
     * {@link Object#equals(Object) equality} with the provided property value.
     * <p>
     * The <tt>property</tt> parameter may be specified as a "path" of the form
     * "prop1.prop2". If any of the resulting properties along the path result
     * in null this method will return null.
     * <p>
     * Indexed access to nested list and array properties is supported through
     * the syntax {@code "prop1[M].prop2.prop3[N]"}, where {@code prop1} and
     * {@code prop3} are list or array properties, {@code M} is the index of the
     * {@code prop2} element to retrieve from {@code prop1}, and {@code N} is
     * the index of array or list property {@code prop3} to retrieve. Indexed
     * access to `java.util.Set` properties is <b>not</b> supported.
     * <p>
     * Evaluation of nested properties for <b>any</b> member of a collection
     * property (including Array, List, and Set properties) is supported through
     * the syntax {@code "colProp.name}, which will evaluate to the first
     * {@code name} property of the first {@code colProp} property that matches
     * the expected value. For example,
     * {@code Fitler filter = equal("styles.id", "id1")} creates a
     * predicate that evaluates to {@code true} if any style in the set of
     * styles of a layer has the given id.
     * <p>
     * <p>
     * If the evaluated object property value and the argument value are not of
     * the same type, the returned {@code Predicate} will use the
     * {@link Converters} framework to try to match the two property types
     * before performing an {@code Object.equals} check.
     * <p>
     * Examples:
     * <ul>
     * <li>Simple: {@code equal("id", "myId");}
     * <li>Nested: {@code equal("resource.metadata.someKey", Boolean.TRUE);}
     * <li>Any Collection (for List, Array, and Set properties):
     * {@code equal("styles.name", "point");}: any style in the styles property
     * whose name is "point"
     * <li>Indexed (for List and Array properties):
     * {@code equal("resource.attributes[1]", myAttribute);}
     * <li>Combined:
     * {@code equal("resource.attributes[1].minOccurs", Integer.valueOf(1));}
     * </ul>
     * 
     * @param property the qualified property name of the predicate's input
     *        object to evaluate
     * @param expected the value to check the input object's property against
     * @see PropertyIsEqualTo
     * 
     */
    public static Filter equal(final String property, final Object expected) {
        return equal(property, expected, MatchAction.ALL);
    }

    public static Filter equal(final String property, final Object expected,
            final MatchAction matchAction) {
        final boolean matchCase = true;
        return factory.equal(factory.property(property),
                factory.literal(expected), matchCase, matchAction);
    }

    /**
     * @return a predicate that evaluates whether the given String
     *         {@code property} contains the required character string, in a
     *         <b>case insensitive</b> manner.
     */
    public static Filter contains(final String property,
            final String subsequence) {
        PropertyName propertyName = factory.property(property);
        return contains(propertyName, subsequence);
    }

    public static Filter contains(final PropertyName propertyName,
            final String subsequence) {
        String pattern = "*" + subsequence + "*";
        String wildcard = "*";
        String singleChar = "?";
        String escape = "\\";
        boolean matchCase = false;

        return factory.like(propertyName, pattern, wildcard, singleChar,
                escape, matchCase);
    }

    public static Filter fullTextSearch(final String subsequence) {
        return contains(ANY_TEXT, subsequence);
    }

    /**
     * Returns a predicate that evaluates to {@code true} if each of its
     * components evaluates to {@code true}.
     * <p>
     * The components are evaluated in order, and evaluation will be
     * "short-circuited" as soon as a false predicate is found.
     * 
     */
    public static Filter and(Filter op1, Filter op2) {
        List<Filter> children = new ArrayList<Filter>();
        if (op1 instanceof And) {
            children.addAll(((And) op1).getChildren());
        } else {
            children.add(op1);
        }
        if (op2 instanceof And) {
            children.addAll(((And) op2).getChildren());
        } else {
            children.add(op2);
        }

        return factory.and(children);
    }

    /**
     * Returns a predicate that evaluates to {@code true} if each of its
     * components evaluates to {@code true}.
     * <p>
     * The components are evaluated in order, and evaluation will be
     * "short-circuited" as soon as a false predicate is found.
     * 
     */
    public static Filter and(Filter... operands) {
        List<Filter> anded = Lists.newArrayList(operands);
        return factory.and(anded);
    }

    /**
     * Returns a predicate that evaluates to {@code true} if either of its
     * components evaluates to {@code true}.
     * <p>
     * The components are evaluated in order, and evaluation will be
     * "short-circuited" as soon as a true predicate is found.
     */
    public static Filter or(Filter op1, Filter op2) {
        List<Filter> children = new ArrayList<Filter>();
        if (op1 instanceof Or) {
            children.addAll(((Or) op1).getChildren());
        } else {
            children.add(op1);
        }
        if (op2 instanceof Or) {
            children.addAll(((Or) op2).getChildren());
        } else {
            children.add(op2);
        }
        return factory.or(children);
    }

    public static Filter or(Filter... operands) {
        List<Filter> ored = Lists.newArrayList(operands);
        return factory.or(ored);
    }

    public static Filter isNull(final String propertyName) {
        return factory.isNull(factory.property(propertyName));
    }

    public static SortBy asc(final String propertyName) {
        return sortBy(propertyName, true);
    }

    public static SortBy desc(final String propertyName) {
        return sortBy(propertyName, false);
    }

    public static SortBy sortBy(final String propertyName,
            final boolean ascending) {
        return factory.sort(propertyName, ascending ? SortOrder.ASCENDING
                : SortOrder.DESCENDING);
    }
}

Catalog Extensions

The Catalog interface is augmented with two methods in order to enable counting the number, and obtaining a stream of Catalog objects for a given query predicate, as well as to enable paged queries.

@ParametersAreNonnullByDefault
public interface Catalog {

    .... previous methods ...


    /**
     * Returns the number of catalog objects of the requested type that match
     * the given query predicate.
     * 
     * @param of the type of catalog objects to return. Super interfaces of
     *        concrete catalog objects are allowed (such as
     *        {@code StoreInfo.class} and {@code ResourceInfo.class}, although
     *        the more generic {@code Info.class} and {@code CatalogInfo.class}
     *        are not.
     * @param filter the query predicate, use {@link Filter#INCLUDE} if needed,
     *        {@code null} is not accepted.
     * @return the total number of catalog objects of the requested type that
     *         match the query predicate.
     */
    public <T extends CatalogInfo> int count(final Class<T> of,
            final Filter filter);

    /**
     * Access to a single configuration object by the given predicate filter,
     * fails if more than one object satisfies the filter criteria.
     * <p>
     * Generally useful for query by id or name where name is known to be
     * unique, either globally or per workspace, although usage is not limited
     * to those cases.
     * <p>
     * Examples:
     * 
     * <pre>
     * <code>
     * import static org.geoserver.catalog.Predicates.equal;
     * import static org.geoserver.catalog.Predicates.and;
     * import static org.geoserver.catalog.Predicates.isNull;
     * ...
     * Catalog catalog = ...
     * LayerInfo layer = catalog.get(LayerInfo.class, equal("id", "layer1");
     * 
     * WorkspaceInfo ws = catalog.get(WorkspaceInfo.class, 
     *                  equal("resource.store.workspace.name", 
     *                  layer.getResource().getStore().getWorkspace().getName);
     *                                
     * LayerGroupInfo wslg = catalog.get(LayerGroupInfo.class, 
     *                          and(equal("name", "lg1"), 
     *                              equal("workspace.name", "ws1"));
     *                              
     * LayerGroupInfo globallg = catalog.get(LayerGroupInfo.class, 
     *                          and(equal("name", "lg1"), isNull("workspace.id"));
     * </code>
     * </pre>
     * 
     * @return the single object of the given {@code type} that matches the
     *         given filter, or {@code null} if no object matches the provided
     *         filter.
     * @throws IllegalArgumentExeption if more than one object of type {@code T}
     *         match the provided filter.
     */
    <T extends CatalogInfo> T get(Class<T> type, Filter filter)
            throws IllegalArgumentException;

    /**
     * Returns an {@link Iterator} over the catalog objects of the requested
     * type that match the given query predicate, positioned at the specified
     * {@code offset} and limited to the number requested number of elements.
     * <p>
     * The returned iterator <strong>shall</strong> be closed once it is no
     * longer needed, to account for streaming implementations of this interface
     * to release any needed resource such as database or remote service
     * connections. Example usage:
     * 
     * <pre>
     * <code>
     * Catalog catalog = ...
     * Filter filter = ...
     * CloseableIterator<LayerInfo> iterator = catalog.list(LayerInfo.class, filter);
     * try{
     *   while(iterator.hasNext()){
     *     iterator.next();
     *   }
     * }finally{
     *   iterator.close();
     * }
     * </code>
     * </pre>
     * 
     * @param of the type of catalog objects to return. Super interfaces of
     *        concrete catalog objects are allowed (such as
     *        {@code StoreInfo.class} and {@code ResourceInfo.class}, although
     *        the more generic {@code Info.class} and {@code CatalogInfo.class}
     *        are not.
     * @param filter the query predicate, use {@link Filter#INCLUDE} if needed,
     *        {@code null} is not accepted.
     * 
     * @return an iterator over the predicate matching catalog objects that must
     *         be closed once consumed
     * @throws IllegalArgumentException if {@code sortOrder != null} and
     *         {@link #canSort !canSort(of, sortOrder)}
     */
    public <T extends CatalogInfo> CloseableIterator<T> list(final Class<T> of,
            final Filter filter);

    /**
     * Returns an {@link Iterator} over the catalog objects of the requested
     * type that match the given query predicate, positioned at the specified
     * {@code offset} and limited to the number requested number of elements.
     * <p>
     * Through the optional {@code offset} and {@code count} arguments, this
     * method allows for paged queries over the catalog contents. Note that
     * although there's no prescribed sort order, catalog back end
     * implementations must provide a natural sort order (either based on id or
     * otherwise), in order for paged queries to be consistent between calls for
     * the same predicate.
     * <p>
     * The returned iterator <strong>shall</strong> be closed once it is no
     * longer needed, to account for streaming implementations of this interface
     * to release any needed resource such as database or remote service
     * connections. Example usage:
     * 
     * <pre>
     * <code>
     * Catalog catalog = ...
     * Filter filter = ...
     * CloseableIterator<LayerInfo> iterator = catalog.list(LayerInfo.class, filter);
     * try{
     *   while(iterator.hasNext()){
     *     iterator.next();
     *   }
     * }finally{
     *   iterator.close();
     * }
     * </code>
     * </pre>
     * 
     * @param of the type of catalog objects to return. Super interfaces of
     *        concrete catalog objects are allowed (such as
     *        {@code StoreInfo.class} and {@code ResourceInfo.class}, although
     *        the more generic {@code Info.class} and {@code CatalogInfo.class}
     *        are not.
     * @param filter the query predicate, use {@link Predicates#ACCEPT_ALL} if
     *        needed, {@code null} is not accepted.
     * @param offset {@code null} to return an iterator starting at the first
     *        matching object, otherwise an integer {@code >= 0} to return an
     *        iterator positioned at the specified offset.
     * @param count {@code null} to return a non limited in number of elements
     *        iterator, an integer {@code >= 0} otherwise to specify the maximum
     *        number of elements the iterator shall return.
     * @param sortBy
     * 
     * @return an iterator over the predicate matching catalog objects that must
     *         be closed once consumed
     * @throws IllegalArgumentException if {@code sortOrder != null} and
     *         {@link #canSort !canSort(of, sortOrder)}
     */
    public <T extends CatalogInfo> CloseableIterator<T> list(final Class<T> of,
            final Filter filter, @Nullable Integer offset,
            @Nullable Integer count, @Nullable SortBy sortBy);
}

CatalogFacade

The CatalogFacade interface is augmented by the following three methods in order to cope up with the three main interface use cases in a generic way: a) get a single object, b) get a filtered and possibly paged list of objects, and c) compute the number of objects that satisfy a query criteria.

public interface CatalogFacade {

    ....
    /**
     * @return the number of catalog objects of the requested type that match
     *         the given filter
     */
    public <T extends CatalogInfo> int count(final Class<T> of,
            final Filter filter);

    /**
     * @return {@code true} if {@link #list} can sort objects of the given type
     *         by the given property name, {@code false} otherwise
     */
    public boolean canSort(Class<? extends CatalogInfo> type,
            String propertyName);

    /**
     * @return an iterator over the catalog objects of the requested type that
     *         match the given filter
     */
    public <T extends CatalogInfo> CloseableIterator<T> list(final Class<T> of,
            final Filter filter, @Nullable Integer offset,
            @Nullable Integer count, @Nullable SortBy sortOrder);
}

Default Implementation

DefaultCatalogFacade

public class DefaultCatalogFacade extends AbstractCatalogFacade implements CatalogFacade {
    protected MultiHashMap/* <Class> */stores = ...
    protected Map<String, DataStoreInfo> defaultStores = ...
    protected MultiHashMap/* <Class> */resources = ...
    protected HashMap<String, NamespaceInfo> namespaces = ...
    protected HashMap<String, WorkspaceInfo> workspaces = ...
    protected List<LayerInfo> layers = ...
    protected List<MapInfo> maps = ...
    protected List<LayerGroupInfo> layerGroups = ...
    protected List<StyleInfo> styles = ...

    ...
    @Override
    public <T extends CatalogInfo> int count(final Class<T> of,
            final Filter filter) {
        return Iterables.size(iterable(of, filter, null));
    }

    /**
     * This default implementations supports sorting against properties (could
     * be nested) that are either of a primitive type or implement
     * {@link Comparable}.
     * 
     * @param type the type of object to sort
     * @param propertyName the property name of the objects of type {@code type}
     *        to sort by
     * @see org.geoserver.catalog.CatalogFacade#canSort(java.lang.Class,
     *      java.lang.String)
     */
    @Override
    public boolean canSort(final Class<? extends CatalogInfo> type,
            final String propertyName) {
        final String[] path = propertyName.split("\\.");
        Class<?> clazz = type;
        for (int i = 0; i < path.length; i++) {
            String property = path[i];
            Method getter;
            try {
                getter = OwsUtils.getter(clazz, property, null);
            } catch (RuntimeException e) {
                return false;
            }
            clazz = getter.getReturnType();
            if (i == path.length - 1) {
                boolean primitive = clazz.isPrimitive();
                boolean comparable = Comparable.class.isAssignableFrom(clazz);
                boolean canSort = primitive || comparable;
                return canSort;
            }
        }
        throw new IllegalStateException("empty property name");
    }

    @Override
    public <T extends CatalogInfo> CloseableIterator<T> list(final Class<T> of,
            final Filter filter, @Nullable Integer offset,
            @Nullable Integer count, @Nullable SortBy sortOrder) {

        if (null != sortOrder
                && !canSort(of, sortOrder.getPropertyName().getPropertyName())) {
            throw new IllegalArgumentException("Can't sort objects of type "
                    + of.getName() + " by " + sortOrder.getPropertyName());
        }

        Iterable<T> iterable = iterable(of, filter, sortOrder);

        if (offset != null && offset.intValue() > 0) {
            iterable = Iterables.skip(iterable, offset.intValue());
        }

        if (count != null && count.intValue() >= 0) {
            iterable = Iterables.limit(iterable, count.intValue());
        }

        Iterator<T> iterator = iterable.iterator();

        return new CloseableIteratorAdapter<T>(iterator);
    }

    public <T extends CatalogInfo> Iterable<T> iterable(
            final Class<? super T> of, final Filter filter, final SortBy sortBy) {
        List<T> all;

        T t = null;
        if (NamespaceInfo.class.isAssignableFrom(of)) {
            all = (List<T>) getNamespaces();
        } else if (WorkspaceInfo.class.isAssignableFrom(of)) {
            all = (List<T>) getWorkspaces();
        } else if (StoreInfo.class.isAssignableFrom(of)) {
            all = (List<T>) getStores(of);
        } else if (ResourceInfo.class.isAssignableFrom(of)) {
            all = (List<T>) getResources(of);
        } else if (LayerInfo.class.isAssignableFrom(of)) {
            all = (List<T>) getLayers();
        } else if (LayerGroupInfo.class.isAssignableFrom(of)) {
            all = (List<T>) getLayerGroups();
        } else if (StyleInfo.class.isAssignableFrom(of)) {
            all = (List<T>) getStyles();
        } else if (MapInfo.class.isAssignableFrom(of)) {
            all = (List<T>) getMaps();
        } else {
            throw new IllegalArgumentException("Unknown type: " + of);
        }

        if (null != sortBy) {
            Ordering<Object> ordering = Ordering.from(comparator(sortBy));
            if (SortOrder.DESCENDING.equals(sortBy.getSortOrder())) {
                ordering = ordering.reverse();
            }
            all = ordering.sortedCopy(all);
        }

        if (Filter.INCLUDE.equals(filter)) {
            return all;
        }

        com.google.common.base.Predicate<T> filterAdapter = new com.google.common.base.Predicate<T>() {

            @Override
            public boolean apply(T input) {
                return filter.evaluate(input);
            }
        };

        return Iterables.filter(all, filterAdapter);
    }

    private Comparator<Object> comparator(final SortBy sortOrder) {
        return new Comparator<Object>() {
            @Override
            public int compare(Object o1, Object o2) {
                Object v1 = OwsUtils.get(o1, sortOrder.getPropertyName()
                        .getPropertyName());
                Object v2 = OwsUtils.get(o2, sortOrder.getPropertyName()
                        .getPropertyName());
                if (v1 == null) {
                    if (v2 == null) {
                        return 0;
                    } else {
                        return -1;
                    }
                } else if (v2 == null) {
                    return 1;
                }
                Comparable c1 = (Comparable) v1;
                Comparable c2 = (Comparable) v2;
                return c1.compareTo(c2);
            }
        };
    }
Clone this wiki locally