/* (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.*;
import static org.geoserver.jdbcconfig.internal.DbUtils.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.geoserver.catalog.Info;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.catalog.Predicates;
import org.geoserver.catalog.PublishedInfo;
import org.geoserver.catalog.impl.ClassMappings;
import org.geoserver.ows.util.ClassProperties;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.util.logging.Logging;
import org.opengis.filter.FilterFactory;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.google.common.io.Resources;
public class DbMappings {
private static final Logger LOGGER = Logging.getLogger(DbMappings.class);
private final Dialect dialect;
private BiMap<Integer, Class<?>> types;
private BiMap<Class<?>, Integer> typeIds;
/**
* Per type oid property types. Keys are {@link #getTypeId(Class) type ids}, values are a map of
* property name to property type for that type of object.
*/
private Map<Integer, Map<String, PropertyType>> propertyTypes;
@SuppressWarnings("unchecked")
private static final Set<Class<? extends Serializable>> INDEXABLE_TYPES = ImmutableSet.of(//
String.class, //
Boolean.class,//
Number.class, //
BigInteger.class, //
BigDecimal.class, //
Byte.class, //
Short.class, //
Integer.class, //
Long.class, //
Float.class, //
Double.class //
);
public DbMappings(Dialect dialect) {
this.dialect = dialect;
}
public Integer getTypeId(Class<?> type) {
Integer typeId = typeIds.get(type);
return typeId;
}
public Class<?> getType(Integer typeId) {
return types.get(typeId);
}
/**
* @param template
*/
public void initDb(final NamedParameterJdbcOperations template) {
LOGGER.fine("Initializing Catalog and Config database");
ClassMappings[] classMsappings = ClassMappings.values();
{
BiMap<Integer, Class<?>> existingTypes = loadTypes(template);
for (ClassMappings cm : classMsappings) {
Class<? extends Info> clazz = cm.getInterface();
if (!existingTypes.containsValue(clazz)) {
createType(clazz, template);
}
}
this.types = loadTypes(template);
this.typeIds = this.types.inverse();
}
this.propertyTypes = loadPropertyTypes(template);
// create all direct property types for which we don't need a special mapping entry on
// nested_properties.properties. Need to do this before adding nested properties for
// relationships to be found
for (ClassMappings cm : classMsappings) {
Class<? extends Info> clazz = cm.getInterface();
addDirectPropertyTypes(clazz, template);
}
// create all nested and/or collection properties, both self and related to other objects,
// as defined nested_properties.properties
final Multimap<Class<?>, PropertyTypeDef> nestedPropertyTypeDefs = loadNestedPropertyTypeDefs();
for (ClassMappings cm : classMsappings) {
Class<? extends Info> clazz = cm.getInterface();
Collection<PropertyTypeDef> nestedPropDefs = nestedPropertyTypeDefs.get(clazz);
if (!nestedPropDefs.isEmpty()) {
addNestedPropertyTypes(template, nestedPropDefs);
}
}
this.propertyTypes = ImmutableMap.copyOf(this.propertyTypes);
}
private static class PropertyTypeDef {
final Class<?> propertyOf;
final String propertyName;
@Nullable
final Class<?> targetPropertyOf;
@Nullable
final String targetPropertyName;
@Nullable
final boolean isCollection;
@Nullable
final Boolean isText;
public PropertyTypeDef(Class<?> propertyOf, String propertyName,
@Nullable Class<?> targetPropertyOf, @Nullable String targetPropertyName,
boolean isCollection, Boolean isText) {
this.propertyOf = propertyOf;
this.propertyName = propertyName;
this.targetPropertyOf = targetPropertyOf;
this.targetPropertyName = targetPropertyName;
this.isCollection = isCollection;
this.isText = isText;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}
/**
*
*/
private Multimap<Class<?>, PropertyTypeDef> loadNestedPropertyTypeDefs() {
Properties properties = loadTypeDefsFromResource();
Multimap<Class<?>, PropertyTypeDef> byTypePropDefs = ArrayListMultimap.create();
for (String classPropName : properties.stringPropertyNames()) {
final String propertyName;
final Class<?> objectType;
final boolean collectionProperty;
final Class<?> targetObjectType;
final String targetPropertyName;
final Boolean textProperty;
{
int classNameSeparatorIndex = classPropName.indexOf('.');
String simpleClassName = classPropName.substring(0, classNameSeparatorIndex);
objectType = toClass(simpleClassName);
propertyName = classPropName.substring(1 + classNameSeparatorIndex);
final String propertySpec = properties.getProperty(classPropName);
String[] propTarget = propertySpec.split(":");
String targetClassPropName = propTarget.length > 0 ? propTarget[0] : null;
if (targetClassPropName.trim().length() == 0) {
targetObjectType = null;
targetPropertyName = null;
} else {
classNameSeparatorIndex = targetClassPropName.indexOf('.');
simpleClassName = targetClassPropName.substring(0, classNameSeparatorIndex);
targetObjectType = toClass(simpleClassName);
targetPropertyName = targetClassPropName.substring(1 + classNameSeparatorIndex);
}
String colType = propTarget.length > 1 ? propTarget[1] : null;
String textType = propTarget.length > 1 ? (propTarget.length > 2 ? propTarget[2]
: propTarget[1]) : null;
collectionProperty = "list".equalsIgnoreCase(colType)
|| "set".equalsIgnoreCase(colType);
if ("text".equalsIgnoreCase(textType)) {
textProperty = Boolean.TRUE;
} else {
textProperty = null;
}
}
PropertyTypeDef ptd = new PropertyTypeDef(objectType, propertyName, targetObjectType,
targetPropertyName, collectionProperty, textProperty);
byTypePropDefs.put(objectType, ptd);
}
return byTypePropDefs;
}
private Properties loadTypeDefsFromResource() {
Properties properties = new Properties();
try {
final String resourceName = "nested_properties.properties";
URL resource = Resources.getResource(getClass(), resourceName);
InputStream in = resource.openStream();
try {
properties.load(in);
} finally {
Closeables.close(in, true);
}
} catch (IOException e) {
throw Throwables.propagate(e);
}
return properties;
}
/**
* @param simpleClassName
*
*/
private Class<?> toClass(String simpleClassName) {
for (Class<?> c : this.typeIds.keySet()) {
if (simpleClassName.equalsIgnoreCase(c.getSimpleName())) {
return c;
}
}
throw new IllegalArgumentException("Unknown type: '" + simpleClassName + "'");
}
private Map<Integer, Map<String, PropertyType>> loadPropertyTypes(
NamedParameterJdbcOperations template) {
final String query = "select oid, target_property, type_id, name, collection, text from property_type";
RowMapper<PropertyType> rowMapper = new RowMapper<PropertyType>() {
@Override
public PropertyType mapRow(ResultSet rs, int rowNum) throws SQLException {
Integer oid = rs.getInt(1);
// cannot use getInteger and we might get BigDecimal or Integer
Number targetPropertyOid = (Number) rs.getObject(2);
Integer objectTypeOid = rs.getInt(3);
String propertyName = rs.getString(4);
Boolean collectionProperty = rs.getBoolean(5);
Boolean textProperty = rs.getBoolean(6);
if (targetPropertyOid != null) {
targetPropertyOid = targetPropertyOid.intValue();
}
PropertyType pt = new PropertyType(oid, (Integer) targetPropertyOid, objectTypeOid,
propertyName, collectionProperty, textProperty);
return pt;
}
};
final List<PropertyType> propertyTypes;
{
final Map<String, ?> params = Collections.emptyMap();
propertyTypes = template.query(query, params, rowMapper);
}
Map<Integer, Map<String, PropertyType>> perTypeProps = Maps.newHashMap();
for (PropertyType pt : propertyTypes) {
Integer objectType = pt.getObjectTypeOid();
Map<String, PropertyType> typeProperties = perTypeProps.get(objectType);
if (typeProperties == null) {
typeProperties = Maps.newHashMap();
perTypeProps.put(objectType, typeProperties);
}
typeProperties.put(pt.getPropertyName(), pt);
}
return perTypeProps;
}
private BiMap<Integer, Class<?>> loadTypes(NamedParameterJdbcOperations template) {
String sql = "select oid, typename from type";
SqlRowSet rowSet = template.queryForRowSet(sql, params("", ""));
BiMap<Integer, Class<?>> types = HashBiMap.create();
if (rowSet.first()) {
do {
Number oid = (Number) rowSet.getObject(1);
String typeName = rowSet.getString(2);
Class<?> clazz;
try {
clazz = Class.forName(typeName);
} catch (ClassNotFoundException e) {
throw Throwables.propagate(e);
}
types.put(oid.intValue(), clazz);
} while (rowSet.next());
}
return types;
}
private void createType(Class<? extends Info> clazz, NamedParameterJdbcOperations template) {
final String typeName = clazz.getName();
String sql = String.format("insert into type (typename, oid) values (:typeName, %s)",
dialect.nextVal("seq_TYPE"));
int update = template.update(sql, params("typeName", typeName));
if (1 == update) {
log("created type " + typeName);
}
}
private void addDirectPropertyTypes(final Class<? extends Info> clazz,
final NamedParameterJdbcOperations template) {
log("Creating property mappings for " + clazz.getName());
final ClassProperties classProperties = new ClassProperties(clazz);
List<String> properties = Lists.newArrayList(classProperties.properties());
Collections.sort(properties);
for (String propertyName : properties) {
propertyName = fixCase(propertyName);
Method getter = classProperties.getter(propertyName, null);
if (getter == null) {
continue;
}
Class<?> returnType = getter.getReturnType();
if (returnType.isPrimitive() || returnType.isEnum()
|| INDEXABLE_TYPES.contains(returnType)) {
final Class<?> componentType = returnType.isArray() ? returnType.getComponentType()
: returnType;
boolean isText = componentType.isEnum()
|| CharSequence.class.isAssignableFrom(componentType);
isText &= !"id".equals(propertyName);// id is not on the full text search list of
// properties
addPropertyType(template, clazz, propertyName, null, false, isText);
} else {
log("Ignoring property " + propertyName + ":" + returnType.getSimpleName());
}
}
log("----------------------");
}
/**
* @param clazz
* @param nestedPropDefs
*/
private void addNestedPropertyTypes(final NamedParameterJdbcOperations template,
Collection<PropertyTypeDef> nestedPropDefs) {
for (PropertyTypeDef ptd : nestedPropDefs) {
final Class<?> propertyOf = ptd.propertyOf;
final String propertyName = ptd.propertyName;
final boolean isCollection = ptd.isCollection;
final Class<?> targetPropertyOf = ptd.targetPropertyOf;
final String targetPropertyName = ptd.targetPropertyName;
final Boolean isText = ptd.isText;
PropertyType targetPropertyType = null;
if (targetPropertyOf != null) {
final Integer targetPropId = getTypeId(targetPropertyOf);
checkState(
null != targetPropId,
Joiner.on("").join("Property ", propertyOf.getName(), ".", propertyName,
" references property ", targetPropertyOf.getName(), ".",
targetPropertyName, " but target property typ does not exist"));
Map<String, PropertyType> targetPropertyTypes;
targetPropertyTypes = this.propertyTypes.get(targetPropId);
checkState(targetPropertyTypes != null, "PropertyTypes of target type "
+ targetPropertyOf.getName() + " not found while adding property "
+ propertyName + " of " + propertyOf.getName());
targetPropertyType = targetPropertyTypes.get(targetPropertyName);
checkState(targetPropertyType != null);
}
boolean text = isText == null ? false : isText.booleanValue();
addPropertyType(template, propertyOf, propertyName, targetPropertyType, isCollection,
text);
}
}
public PropertyType getPropertyType(Integer propId) {
for (Entry<Integer, Map<String, PropertyType>> e : this.propertyTypes.entrySet()) {
for (PropertyType pt : e.getValue().values()) {
if (pt.getOid().equals(propId)) {
return pt;
}
}
}
throw new IllegalArgumentException("PropertyType not found: " + propId);
}
/**
* @param infoClazz
* @param template
* @param propertyName
* @param targetProperty
* @param isCollection
* @return the newly added property type, or {@code null} if it was not added to the database
* (i.e. already exists)
*/
private PropertyType addPropertyType(final NamedParameterJdbcOperations template,
final Class<?> infoClazz, final String propertyName,
@Nullable final PropertyType targetProperty, final boolean isCollection,
final boolean isText) {
checkNotNull(template);
checkNotNull(infoClazz);
checkNotNull(propertyName);
final Integer typeId = getTypeId(infoClazz);
if (null == typeId) {
throw new IllegalStateException("Unknown type id for " + infoClazz.getName());
}
Map<String, ?> params;
log("Checking for ", propertyName);
String query = "select count(*) from property_type "//
+ "where type_id = :objectType and name = :propName";
params = params("objectType", typeId, "propName", propertyName);
logStatement(query, params);
final int exists = template.queryForObject(query, params, Integer.class);
PropertyType pType;
if (exists == 0) {
log("Adding ", propertyName);
Integer targetPropertyOid = targetProperty == null ? null : targetProperty.getOid();
String insert = String.format("insert into property_type (oid, target_property, type_id, name, collection, text) "
+ "values (%s, :target, :type, :name, :collection, :isText)",
dialect.nextVal("seq_PROPERTY_TYPE"));
params = params("target", targetPropertyOid, "type", typeId, "name", propertyName,
"collection", isCollection, "isText", isText);
logStatement(insert, params);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(insert, new MapSqlParameterSource(params), keyHolder, new String[] {"oid"});
// looks like some db's return the pk different than others, so lets try both ways
Number pTypeKey = (Number) keyHolder.getKeys().get("oid");
if (pTypeKey == null) {
pTypeKey = keyHolder.getKey();
}
pType = new PropertyType(pTypeKey.intValue(), targetPropertyOid, typeId, propertyName,
isCollection, isText);
} else {
log("Not adding property type ", infoClazz.getSimpleName(), ".", propertyName,
" as it already exists");
pType = null;
}
if (pType != null) {
Map<String, PropertyType> map = this.propertyTypes.get(typeId);
if (map == null) {
map = Maps.newHashMap();
this.propertyTypes.put(typeId, map);
}
map.put(pType.getPropertyName(), pType);
}
return pType;
}
/**
* @param propertyName
*
*/
private String fixCase(String propertyName) {
if (propertyName.length() > 1) {
char first = propertyName.charAt(0);
char second = propertyName.charAt(1);
if (!Character.isUpperCase(second)) {
propertyName = Character.toLowerCase(first) + propertyName.substring(1);
}
}
return propertyName;
}
private void log(String... msg) {
String message = Joiner.on("").join(msg).toString();
// System.err.println(message);
LOGGER.finer(message);
}
/**
* @param queryType
*
*/
@SuppressWarnings("unchecked")
public List<Integer> getConcreteQueryTypes(Class<?> queryType) {
ClassMappings mappings = ClassMappings.fromInterface((Class<? extends Info>) queryType);
Class<? extends Info>[] concreteInterfaces = mappings.concreteInterfaces();
List<Integer> inValues = new ArrayList<Integer>(concreteInterfaces.length);
for (Class<?> type : concreteInterfaces) {
Integer typeId = getTypeId(type);
inValues.add(typeId);
}
return inValues;
}
@SuppressWarnings("unchecked")
public Set<PropertyType> getPropertyTypes(final Class<?> queryType, String propertyName) {
checkArgument(queryType.isInterface(), "queryType should be an interface");
propertyName = removeIndexes(propertyName);
Set<PropertyType> matches = Sets.newHashSet();
ClassMappings classMappings;
classMappings = ClassMappings.fromInterface((Class<? extends Info>) queryType);
checkState(classMappings != null, "ClassMappings not found for " + queryType);
Class<? extends Info>[] concreteInterfaces = classMappings.concreteInterfaces();
for (Class<? extends Info> concreteType : concreteInterfaces) {
Map<String, PropertyType> propTypes = getPropertyTypes(concreteType);
if (null == propTypes) {
continue;
}
if (Predicates.ANY_TEXT.getPropertyName().equals(propertyName)) {
for (PropertyType propertyType : propTypes.values()) {
if (propertyType.isText()) {
matches.add(propertyType);
}
}
} else {
PropertyType propertyType = propTypes.get(propertyName);
if (null != propertyType) {
matches.add(propertyType);
}
}
}
return matches;
}
public Set<Integer> getPropertyTypeIds(Class<?> targetQueryType, String targetPropertyName) {
Set<PropertyType> propertyTypes = getPropertyTypes(targetQueryType, targetPropertyName);
Set<Integer> concretePropertyTypeIds = new TreeSet<Integer>();
for (PropertyType pt : propertyTypes) {
concretePropertyTypeIds.add(pt.getOid());
}
return concretePropertyTypeIds;
}
public Map<String, PropertyType> getPropertyTypes(final Class<?> queryType) {
checkArgument(queryType.isInterface(), "queryType should be an interface");
final Integer typeId = getTypeId(queryType);
Map<String, PropertyType> propTypes = this.propertyTypes.get(typeId);
return propTypes;
}
/**
* @param info
*
*/
public Iterable<Property> properties(Info object) {
checkArgument(!(object instanceof Proxy));
final ClassMappings classMappings = ClassMappings.fromImpl(object.getClass());
checkNotNull(classMappings);
return properties(object, classMappings);
}
public Iterable<Property> changedProperties(Info oldObject, Info object) {
checkArgument(!(oldObject instanceof Proxy));
final ClassMappings classMappings = ClassMappings.fromImpl(oldObject.getClass());
checkNotNull(classMappings);
ImmutableSet<Property> oldProperties = properties(oldObject, classMappings);
ImmutableSet<Property> newProperties = properties(object, classMappings);
Set<Property> changedProps = Sets.difference(newProperties, oldProperties);
return changedProps;
}
private ImmutableSet<Property> properties(Info object, final ClassMappings classMappings) {
final Class<? extends Info> type = classMappings.getInterface();
final Integer typeId = getTypeId(type);
final FilterFactory ff = CommonFactoryFinder.getFilterFactory();
final ImmutableList<PropertyType> typeProperties = getTypeProperties(typeId);
ImmutableSet.Builder<Property> builder = ImmutableSet.builder();
for (PropertyType pt : typeProperties) {
String propertyName = pt.getPropertyName();
Object value;
if (object instanceof NamespaceInfo && "name".equalsIgnoreCase(propertyName)) {
// HACK for derived property, ModificationProxy evaluates it to the old value
value = ((NamespaceInfo) object).getPrefix();
} else if (object instanceof LayerInfo && "name".equalsIgnoreCase(propertyName)) {
// HACK for derived property, ModificationProxy evaluates it to old value. Remove
// when layer name is decoupled from resource name
value = ((LayerInfo) object).getResource().getName();
} else if (object instanceof LayerInfo && "title".equalsIgnoreCase(propertyName)) {
// HACK for derived property, ModificationProxy evaluates it to old value. Remove
// when layer name is decoupled from resource name
value = ((LayerInfo) object).getResource().getTitle();
} else if (object instanceof PublishedInfo
&& "prefixedName".equalsIgnoreCase(propertyName)) {
// HACK for derived property, it is not a regular javabean property
value = ((PublishedInfo) object).prefixedName();
} else {
// proceed as it should
value = ff.property(propertyName).evaluate(object);
}
Property prop = new Property(pt, value);
builder.add(prop);
}
return builder.build();
}
/**
* @param typeId
*
*/
private ImmutableList<PropertyType> getTypeProperties(Integer typeId) {
Map<String, PropertyType> properties = this.propertyTypes.get(typeId);
return ImmutableList.copyOf(properties.values());
}
private String removeIndexes(String propName) {
int idx;
while ((idx = propName.indexOf('[')) > 0) {
String pre = propName.substring(0, idx);
int closeIdx = propName.indexOf(']');
String post = propName.substring(1 + closeIdx);
propName = pre + post;
}
return propName;
}
}