/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2013, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.db.postgres;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.sis.internal.feature.AttributeConvention;
import org.geotoolkit.db.JDBCFeatureStoreUtilities;
import org.geotoolkit.db.dialect.SQLDialect;
import org.geotoolkit.version.AbstractVersionControl;
import org.geotoolkit.version.Version;
import org.geotoolkit.version.VersioningException;
import org.opengis.feature.Attribute;
import org.opengis.feature.AttributeType;
import org.opengis.feature.FeatureAssociationRole;
import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyType;
/**
* Manage versioning for a given feature type.
*
* @author Johann Sorel (Geomatys)
*/
public class PostgresVersionControl extends AbstractVersionControl{
private final PostgresFeatureStore featureStore;
private final FeatureType featureType;
private final SQLDialect dialect;
private Boolean isVersioned = null;
public PostgresVersionControl(PostgresFeatureStore featureStore, FeatureType featureType) {
this.featureStore = featureStore;
this.featureType = featureType;
this.dialect = featureStore.getDialect();
}
@Override
public synchronized boolean isEditable() {
return true;
}
@Override
public synchronized void startVersioning() throws VersioningException {
if(isVersioned()){
//versioning already active, do nothing
return;
}
//install history functions, won't do anything if already present
featureStore.installHSFunctions();
final String schemaName = featureStore.getDatabaseSchema();
final Set<FeatureType> visited = new HashSet<>();
createVersioningTable(schemaName, featureType, visited);
//clear cache
isVersioned = null;
}
/**
* Create versioning table for given table.
*
* @param schemaName
* @param tableName
* @param visited set of already visited types, there might be recursion
* or multiple properties with the same type.
*/
private void createVersioningTable(final String schemaName, final FeatureType type, final Set<FeatureType> visited) throws VersioningException{
if(visited.contains(type)) return;
visited.add(type);
final String tableName = type.getName().tip().toString();
final StringBuilder sb = new StringBuilder("SELECT \"HS_CreateHistory\"(");
sb.append('\'');
if(schemaName!=null && !schemaName.isEmpty()){
sb.append(schemaName).append('.');
}
sb.append(tableName);
sb.append('\'');
sb.append(',');
final List<String> hsColumnNames = new ArrayList<>();
for(PropertyType desc : type.getProperties(true)){
if(AttributeConvention.contains(desc.getName())) continue;
if(desc instanceof FeatureAssociationRole){
//complex type, create sub table history
FeatureAssociationRole far = (FeatureAssociationRole) desc;
createVersioningTable(schemaName, far.getValueType(), visited);
}else if(desc instanceof AttributeType) {
hsColumnNames.add("'"+desc.getName().tip().toString()+"'");
}
}
Connection cnx = null;
Statement stmt = null;
try{
cnx = featureStore.getDataSource().getConnection();
stmt = cnx.createStatement();
sb.append("array[");
for(int i=0,n=hsColumnNames.size();i<n;i++){
if(i!=0) sb.append(',');
sb.append(hsColumnNames.get(i));
}
sb.append("]);");
stmt.executeQuery(sb.toString());
}catch(SQLException ex){
throw new VersioningException(ex.getMessage(), ex);
}finally{
JDBCFeatureStoreUtilities.closeSafe(featureStore.getLogger(), cnx, stmt, null);
}
}
@Override
public synchronized void dropVersioning() throws VersioningException {
if(!isVersioned()){
//versioning not active, do nothing
return;
}
//install history functions, won't do anything if already present
featureStore.installHSFunctions();
final String schemaName = featureStore.getDatabaseSchema();
final Set<FeatureType> visited = new HashSet<>();
dropVersioning(schemaName, featureType, visited);
//clear cache
isVersioned = null;
}
private void dropVersioning(final String schemaName, final FeatureType type, final Set<FeatureType> visited) throws VersioningException{
if(visited.contains(type)) return;
visited.add(type);
final String tableName = type.getName().tip().toString();
//drop complex properties versioning
for(PropertyType desc : type.getProperties(true)){
if(desc instanceof FeatureAssociationRole){
//complex type, drop sub table history
FeatureAssociationRole far = (FeatureAssociationRole) desc;
dropVersioning(schemaName, far.getValueType(),visited);
}
}
final StringBuilder sb = new StringBuilder("SELECT \"HS_DropHistory\"(");
sb.append('\'');
if(schemaName!=null && !schemaName.isEmpty()){
sb.append(schemaName).append('.');
}
sb.append(tableName);
sb.append('\'');
sb.append(");");
Connection cnx = null;
Statement stmt = null;
try{
cnx = featureStore.getDataSource().getConnection();
stmt = cnx.createStatement();
stmt.executeQuery(sb.toString());
}catch(SQLException ex){
throw new VersioningException(ex.getMessage(), ex);
}finally{
JDBCFeatureStoreUtilities.closeSafe(featureStore.getLogger(), cnx, stmt, null);
}
}
@Override
public synchronized void trim(Version version) throws VersioningException {
trim(version.getDate());
}
@Override
public void trim(Date date) throws VersioningException {
if(!isVersioned()){
return ;
}
final String schemaName = featureStore.getDatabaseSchema();
final Set<FeatureType> visited = new HashSet<>();
trim(schemaName, featureType, date, visited);
}
private void trim(final String schemaName, final FeatureType type, final Date date, final Set<FeatureType> visited) throws VersioningException{
if(visited.contains(type)) return;
visited.add(type);
final String tableName = type.getName().tip().toString();
//trim complex properties versioning
for(PropertyType desc : type.getProperties(true)){
if(desc instanceof FeatureAssociationRole){
//complex type, trim sub table history
FeatureAssociationRole far = (FeatureAssociationRole) desc;
trim(schemaName, far.getValueType(), date, visited);
}
}
Connection cnx = null;
Statement stmt = null;
ResultSet rs = null;
try{
cnx = featureStore.getDataSource().getConnection();
stmt = cnx.createStatement();
final StringBuilder sb = new StringBuilder("SELECT \"HSX_TrimHistory\"(");
sb.append('\'');
if(schemaName != null && !schemaName.isEmpty()){
sb.append(schemaName).append('.');
}
sb.append(tableName);
sb.append('\'');
sb.append(", TIMESTAMP '");
sb.append(new Timestamp(date.getTime()).toString());
sb.append("');");
rs = stmt.executeQuery(sb.toString());
}catch(SQLException ex){
throw new VersioningException(ex.getMessage(), ex);
}finally{
JDBCFeatureStoreUtilities.closeSafe(featureStore.getLogger(), cnx, stmt, null);
}
}
@Override
public synchronized void revert(Version version) throws VersioningException {
revert(version.getDate());
}
@Override
public void revert(final Date date) throws VersioningException {
if(!isVersioned()){
return ;
}
final String schemaName = featureStore.getDatabaseSchema();
final Set<FeatureType> visited = new HashSet<>();
revert(schemaName, featureType, date, visited);
}
private void revert(final String schemaName, final FeatureType type, final Date date, final Set<FeatureType> visited) throws VersioningException{
if(visited.contains(type)) return;
visited.add(type);
final String tableName = type.getName().tip().toString();
//revert complex properties versioning
for(PropertyType desc : type.getProperties(true)){
if(desc instanceof FeatureAssociationRole){
//complex type, revert sub table history
FeatureAssociationRole far = (FeatureAssociationRole) desc;
revert(schemaName, far.getValueType(), date, visited);
}
}
Connection cnx = null;
Statement stmt = null;
ResultSet rs = null;
try{
cnx = featureStore.getDataSource().getConnection();
stmt = cnx.createStatement();
final StringBuilder sb = new StringBuilder("SELECT \"HSX_RevertHistory\"(");
sb.append('\'');
if(schemaName != null && !schemaName.isEmpty()){
sb.append(schemaName).append('.');
}
sb.append(tableName);
sb.append('\'');
sb.append(", TIMESTAMP '");
sb.append(new Timestamp(date.getTime()).toString());
sb.append("');");
rs = stmt.executeQuery(sb.toString());
}catch(SQLException ex){
throw new VersioningException(ex.getMessage(), ex);
}finally{
JDBCFeatureStoreUtilities.closeSafe(featureStore.getLogger(), cnx, stmt, null);
}
}
@Override
public synchronized List<Version> list() throws VersioningException {
if(!isVersioned()){
return Collections.EMPTY_LIST;
}
final List<Version> versions = new ArrayList<Version>();
final String schemaName = featureStore.getDatabaseSchema();
final String tableName = getHSTableName();
Connection cnx = null;
Statement stmt = null;
ResultSet rs = null;
try{
cnx = featureStore.getDataSource().getConnection();
stmt = cnx.createStatement();
final StringBuilder sb = new StringBuilder("SELECT distinct(sub.date) as date FROM (");
sb.append("SELECT \"HS_Begin\" AS date from ");
dialect.encodeSchemaAndTableName(sb, schemaName, tableName);
sb.append(" UNION ");
sb.append("SELECT \"HS_End\" AS date from ");
dialect.encodeSchemaAndTableName(sb, schemaName, tableName);
sb.append(" WHERE \"HS_End\" IS NOT NULL");
sb.append(") AS sub ORDER BY date ASC");
rs = stmt.executeQuery(sb.toString());
while(rs.next()){
final Timestamp ts = rs.getTimestamp(1);
final Version v = new Version(this, ts.toString(), ts);
versions.add(v);
}
}catch(SQLException ex){
throw new VersioningException(ex.getMessage(), ex);
}finally{
JDBCFeatureStoreUtilities.closeSafe(featureStore.getLogger(), cnx, stmt, null);
}
return versions;
}
@Override
public synchronized boolean isVersioned() throws VersioningException {
boolean hasHS = featureStore.hasHSFunctions();
if(!hasHS) return false;
if(isVersioned!=null) return isVersioned;
//search for the versioning table
final String schemaName = featureStore.getDatabaseSchema();
final String tableName = getHSTableName();
Connection cnx = null;
Statement stmt = null;
ResultSet rs = null;
try{
cnx = featureStore.getDataSource().getConnection();
stmt = cnx.createStatement();
final StringBuilder sb = new StringBuilder("SELECT count(*) from \"information_schema\".\"tables\" WHERE ");
sb.append("\"table_schema\"=");
dialect.encodeValue(sb, schemaName, String.class);
sb.append(" AND \"table_name\"=");
dialect.encodeValue(sb, tableName, String.class);
rs = stmt.executeQuery(sb.toString());
rs.next();
final int nb = rs.getInt(1);
isVersioned = nb>0;
}catch(SQLException ex){
throw new VersioningException(ex.getMessage(), ex);
}finally{
JDBCFeatureStoreUtilities.closeSafe(featureStore.getLogger(), cnx, stmt, null);
}
return isVersioned;
}
/**
* Get the history derivated table name.
* @return String
*/
public String getHSTableName(){
return "HS_TBL_"+featureType.getName().tip().toString();
}
}