org.geoserver.jdbcconfig.internal.ConfigDatabase.java Source code

Java tutorial

Introduction

Here is the source code for org.geoserver.jdbcconfig.internal.ConfigDatabase.java

Source

/* (c) 2014 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.jdbcconfig.internal;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.geoserver.jdbcconfig.internal.DbUtils.logStatement;
import static org.geoserver.jdbcconfig.internal.DbUtils.params;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;
import javax.sql.DataSource;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.wicket.util.string.Strings;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.CatalogVisitorAdapter;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.Info;
import org.geoserver.catalog.LayerGroupInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.MetadataMap;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.catalog.Predicates;
import org.geoserver.catalog.PublishedInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.WMSLayerInfo;
import org.geoserver.catalog.WMSStoreInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.event.CatalogAddEvent;
import org.geoserver.catalog.event.CatalogListener;
import org.geoserver.catalog.event.CatalogModifyEvent;
import org.geoserver.catalog.event.CatalogPostModifyEvent;
import org.geoserver.catalog.event.CatalogRemoveEvent;
import org.geoserver.catalog.impl.CatalogImpl;
import org.geoserver.catalog.impl.ClassMappings;
import org.geoserver.catalog.impl.ModificationProxy;
import org.geoserver.catalog.impl.ResourceInfoImpl;
import org.geoserver.catalog.impl.StoreInfoImpl;
import org.geoserver.catalog.impl.StyleInfoImpl;
import org.geoserver.catalog.util.CloseableIterator;
import org.geoserver.catalog.util.CloseableIteratorAdapter;
import org.geoserver.config.ConfigurationListenerAdapter;
import org.geoserver.config.GeoServer;
import org.geoserver.config.GeoServerInfo;
import org.geoserver.config.LoggingInfo;
import org.geoserver.config.ServiceInfo;
import org.geoserver.config.SettingsInfo;
import org.geoserver.config.impl.CoverageAccessInfoImpl;
import org.geoserver.config.impl.GeoServerInfoImpl;
import org.geoserver.config.impl.JAIInfoImpl;
import org.geoserver.ows.util.OwsUtils;
import org.geoserver.platform.resource.Resource;
import org.geoserver.util.CacheProvider;
import org.geoserver.util.DefaultCacheProvider;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.util.Converters;
import org.geotools.util.logging.Logging;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

/**
 * 
 */
public class ConfigDatabase {

    public static final Logger LOGGER = Logging.getLogger(ConfigDatabase.class);

    private Dialect dialect;

    private DataSource dataSource;

    private DbMappings dbMappings;

    private CatalogImpl catalog;

    private GeoServer geoServer;

    private NamedParameterJdbcOperations template;

    private XStreamInfoSerialBinding binding;

    private Cache<String, Info> cache;

    private InfoRowMapper<CatalogInfo> catalogRowMapper;

    private InfoRowMapper<Info> configRowMapper;

    private CatalogClearingListener catalogListener;
    private ConfigClearingListener configListener;

    /**
     * Protected default constructor needed by spring-jdbc instrumentation
     */
    protected ConfigDatabase() {
        //
    }

    public ConfigDatabase(final DataSource dataSource, final XStreamInfoSerialBinding binding) {
        this(dataSource, binding, null);
    }

    public ConfigDatabase(final DataSource dataSource, final XStreamInfoSerialBinding binding,
            CacheProvider cacheProvider) {

        this.binding = binding;
        this.template = new NamedParameterJdbcTemplate(dataSource);
        // cannot use dataSource at this point due to spring context config hack
        // in place to support tx during testing
        this.dataSource = dataSource;

        this.catalogRowMapper = new InfoRowMapper<CatalogInfo>(CatalogInfo.class, binding);
        this.configRowMapper = new InfoRowMapper<Info>(Info.class, binding);

        if (cacheProvider == null) {
            cacheProvider = DefaultCacheProvider.findProvider();
        }
        cache = cacheProvider.getCache("catalog");
    }

