/* (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);
}
}
}