/* (c) 2014 - 2015 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.catalog.impl; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Throwables.propagate; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.catalog.Info; import org.geoserver.catalog.Predicates; import org.geoserver.catalog.ResourceInfo; import org.geoserver.ows.util.OwsUtils; import org.geotools.filter.expression.PropertyAccessor; import org.geotools.util.Converters; import org.geotools.util.logging.Logging; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.Closeables; /** * 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 { private static final Logger LOGGER = Logging.getLogger(CatalogPropertyAccessor.class); @Override public boolean canHandle(Object object, String xpath, Class<?> target) { return object instanceof Info; } @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(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 * */ @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) { if(o == null) { continue; } // if one of the nested properties is not found just ignore and move // to the next one, we can have mixed collections (e.g., layer group layers) try { Object value = getProperty(o, propName); Object nested = getProperty(value, propertyNames, offset + 1); result.add(nested); } catch(Exception e) { LOGGER.log(Level.FINE, "Skipping nested property not found", e); } } 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 { //special case for ResourceInfo bounding box, used the derived property if ("boundingBox".equalsIgnoreCase(propName) && input instanceof ResourceInfo) { try { value = ((ResourceInfo) input).boundingBox(); } catch (Exception e) { throw new IllegalArgumentException(e); } } else { value = OwsUtils.get(input, propName); } } // if our nested access stumbles onto a null, we return a null value to allow // for full text searches to work (e.g., workspace.name, but workspace can be null // in both layer groups and styles if (value == null) { return null; } 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 { try { Closeables.close(stream, false); } catch (IOException e) { LOGGER.log(Level.FINE, "Ignoring exception thrown while closing " + resource + " in CatalogPropertyAccessor", e); } } 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)); } } }