    private Dialect dialect() {
        if (dialect == null) {
            this.dialect = Dialect.detect(dataSource);
        }
        return dialect;
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void initDb(@Nullable Resource resource) throws IOException {
        this.dbMappings = new DbMappings(dialect());
        if (resource != null) {
            runInitScript(resource);
        }
        dbMappings.initDb(template);
    }

    private void runInitScript(Resource resource) throws IOException {

        LOGGER.info("------------- Running catalog database init script " + resource.path() + " ------------");

        try (InputStream in = resource.in()) {
            Util.runScript(in, template.getJdbcOperations(), LOGGER);
        }

        LOGGER.info("Initialization SQL script run sucessfully");
    }

    public DbMappings getDbMappings() {
        return dbMappings;
    }

    public void setCatalog(CatalogImpl catalog) {
        this.catalog = catalog;
        this.binding.setCatalog(catalog);

        catalog.removeListeners(CatalogClearingListener.class);
        catalog.addListener(new CatalogClearingListener());
    }

    public CatalogImpl getCatalog() {
        return this.catalog;
    }

    public void setGeoServer(GeoServer geoServer) {
        this.geoServer = geoServer;

        if (configListener != null)
            geoServer.removeListener(configListener);
        configListener = new ConfigClearingListener();
        geoServer.addListener(configListener);
    }

    public GeoServer getGeoServer() {
        return geoServer;
    }

    public <T extends CatalogInfo> int count(final Class<T> of, final Filter filter) {

        QueryBuilder<T> sqlBuilder = QueryBuilder.forCount(dialect, of, dbMappings).filter(filter);

        final StringBuilder sql = sqlBuilder.build();
        final Filter unsupportedFilter = sqlBuilder.getUnsupportedFilter();
        final boolean fullySupported = Filter.INCLUDE.equals(unsupportedFilter);
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.finer("Original filter: " + filter);
            LOGGER.finer("Supported filter: " + sqlBuilder.getSupportedFilter());
            LOGGER.finer("Unsupported filter: " + sqlBuilder.getUnsupportedFilter());
        }
        final int count;
        if (fullySupported) {
            final Map<String, Object> namedParameters = sqlBuilder.getNamedParameters();
            logStatement(sql, namedParameters);

            count = template.queryForObject(sql.toString(), namedParameters, Integer.class);
        } else {
            LOGGER.fine(
                    "Filter is not fully supported, doing scan of supported part to return the number of matches");
            // going the expensive route, filtering as much as possible
            CloseableIterator<T> iterator = query(of, filter, null, null, (SortBy) null);
            try {
                return Iterators.size(iterator);
            } finally {
                iterator.close();
            }
        }
        return count;
    }

    public <T extends Info> CloseableIterator<T> query(final Class<T> of, final Filter filter,
            @Nullable Integer offset, @Nullable Integer limit, @Nullable SortBy sortOrder) {
        if (sortOrder == null) {
            return query(of, filter, offset, limit, new SortBy[] {});
        } else {
            return query(of, filter, offset, limit, new SortBy[] { sortOrder });
        }
    }

    public <T extends Info> CloseableIterator<T> query(final Class<T> of, final Filter filter,
            @Nullable Integer offset, @Nullable Integer limit, @Nullable SortBy... sortOrder) {

        checkNotNull(of);
        checkNotNull(filter);
        checkArgument(offset == null || offset.intValue() >= 0);
        checkArgument(limit == null || limit.intValue() >= 0);

        QueryBuilder<T> sqlBuilder = QueryBuilder.forIds(dialect, of, dbMappings).filter(filter).offset(offset)
                .limit(limit).sortOrder(sortOrder);

        final StringBuilder sql = sqlBuilder.build();
        final Map<String, Object> namedParameters = sqlBuilder.getNamedParameters();
        final Filter unsupportedFilter = sqlBuilder.getUnsupportedFilter();
        final boolean fullySupported = Filter.INCLUDE.equals(unsupportedFilter);

        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.finer("Original filter: " + filter);
            LOGGER.finer("Supported filter: " + sqlBuilder.getSupportedFilter());
            LOGGER.finer("Unsupported filter: " + sqlBuilder.getUnsupportedFilter());
        }
        logStatement(sql, namedParameters);

        Stopwatch sw = Stopwatch.createStarted();
        // the oracle offset/limit implementation returns a two column result set
        // with rownum in the 2nd - queryForList will throw an exception
        List<String> ids = template.query(sql.toString(), namedParameters, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getString(1);
            }
        });
        sw.stop();
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine(Joiner.on("").join("query returned ", ids.size(), " records in ", sw.toString()));
        }

        List<T> lazyTransformed = Lists.transform(ids, new Function<String, T>() {
            @Nullable
            @Override
            public T apply(String id) {
                return getById(id, of);
            }
        });

        CloseableIterator<T> result;
        Iterator<T> iterator = Iterators.filter(lazyTransformed.iterator(),
                com.google.common.base.Predicates.notNull());

        if (fullySupported) {
            result = new CloseableIteratorAdapter<T>(iterator);
        } else {
            // Apply the filter
            result = CloseableIteratorAdapter.filter(iterator, filter);
            // The offset and limit should not have been applied as part of the query
            assert (!sqlBuilder.isOffsetLimitApplied());
            // Apply offset and limits after filtering
            result = applyOffsetLimit(result, offset, limit);
        }

        return result;
    }

    private <T extends Info> CloseableIterator<T> applyOffsetLimit(CloseableIterator<T> iterator, Integer offset,
            Integer limit) {
        if (offset != null) {
            Iterators.advance(iterator, offset.intValue());
        }
        if (limit != null) {
            iterator = CloseableIteratorAdapter.limit(iterator, limit.intValue());
        }
        return iterator;
    }

    public <T extends Info> List<T> queryAsList(final Class<T> of, final Filter filter, Integer offset,
            Integer count, SortBy sortOrder) {

        CloseableIterator<T> iterator = query(of, filter, offset, count, sortOrder);
        List<T> list;
        try {
            list = ImmutableList.copyOf(iterator);
        } finally {
            iterator.close();
        }
        return list;
    }

    public <T extends CatalogInfo> T getDefault(final String key, Class<T> type) {
        String sql = "SELECT ID FROM DEFAULT_OBJECT WHERE DEF_KEY = :key";

        String defaultObjectId;
        try {
            ImmutableMap<String, String> params = ImmutableMap.of("key", key);
            logStatement(sql, params);
            defaultObjectId = template.queryForObject(sql, params, String.class);
        } catch (EmptyResultDataAccessException notFound) {
            return null;
        }
        return getById(defaultObjectId, type);
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public <T extends Info> T add(final T info) {
        checkNotNull(info);
        checkNotNull(info.getId(), "Object has no id");
        checkArgument(!(info instanceof Proxy), "Added object shall not be a dynamic proxy");

        final String id = info.getId();

        byte[] value = binding.objectToEntry(info);

        final String blob = new String(value);
        final Class<T> interf = ClassMappings.fromImpl(info.getClass()).getInterface();
        final Integer typeId = dbMappings.getTypeId(interf);

        Map<String, ?> params = params("type_id", typeId, "id", id, "blob", blob);
        final String statement = String.format(
                "insert into object (oid, type_id, id, blob) values (%s, :type_id, :id, :blob)",
                dialect.nextVal("seq_OBJECT"));
        logStatement(statement, params);
        KeyHolder keyHolder = new GeneratedKeyHolder();
        int updateCount = template.update(statement, new MapSqlParameterSource(params), keyHolder,
                new String[] { "oid" });
        checkState(updateCount == 1, "Insert statement failed");
        // looks like some db's return the pk different than others, so lets try both ways
        Number key = (Number) keyHolder.getKeys().get("oid");
        if (key == null) {
            key = keyHolder.getKey();
        }
        addAttributes(info, key);

        cache.put(id, info);
        return getById(id, interf);
    }

    private void addAttributes(final Info info, final Number infoPk) {
        final String id = info.getId();
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.finer("Storing properties of " + id + " with pk " + infoPk);
        }

        final Iterable<Property> properties = dbMappings.properties(info);

        for (Property prop : properties) {
            if (LOGGER.isLoggable(Level.FINEST)) {
                LOGGER.finest("Adding property " + prop.getPropertyName() + "='" + prop.getValue() + "'");
            }

            final List<?> values = valueList(prop);

            Object propValue;
            Integer colIndex;

            for (int index = 0; index < values.size(); index++) {
                colIndex = prop.isCollectionProperty() ? (index + 1) : 0;
                propValue = values.get(index);
                final String storedValue = marshalValue(propValue);

                addAttribute(info, infoPk, prop, colIndex, storedValue);
            }
        }
    }

    private void addAttribute(final Info info, final Number infoPk, Property prop, Integer colIndex,
            final String storedValue) {
        Map<String, ?> params = params("value", storedValue);

        final String insertPropertySQL = "insert into object_property " //
                + "(oid, property_type, related_oid, related_property_type, colindex, value, id) " //
                + "values (:object_id, :property_type, :related_oid, :related_property_type, :colindex, :value, :id)";

        final boolean isRelationShip = prop.isRelationship();

        Integer relatedObjectId = null;
        final Integer concreteTargetPropertyOid;

        if (isRelationShip) {
            Info relatedObject = lookUpRelatedObject(info, prop, colIndex);
            if (relatedObject == null) {
                concreteTargetPropertyOid = null;
            } else {
                // the related property may refer to an abstract type (e.g.
                // LayerInfo.resource.name), so we need to find out the actual property type id (for
                // example, whether it belongs to FeatureTypeInfo or CoverageInfo)
                relatedObject = ModificationProxy.unwrap(relatedObject);
                relatedObjectId = this.findObjectId(relatedObject);

                Integer targetPropertyOid = prop.getPropertyType().getTargetPropertyOid();
                PropertyType targetProperty;
                String targetPropertyName;

                Class<?> targetQueryType;
                ClassMappings classMappings = ClassMappings.fromImpl(relatedObject.getClass());
                targetQueryType = classMappings.getInterface();
                targetProperty = dbMappings.getPropertyType(targetPropertyOid);
                targetPropertyName = targetProperty.getPropertyName();

                Set<Integer> propertyTypeIds;
                propertyTypeIds = dbMappings.getPropertyTypeIds(targetQueryType, targetPropertyName);
                checkState(propertyTypeIds.size() == 1);
                concreteTargetPropertyOid = propertyTypeIds.iterator().next();
            }
        } else {
            concreteTargetPropertyOid = null;
        }

        final Number propertyType = prop.getPropertyType().getOid();
        final String id = info.getId();

        params = params("object_id", infoPk, //
                "property_type", propertyType, //
                "id", id, //
                "related_oid", relatedObjectId, //
                "related_property_type", concreteTargetPropertyOid, //
                "colindex", colIndex, //
                "value", storedValue);

        logStatement(insertPropertySQL, params);
        template.update(insertPropertySQL, params);
    }

    /**
     * @param info
     * @param prop
     *
     */
    private Info lookUpRelatedObject(final Info info, final Property prop, @Nullable Integer collectionIndex) {

        checkArgument(collectionIndex == 0 || prop.isCollectionProperty());

        final FilterFactory ff = CommonFactoryFinder.getFilterFactory();

        final Integer targetPropertyTypeId = prop.getPropertyType().getTargetPropertyOid();
        checkArgument(targetPropertyTypeId != null);

        final PropertyType targetPropertyType = dbMappings.getPropertyType(targetPropertyTypeId);
        checkState(targetPropertyType != null);

        final Class<?> targetType = dbMappings.getType(targetPropertyType.getObjectTypeOid());
        checkState(targetType != null);

        final String localPropertyName = prop.getPropertyName();
        String[] steps = localPropertyName.split("\\.");
        // Step back through ancestor property references If starting at a.b.c.d, then look at a.b.c, then a.b, then a
        for (int i = steps.length - 1; i >= 0; i--) {
            String backPropName = Strings.join(".", Arrays.copyOfRange(steps, 0, i));
            Object backProp = ff.property(backPropName).evaluate(info);
            if (backProp != null) {
                if (prop.isCollectionProperty() && (backProp instanceof Set || backProp instanceof List)) {
                    List<?> list;
                    if (backProp instanceof Set) {
                        list = asValueList(backProp);
                        if (list.size() > 0 && list.get(0) != null
                                && targetType.isAssignableFrom(list.get(0).getClass())) {
                            String targetPropertyName = targetPropertyType.getPropertyName();
                            final PropertyName expr = ff.property(targetPropertyName);
                            Collections.sort(list, new Comparator<Object>() {
                                @Override
                                public int compare(Object o1, Object o2) {
                                    Object v1 = expr.evaluate(o1);
                                    Object v2 = expr.evaluate(o2);
                                    String m1 = marshalValue(v1);
                                    String m2 = marshalValue(v2);
                                    return m1 == null ? (m2 == null ? 0 : -1) : (m2 == null ? 1 : m1.compareTo(m2));
                                }
                            });
                        }
                    } else {
                        list = (List<?>) backProp;
                    }
                    if (collectionIndex <= list.size()) {
                        backProp = list.get(collectionIndex - 1);
                    }
                }
                if (targetType.isAssignableFrom(backProp.getClass())) {
                    return (Info) backProp;
                }
            }
        }
        // throw new IllegalArgumentException("Found no related object of type "
        // + targetType.getName() + " for property " + localPropertyName + " of " + info);
        return null;
    }

    private List<?> valueList(Property prop) {
        final Object value = prop.getValue();
        return asValueList(value);
    }

    private List<?> asValueList(final Object value) {
        final List<?> values;
        if (value instanceof List) {
            values = (List<?>) value;
        } else if (value instanceof Collection) {
            values = Lists.newArrayList((Collection<?>) value);
        } else {
            values = Lists.newArrayList(value);
        }
        return values;
    }

    /**
     * @return the stored representation of a scalar property value
     */
    private String marshalValue(Object propValue) {
        // TODO pad numeric values
        String marshalled = Converters.convert(propValue, String.class);
        return marshalled;
    }

    /**
     * @param info
     */
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void remove(Info info) {

        final Integer oid = findObjectId(info);
        if (oid == null) {
            return;
        }
        cache.invalidate(info.getId());

        String deleteObject = "delete from object where id = :id";
        String deleteRelatedProperties = "delete from object_property where related_oid = :oid";

        int updateCount = template.update(deleteObject, ImmutableMap.of("id", info.getId()));
        if (updateCount != 1) {
            LOGGER.warning("Requested to delete " + info + " (" + info.getId()
                    + ") but nothing happened on the database.");
        }
        final int relatedPropCount = template.update(deleteRelatedProperties, params("oid", oid));
        LOGGER.fine("Removed " + relatedPropCount + " related properties of " + info.getId());

        cache.invalidate(info.getId());
    }

    /**
     * @param info
     *
     */
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public <T extends Info> T save(T info) {
        checkNotNull(info);

        final String id = info.getId();

        checkNotNull(id, "Can't modify an object with no id");

        final ModificationProxy modificationProxy = ModificationProxy.handler(info);
        Preconditions.checkNotNull(modificationProxy, "Not a modification proxy: ", info);

        final Info oldObject = (Info) modificationProxy.getProxyObject();

        cache.invalidate(id);

        // get changed properties before h.commit()s
        final Iterable<Property> changedProperties = dbMappings.changedProperties(oldObject, info);

        // see HACK block bellow
        final boolean updateResouceLayersName = info instanceof ResourceInfo
                && modificationProxy.getPropertyNames().contains("name");
        final boolean updateResourceLayersKeywords = CollectionUtils.exists(modificationProxy.getPropertyNames(),
                new Predicate() {
                    @Override
                    public boolean evaluate(Object input) {
                        return ((String) input).contains("keyword");
                    }

                });

        modificationProxy.commit();

        Map<String, ?> params;

        // get the object's internal id
        final Integer objectId = findObjectId(info);
        final String blob;
        try {
            byte[] value = binding.objectToEntry(info);
            blob = new String(value, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw Throwables.propagate(e);
        }
        String updateStatement = "update object set blob = :blob where oid = :oid";
        params = params("blob", blob, "oid", objectId);
        logStatement(updateStatement, params);
        template.update(updateStatement, params);

        updateQueryableProperties(oldObject, objectId, changedProperties);

        cache.invalidate(id);
        Class<T> clazz = ClassMappings.fromImpl(oldObject.getClass()).getInterface();

        // / <HACK>
        // we're explicitly changing the resourceinfo's layer name property here because
        // LayerInfo.getName() is a derived property. This can be removed once LayerInfo.name become
        // a regular JavaBean property
        if (info instanceof ResourceInfo) {
            if (updateResouceLayersName) {
                updateResourceLayerName((ResourceInfo) info);
            }
            if (updateResourceLayersKeywords) {
                updateResourceLayerKeywords((ResourceInfo) info);
            }
        }
        // / </HACK>
        return getById(id, clazz);
    }

    private <T> void updateResourceLayerName(ResourceInfo info) {
        final Object newValue = info.getName();
        Filter filter = Predicates.equal("resource.id", info.getId());
        List<LayerInfo> resourceLayers;
        resourceLayers = this.queryAsList(LayerInfo.class, filter, null, null, null);
        for (LayerInfo layer : resourceLayers) {
            Set<PropertyType> propertyTypes = dbMappings.getPropertyTypes(LayerInfo.class, "name");
            PropertyType propertyType = propertyTypes.iterator().next();
            Property changedProperty = new Property(propertyType, newValue);
            Integer layerOid = findObjectId(layer);
            updateQueryableProperties(layer, layerOid, ImmutableSet.of(changedProperty));
        }
    }

    private <T> void updateResourceLayerKeywords(ResourceInfo info) {
        final Object newValue = info.getKeywords();
        Filter filter = Predicates.equal("resource.id", info.getId());
        List<LayerInfo> resourceLayers;
        resourceLayers = this.queryAsList(LayerInfo.class, filter, null, null, null);
        for (LayerInfo layer : resourceLayers) {
            Set<PropertyType> propertyTypes = dbMappings.getPropertyTypes(LayerInfo.class,
                    "resource.keywords.value");
            PropertyType propertyType = propertyTypes.iterator().next();
            Property changedProperty = new Property(propertyType, newValue);
            Integer layerOid = findObjectId(layer);
            updateQueryableProperties(layer, layerOid, ImmutableSet.of(changedProperty));
        }
    }

    private Integer findObjectId(final Info info) {
        final String id = info.getId();
        final String oidQuery = "select oid from object where id = :id";
        Map<String, ?> params = params("id", id);
        logStatement(oidQuery, params);
        final Integer objectId = template.queryForObject(oidQuery, params, Integer.class);
        Preconditions.checkState(objectId != null, "Object not found: " + id);
        return objectId;
    }

    private void updateQueryableProperties(final Info info, final Integer objectId,
            Iterable<Property> changedProperties) {

        Map<String, ?> params;

        final Integer oid = objectId;
        Integer propertyType;
        Integer relatedOid;
        Integer relatedPropertyType;
        Integer colIndex;
        String storedValue;

        for (Property changedProp : changedProperties) {
            LOGGER.finer("Updating property " + changedProp);

            final boolean isRelationship = changedProp.isRelationship();
            propertyType = changedProp.getPropertyType().getOid();

            final List<?> values = valueList(changedProp);

            for (int i = 0; i < values.size(); i++) {
                final Object rawValue = values.get(i);
                storedValue = marshalValue(rawValue);
                checkArgument(changedProp.isCollectionProperty() || values.size() == 1,
                        "Got a multivalued value for a non collection property " + changedProp.getPropertyName()
                                + "=" + values);

                colIndex = changedProp.isCollectionProperty() ? (i + 1) : 0;

                if (isRelationship) {
                    final Info relatedObject = lookUpRelatedObject(info, changedProp, colIndex);
                    relatedOid = relatedObject == null ? null : findObjectId(relatedObject);
                    relatedPropertyType = changedProp.getPropertyType().getTargetPropertyOid();
                } else {
                    // it's a self property, lets update the value on the property table
                    relatedOid = null;
                    relatedPropertyType = null;
                }
                String sql = "update object_property set " //
                        + "related_oid = :related_oid, "//
                        + "related_property_type = :related_property_type, "//
                        + "value = :value "//
                        + "where oid = :oid and property_type = :property_type and colindex = :colindex";
                params = params("related_oid", relatedOid, "related_property_type", relatedPropertyType, "value",
                        storedValue, "oid", oid, "property_type", propertyType, "colindex", colIndex);

                logStatement(sql, params);
                final int updateCnt = template.update(sql, params);

                if (updateCnt == 0) {
                    addAttribute(info, oid, changedProp, colIndex, storedValue);
                } else {
                    // prop existed already, lets update any related property that points to its old
                    // value
                    String updateRelated = "update object_property set value = :value "
                            + "where related_oid = :oid and related_property_type = :property_type and colindex = :colindex";
                    params = params("oid", oid, "property_type", propertyType, "colindex", colIndex, "value",
                            storedValue);
                    logStatement(updateRelated, params);
                    int relatedUpdateCnt = template.update(updateRelated, params);
                    if (LOGGER.isLoggable(Level.FINER)) {
                        LOGGER.finer("Updated " + relatedUpdateCnt + " back pointer properties to "
                                + changedProp.getPropertyName() + " of " + info.getClass().getSimpleName() + "["
                                + info.getId() + "]");
                    }
                }
            }
            if (changedProp.isCollectionProperty()) {
                // delete any remaining collection value that's no longer in the value list
                String sql = "delete from object_property where oid=:oid and property_type=:property_type "
                        + "and colindex > :maxIndex";
                Integer maxIndex = Integer.valueOf(values.size());
                template.update(sql, params("oid", oid, "property_type", propertyType, "maxIndex", maxIndex));
            }
        }
    }

    @Nullable
    public <T extends Info> T getById(final String id, final Class<T> type) {
        Assert.notNull(id, "id");

        Info info = null;
        try {
            final Callable<? extends Info> valueLoader;
            if (CatalogInfo.class.isAssignableFrom(type)) {
                valueLoader = new CatalogLoader(id);
            } else {
                valueLoader = new ConfigLoader(id);
            }

            info = cache.get(id, valueLoader);

        } catch (CacheLoader.InvalidCacheLoadException notFound) {
            return null;
        } catch (ExecutionException e) {
            Throwables.propagate(e.getCause());
        }

        if (info == null) {
            return null;
        }
        if (info instanceof CatalogInfo) {
            info = resolveCatalog((CatalogInfo) info);
        } else if (info instanceof ServiceInfo) {
            resolveTransient((ServiceInfo) info);
        }

        if (type.isAssignableFrom(info.getClass())) {
            // use ModificationProxy only in this case as returned object is cached. saveInternal
            // follows suite checking whether the object being saved is a mod proxy, but that's not
            // mandatory in this implementation and should only be the case when the object was
            // obtained by id
            return ModificationProxy.create(type.cast(info), type);
        }

        return null;
    }

    private <T extends CatalogInfo> T resolveCatalog(final T real) {
        if (real == null) {
            return null;
        }
        CatalogImpl catalog = getCatalog();
        catalog.resolve(real);
        // may the cached value have been serialized and hence lost transient fields? (that's why I
        // don't like having transient fields foreign to the domain model in the catalog config
        // objects)
        resolveTransient(real);

        return real;
    }

    private <T extends CatalogInfo> void resolveTransient(T real) {
        if (null == real) {
            return;
        }
        real = ModificationProxy.unwrap(real);
        if (real instanceof StyleInfoImpl || real instanceof StoreInfoImpl || real instanceof ResourceInfoImpl) {
            OwsUtils.set(real, "catalog", catalog);
        }
        if (real instanceof ResourceInfoImpl) {
            resolveTransient(((ResourceInfoImpl) real).getStore());
        } else if (real instanceof LayerInfo) {
            LayerInfo layer = (LayerInfo) real;
            resolveTransient(layer.getDefaultStyle());
            if (!layer.getStyles().isEmpty()) {
                for (StyleInfo s : layer.getStyles()) {
                    resolveTransient(s);
                }
            }
            resolveTransient(layer.getResource());
        } else if (real instanceof LayerGroupInfo) {
            for (PublishedInfo p : ((LayerGroupInfo) real).getLayers()) {
                resolveTransient(p);
            }
            for (StyleInfo s : ((LayerGroupInfo) real).getStyles()) {
                resolveTransient(s);
            }
        }
    }

    private <T extends ServiceInfo> void resolveTransient(T real) {
        real = ModificationProxy.unwrap(real);
        OwsUtils.resolveCollections(real);
        real.setGeoServer(getGeoServer());
    }

    /**
     * @param type
     * @return immutable list of results
     */
    public <T extends Info> List<T> getAll(final Class<T> clazz) {

        Map<String, ?> params = params("types", typesParam(clazz));

        final String sql = "select id from object where type_id in ( :types ) order by id";

        List<String> ids = template.queryForList(sql, params, String.class);

        List<T> transformed = Lists.transform(ids, new Function<String, T>() {
            @Nullable
            @Override
            public T apply(String input) {
                return getById(input, clazz);
            }
        });
        Iterable<T> filtered = Iterables.filter(transformed, com.google.common.base.Predicates.notNull());
        return ImmutableList.copyOf(filtered);
    }

    private <T extends Info> List<Integer> typesParam(final Class<T> clazz) {

        final Class<?>[] actualTypes;

        actualTypes = ClassMappings.fromInterface(clazz).concreteInterfaces();

        List<Integer> inValues = new ArrayList<Integer>(actualTypes.length);
        for (Class<?> type : actualTypes) {
            inValues.add(this.dbMappings.getTypeId(type));
        }

        return inValues;
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void setDefault(final String key, @Nullable final String id) {
        String sql;
        sql = "DELETE FROM DEFAULT_OBJECT WHERE DEF_KEY = :key";
        template.update(sql, params("key", key));
        if (id != null) {
            sql = "INSERT INTO DEFAULT_OBJECT (DEF_KEY, ID) VALUES(:key, :id)";
            template.update(sql, params("key", key, "id", id));
        }
    }

    public void dispose() {
        cache.invalidateAll();
        cache.cleanUp();
    }

    private final class CatalogLoader implements Callable<CatalogInfo> {

        private final String id;

        public CatalogLoader(final String id) {
            this.id = id;
        }

        @Override
        public CatalogInfo call() throws Exception {
            CatalogInfo info;
            try {
                String sql = "select blob from object where id = :id";
                Map<String, String> params = ImmutableMap.of("id", id);
                logStatement(sql, params);
                info = template.queryForObject(sql, params, catalogRowMapper);
            } catch (EmptyResultDataAccessException noSuchObject) {
                return null;
            }
            return info;
        }
    }

    private final class ConfigLoader implements Callable<Info> {

        private final String id;

        public ConfigLoader(final String id) {
            this.id = id;
        }

        @Override
        public Info call() throws Exception {
            Info info;
            try {
                info = template.queryForObject("select blob from object where id = :id", ImmutableMap.of("id", id),
                        configRowMapper);
            } catch (EmptyResultDataAccessException noSuchObject) {
                return null;
            }
            OwsUtils.resolveCollections(info);
            if (info instanceof GeoServerInfo) {

                GeoServerInfoImpl global = (GeoServerInfoImpl) info;
                if (global.getMetadata() == null) {
                    global.setMetadata(new MetadataMap());
                }
                if (global.getClientProperties() == null) {
                    global.setClientProperties(new HashMap<Object, Object>());
                }
                if (global.getCoverageAccess() == null) {
                    global.setCoverageAccess(new CoverageAccessInfoImpl());
                }
                if (global.getJAI() == null) {
                    global.setJAI(new JAIInfoImpl());
                }
            }
            if (info instanceof ServiceInfo) {
                ((ServiceInfo) info).setGeoServer(geoServer);
            }

            return info;
        }
    }

    /**
     * @return whether there exists a property named {@code propertyName} for the given type of
     *         object, and hence native sorting can be done over it.
     */
    public boolean canSort(Class<? extends CatalogInfo> type, String propertyName) {
        Set<PropertyType> propertyTypes = dbMappings.getPropertyTypes(type, propertyName);
        return !propertyTypes.isEmpty();
    }

    void clear(Info info) {
        cache.invalidate(info.getId());
    }

    /**
     * Listens to catalog events clearing cache entires when resources are modified.
     */
    // Copied from org.geoserver.catalog.ResourcePool
    public class CatalogClearingListener extends CatalogVisitorAdapter implements CatalogListener {

        public void handleAddEvent(CatalogAddEvent event) {
        }

        public void handleModifyEvent(CatalogModifyEvent event) {
        }

        public void handlePostModifyEvent(CatalogPostModifyEvent event) {
            event.getSource().accept(this);
        }

        public void handleRemoveEvent(CatalogRemoveEvent event) {
            event.getSource().accept(this);
        }

        public void reloaded() {
        }

        @Override
        public void visit(DataStoreInfo dataStore) {
            clear(dataStore);
        }

        @Override
        public void visit(CoverageStoreInfo coverageStore) {
            clear(coverageStore);
        }

        @Override
        public void visit(FeatureTypeInfo featureType) {
            clear(featureType);
        }

        @Override
        public void visit(WMSStoreInfo wmsStore) {
            clear(wmsStore);
        }

        @Override
        public void visit(StyleInfo style) {
            clear(style);
        }

        @Override
        public void visit(WorkspaceInfo workspace) {
            clear(workspace);
        }

        @Override
        public void visit(NamespaceInfo workspace) {
            clear(workspace);
        }

        @Override
        public void visit(CoverageInfo coverage) {
            clear(coverage);
        }

        @Override
        public void visit(LayerInfo layer) {
            clear(layer);
        }

        @Override
        public void visit(LayerGroupInfo layerGroup) {
            clear(layerGroup);
        }

        @Override
        public void visit(WMSLayerInfo wmsLayerInfoImpl) {
            clear(wmsLayerInfoImpl);
        }

    }

    /**
     * Listens to configuration events clearing cache entires when resources are modified.
     */
    public class ConfigClearingListener extends ConfigurationListenerAdapter {

        @Override
        public void handlePostGlobalChange(GeoServerInfo global) {
            clear(global);
        }

        @Override
        public void handleSettingsPostModified(SettingsInfo settings) {
            clear(settings);
        }

        @Override
        public void handleSettingsRemoved(SettingsInfo settings) {
            clear(settings);
        }

        @Override
        public void handlePostLoggingChange(LoggingInfo logging) {
            clear(logging);
        }

        @Override
        public void handlePostServiceChange(ServiceInfo service) {
            clear(service);
        }

        @Override
        public void handleServiceRemove(ServiceInfo service) {
            clear(service);
        }
    }

}