GenericHierarchicalStorage.java :  » J2EE » fleXive » com » flexive » core » storage » genericSQL » Java Open Source

Java Open Source » J2EE » fleXive 
fleXive » com » flexive » core » storage » genericSQL » GenericHierarchicalStorage.java
/***************************************************************
 *  This file is part of the [fleXive](R) project.
 *
 *  Copyright (c) 1999-2008
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) project is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation;
 *  either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *  A copy is found in the textfile GPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  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 General Public License for more details.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.core.storage.genericSQL;

import com.flexive.core.DatabaseConst;
import static com.flexive.core.DatabaseConst.*;
import com.flexive.core.LifeCycleInfoImpl;
import com.flexive.core.storage.ContentStorage;
import com.flexive.core.storage.StorageManager;
import com.flexive.extractor.ExtractedData;
import com.flexive.extractor.HtmlExtractor;
import com.flexive.shared.*;
import com.flexive.shared.content.*;
import com.flexive.shared.exceptions.*;
import com.flexive.shared.interfaces.ScriptingEngine;
import com.flexive.shared.interfaces.SequencerEngine;
import com.flexive.shared.media.FxMediaEngine;
import com.flexive.shared.scripting.FxScriptBinding;
import com.flexive.shared.scripting.FxScriptEvent;
import com.flexive.shared.scripting.FxScriptResult;
import com.flexive.shared.security.ACL;
import com.flexive.shared.security.LifeCycleInfo;
import com.flexive.shared.security.Mandator;
import com.flexive.shared.stream.BinaryUploadPayload;
import com.flexive.shared.stream.FxStreamUtils;
import com.flexive.shared.structure.*;
import com.flexive.shared.value.*;
import com.flexive.shared.workflow.Step;
import com.flexive.shared.workflow.StepDefinition;
import com.flexive.shared.workflow.Workflow;
import com.flexive.stream.ServerLocation;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.tidy.Tidy;

import java.io.*;
import java.sql.*;
import java.util.*;

/**
 * Generic implementation of hierarchical content handling.
 * Concrete implementation have to derive from this class and
 * provide a singleton hook for the Database class (static getInstance() method)
 *
 * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 */
public abstract class GenericHierarchicalStorage implements ContentStorage {

    /**
     * {@inheritDoc}
     */
    public String getTableName(FxProperty prop) {
        if (prop.isSystemInternal())
            return DatabaseConst.TBL_CONTENT;
        return DatabaseConst.TBL_CONTENT_DATA;
    }

    /**
     * Helper to convert a var array of string to a 'real' string array
     *
     * @param data strings to put into an array
     * @return string array from parameters
     */
    protected static String[] array(String... data) {
        return data;
    }


    private static final transient Log LOG = LogFactory.getLog(GenericHierarchicalStorage.class);

    protected static final String CONTENT_MAIN_INSERT = "INSERT INTO " + TBL_CONTENT +
            // 1  2   3    4   5    6       7        8
            " (ID,VER,TDEF,ACL,STEP,MAX_VER,LIVE_VER,ISMAX_VER," +
            //9         10       11       12        13         14        15         16         17         18
            "ISLIVE_VER,ISACTIVE,MAINLANG,RELSRC_ID,RELSRC_VER,RELDST_ID,RELDST_VER,RELSRC_POS,RELDST_POS,CREATED_BY," +
            //19        20          21          22
            "CREATED_AT,MODIFIED_BY,MODIFIED_AT,MANDATOR,DBIN_ID,DBIN_VER,DBIN_QUALITY,DBIN_ACL)" +
            "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,-1,1,1,1)";

    protected static final String CONTENT_MAIN_UPDATE = "UPDATE " + TBL_CONTENT + " SET " +
            //    1     2      3         4          5           6            7          8          9
            "TDEF=?,ACL=?,STEP=?,MAX_VER=?,LIVE_VER=?,ISMAX_VER=?,ISLIVE_VER=?,ISACTIVE=?,MAINLANG=?," +
            //         10           11          12           13           14           15            16            17
            "RELSRC_ID=?,RELSRC_VER=?,RELDST_ID=?,RELDST_VER=?,RELSRC_POS=?,RELDST_POS=?,MODIFIED_BY=?,MODIFIED_AT=? " +
            //        18        19
            "WHERE ID=? AND VER=?";
    protected static final String CONTENT_MAIN_BINARY_UPDATE = "UPDATE " + TBL_CONTENT + " SET " +
            //       1          2              3          4          5         6
            "DBIN_ID=?,DBIN_VER=?,DBIN_QUALITY=?,DBIN_ACL=? WHERE ID=? AND VER=?";

    //                                                        1  2   3    4   5    6       7        8         9
    protected static final String CONTENT_MAIN_LOAD = "SELECT ID,VER,TDEF,ACL,STEP,MAX_VER,LIVE_VER,ISMAX_VER,ISLIVE_VER," +
            //10      11       12        13         14        15         16         17         18         19
            "ISACTIVE,MAINLANG,RELSRC_ID,RELSRC_VER,RELDST_ID,RELDST_VER,RELSRC_POS,RELDST_POS,CREATED_BY,CREATED_AT," +
            //20         21          22       23      24
            "MODIFIED_BY,MODIFIED_AT,MANDATOR,DBIN_ID,DBIN_ACL FROM " + TBL_CONTENT;

    //                                                  1   2    3      4     5         6     7       8
    protected static final String CONTENT_DATA_LOAD = "SELECT POS,LANG,ASSIGN,XPATH,XPATHMULT,XMULT,ISGROUP,ISMLDEF," +
            //9     10     11    12    13   14      15        16      17     18    19      20
            "FDATE1,FDATE2,FBLOB,FCLOB,FINT,FBIGINT,FTEXT1024,FDOUBLE,FFLOAT,FBOOL,FSELECT,FREF FROM " + TBL_CONTENT_DATA +
            //         1         2
            " WHERE ID=? AND VER=? ORDER BY XDEPTH ASC, " +/*ISGROUP DESC, PARENTXPATH ASC,*/ "POS ASC";

    //                                                                                                       1         2
    protected static final String CONTENT_DATA_REMOVE_VERSION = "DELETE FROM " + TBL_CONTENT_DATA + " WHERE ID=? AND VER=?";
    protected static final String CONTENT_DATA_FT_REMOVE_VERSION = "DELETE FROM " + TBL_CONTENT_DATA_FT + " WHERE ID=? AND VER=?";

    //security info main query
    protected static final String SECURITY_INFO_MAIN = "SELECT DISTINCT c.ACL, t.ACL, s.ACL, t.SECURITY_MODE, t.ID, c.DBIN_ID, c.DBIN_ACL, c.CREATED_BY, c.MANDATOR FROM " +
            TBL_CONTENT + " c, " + TBL_STRUCT_TYPES + " t, " + TBL_STEP + " s WHERE c.ID=? AND ";
    protected static final String SECURITY_INFO_WHERE = " AND t.ID=c.TDEF AND s.ID=c.STEP";
    protected static final String SECURITY_INFO_VER = SECURITY_INFO_MAIN + "c.VER=?" + SECURITY_INFO_WHERE;
    protected static final String SECURITY_INFO_MAXVER = SECURITY_INFO_MAIN + "c.VER=c.MAX_VER" + SECURITY_INFO_WHERE;
    protected static final String SECURITY_INFO_LIVEVER = SECURITY_INFO_MAIN + "c.VER=c.LIVE_VER" + SECURITY_INFO_WHERE;

    //calculate max_ver and live_ver for a content instance
    protected static final String CONTENT_VER_CALC = "SELECT MAX(VER) AS MAX_VER, COALESCE((SELECT s.VER FROM " + TBL_CONTENT + " s WHERE s.STEP=" +
            "(SELECT w.ID FROM " + TBL_STEP + " w WHERE w.STEPDEF=" + StepDefinition.LIVE_STEP_ID + ") AND s.ID=c.ID),-1) AS LIVE_VER FROM " + TBL_CONTENT + " c WHERE c.ID=? " +
            "GROUP BY c.ID";
    protected static final String CONTENT_VER_UPDATE_1 = "UPDATE " + TBL_CONTENT + " SET MAX_VER=?, LIVE_VER=? WHERE ID=?";
    protected static final String CONTENT_VER_UPDATE_2 = "UPDATE " + TBL_CONTENT + " SET ISMAX_VER=(MAX_VER=VER), ISLIVE_VER=(LIVE_VER=VER) WHERE ID=?";
    protected static final String CONTENT_VER_UPDATE_3 = "UPDATE " + TBL_CONTENT_DATA + " SET ISMAX_VER=(VER=?), ISLIVE_VER=(VER=?) WHERE ID=?";

    protected static final String CONTENT_STEP_DEPENDENCIES = "UPDATE " + TBL_CONTENT + " SET STEP=? WHERE STEP=? AND ID=? AND VER<>?";


    protected static final String CONTENT_REFERENCE_LIVE = "SELECT VER, ACL, STEP, TDEF, CREATED_BY FROM " + TBL_CONTENT + " WHERE ID=? AND ISLIVE_VER=TRUE";
    protected static final String CONTENT_REFERENCE_MAX = "SELECT VER, ACL, STEP, TDEF, CREATED_BY FROM " + TBL_CONTENT + " WHERE ID=? AND ISMAX_VER=TRUE";
    protected static final String CONTENT_REFERENCE_CAPTION = "SELECT FTEXT1024 FROM " + TBL_CONTENT_DATA + " WHERE ID=? AND VER=? AND XPATHMULT='/CAPTION[1]'";

    //getContentVersionInfo() statement
    protected static final String CONTENT_VER_INFO = "SELECT ID, VER, MAX_VER, LIVE_VER, CREATED_BY, CREATED_AT, MODIFIED_BY, MODIFIED_AT FROM " + TBL_CONTENT + " WHERE ID=?";

    //security info property acl query
    protected static final String SECURITY_INFO_PROP = "SELECT DISTINCT a.ACL FROM " + TBL_STRUCT_ASSIGNMENTS + " a, " +
            TBL_CONTENT_DATA + " d WHERE d.ID=? AND ";
    protected static final String SECURITY_INFO_PROP_WHERE = " AND a.ID=d.ASSIGN AND d.ISGROUP=FALSE";
    protected static final String SECURITY_INFO_PROP_VER = SECURITY_INFO_PROP + "d.VER=?" + SECURITY_INFO_PROP_WHERE;
    protected static final String SECURITY_INFO_PROP_MAXVER = SECURITY_INFO_PROP + "d.ISMAX_VER=1" + SECURITY_INFO_PROP_WHERE;
    protected static final String SECURITY_INFO_PROP_LIVEVER = SECURITY_INFO_PROP + "d.ISLIVE_VER=1" + SECURITY_INFO_PROP_WHERE;


    protected static final String CONTENT_MAIN_REMOVE = "DELETE FROM " + TBL_CONTENT + " WHERE ID=?";
    protected static final String CONTENT_DATA_REMOVE = "DELETE FROM " + TBL_CONTENT_DATA + " WHERE ID=?";
    protected static final String CONTENT_DATA_FT_REMOVE = "DELETE FROM " + TBL_CONTENT_DATA_FT + " WHERE ID=?";
    protected static final String CONTENT_BINARY_REMOVE = "DELETE FROM " + TBL_CONTENT_BINARY + " WHERE ID IN (SELECT DISTINCT d.FBLOB FROM " + TBL_CONTENT_DATA + " d WHERE d.ID=?)";
    protected static final String SQL_WHERE_VER = " AND VER=?";
    protected static final String CONTENT_MAIN_REMOVE_VER = CONTENT_MAIN_REMOVE + SQL_WHERE_VER;
    protected static final String CONTENT_DATA_REMOVE_VER = CONTENT_DATA_REMOVE + SQL_WHERE_VER;
    protected static final String CONTENT_DATA_FT_REMOVE_VER = CONTENT_DATA_FT_REMOVE + SQL_WHERE_VER;
    protected static final String CONTENT_BINARY_REMOVE_VER = CONTENT_BINARY_REMOVE + SQL_WHERE_VER;
    protected static final String CONTENT_BINARY_TRANSIT_CLEANUP = "DELETE FROM " + TBL_BINARY_TRANSIT + " WHERE EXPIRE<?";

    protected static final String CONTENT_MAIN_REMOVE_TYPE = "DELETE FROM " + TBL_CONTENT + " WHERE TDEF=?";
    protected static final String CONTENT_DATA_REMOVE_TYPE = "DELETE FROM " + TBL_CONTENT_DATA + " WHERE ID IN (SELECT DISTINCT ID FROM " + TBL_CONTENT + " WHERE TDEF=?)";
    protected static final String CONTENT_DATA_FT_REMOVE_TYPE = "DELETE FROM " + TBL_CONTENT_DATA_FT + " WHERE ID IN (SELECT DISTINCT ID FROM " + TBL_CONTENT + " WHERE TDEF=?)";
    protected static final String CONTENT_BINARY_REMOVE_ID = "DELETE FROM " + TBL_CONTENT_BINARY + " WHERE ID=?";
    protected static final String CONTENT_BINARY_REMOVE_GET = "SELECT DISTINCT FBLOB FROM " + TBL_CONTENT_DATA + " WHERE ID=?";
    protected static final String CONTENT_BINARY_REMOVE_TYPE_GET = "SELECT DISTINCT FBLOB FROM " + TBL_CONTENT_DATA + " d, " + TBL_CONTENT + " c WHERE d.ID=c.ID and c.TDEF=?";

    protected static final String CONTENT_TYPE_PK_RETRIEVE_VERSIONS = "SELECT DISTINCT ID,VER FROM " + TBL_CONTENT + " WHERE TDEF=? ORDER BY ID,VER";
    protected static final String CONTENT_TYPE_PK_RETRIEVE_IDS = "SELECT DISTINCT ID FROM " + TBL_CONTENT + " WHERE TDEF=? ORDER BY ID";

    //    select into xx () select  FBLOB1,FBLOB2,?,?,?
    protected static final String BINARY_TRANSIT_HEADER = "SELECT FBLOB FROM " + TBL_BINARY_TRANSIT + " WHERE BKEY=?";
    //                                                                                     1  2   3             4(handle)5    6                  7                    8                    9       10         11    12
    protected static final String BINARY_TRANSIT = "INSERT INTO " + TBL_CONTENT_BINARY + "(ID,VER,QUALITY,FBLOB,NAME,BLOBSIZE,XMLMETA,CREATED_AT,MIMETYPE,PREVIEW_REF,ISIMAGE,RESOLUTION,WIDTH,HEIGHT,PREV1,PREV1_WIDTH,PREV1_HEIGHT,PREV2,PREV2_WIDTH,PREV2_HEIGHT,PREV3,PREV3_WIDTH,PREV3_HEIGHT,PREV1SIZE,PREV2SIZE,PREV3SIZE) " +
            //      1 2 3       4 5 6       7             8 9 10 11
            "SELECT ?,?,?,FBLOB,?,?,?,UNIX_TIMESTAMP()*1000,?,PREVIEW_REF,?,?,?,?,PREV1,PREV1_WIDTH,PREV1_HEIGHT,PREV2,PREV2_WIDTH,PREV2_HEIGHT,PREV3,PREV3_WIDTH,PREV3_HEIGHT,PREV1SIZE,PREV2SIZE,PREV3SIZE FROM " + TBL_BINARY_TRANSIT + " WHERE BKEY=?";
    //                                                                                                   1              2               3        4              5               6        7              8               9            10           11           12           13
    protected static final String BINARY_TRANSIT_PREVIEWS = "UPDATE " + TBL_BINARY_TRANSIT + " SET PREV1=?, PREV1_WIDTH=?, PREV1_HEIGHT=?, PREV2=?, PREV2_WIDTH=?, PREV2_HEIGHT=?, PREV3=?, PREV3_WIDTH=?, PREV3_HEIGHT=?, PREV1SIZE=?, PREV2SIZE=?, PREV3SIZE=? WHERE BKEY=?";
    protected static final String BINARY_TRANSIT_PREVIEWS_REF = "UPDATE " + TBL_BINARY_TRANSIT + " SET PREVIEW_REF=? WHERE BKEY=?";
    //                                                                                                                                                                          1         2             3
    protected static final String BINARY_DESC_LOAD = "SELECT NAME,BLOBSIZE,XMLMETA,CREATED_AT,MIMETYPE,ISIMAGE,RESOLUTION,WIDTH,HEIGHT FROM " + TBL_CONTENT_BINARY + " WHERE ID=? AND VER=? AND QUALITY=?";

    /**
     * {@inheritDoc}
     */
    public String getUppercaseColumn(FxProperty prop) {
        if (prop.isSystemInternal())
            return null;
        switch (prop.getDataType()) {
            case String1024:
                return "UFTEXT1024";
            case Text:
                return "UFCLOB";
            default:
                return null;
        }
    }

    // propertyId -> columns
    protected static final HashMap<Long, String[]> mainColumnHash;
    // dataType -> columns
    protected static final HashMap<FxDataType, String[]> detailColumnHash;

    static {
        detailColumnHash = new HashMap<FxDataType, String[]>(20);
        detailColumnHash.put(FxDataType.Binary, array("FBLOB"));
        detailColumnHash.put(FxDataType.Boolean, array("FBOOL"));
        detailColumnHash.put(FxDataType.Date, array("FDATE1", "FDATE1_Y", "FDATE1_M", "FDATE1_D"));
        detailColumnHash.put(FxDataType.DateRange, array("FDATE1", "FDATE1_Y", "FDATE1_M", "FDATE1_D",
                "FDATE2", "FDATE2_Y", "FDATE2_M", "FDATE2_D"));
        detailColumnHash.put(FxDataType.DateTime, array("FDATE1", "FDATE1_Y", "FDATE1_M", "FDATE1_D", "FDATE1_HH", "FDATE1_MM", "FDATE1_SS"));
        detailColumnHash.put(FxDataType.DateTimeRange, array("FDATE1", "FDATE1_Y", "FDATE1_M", "FDATE1_D", "FDATE1_HH", "FDATE1_MM", "FDATE1_SS",
                "FDATE2", "FDATE2_Y", "FDATE2_M", "FDATE2_D", "FDATE2_HH", "FDATE2_MM", "FDATE2_SS"));
        detailColumnHash.put(FxDataType.Double, array("FDOUBLE"));
        detailColumnHash.put(FxDataType.Float, array("FFLOAT"));
        detailColumnHash.put(FxDataType.LargeNumber, array("FBIGINT"));
        detailColumnHash.put(FxDataType.Number, array("FINT"));
        detailColumnHash.put(FxDataType.Reference, array("FREF"));
        detailColumnHash.put(FxDataType.String1024, array("FTEXT1024"));
        detailColumnHash.put(FxDataType.Text, array("FCLOB"));
        detailColumnHash.put(FxDataType.HTML, array("FCLOB", "FBOOL", "UFCLOB"));
        detailColumnHash.put(FxDataType.SelectOne, array("FSELECT"));
        detailColumnHash.put(FxDataType.SelectMany, array("FSELECT", "FTEXT1024"/*comma separated list of selected id's*/));

        mainColumnHash = new HashMap<Long, String[]>(20);
        mainColumnHash.put(0L, array("ID"));
        mainColumnHash.put(1L, array("VER"));
        mainColumnHash.put(2L, array("TDEF"));
        mainColumnHash.put(3L, array("MANDATOR"));
        mainColumnHash.put(4L, array("ACL"));
        mainColumnHash.put(5L, array("STEP"));
        mainColumnHash.put(6L, array("MAX_VER"));
        mainColumnHash.put(7L, array("LIVE_VER"));
        mainColumnHash.put(8L, array("ISMAX_VER"));
        mainColumnHash.put(9L, array("ISLIVE_VER"));
        mainColumnHash.put(10L, array("ISACTIVE"));
        mainColumnHash.put(11L, array("MAINLANG"));
        mainColumnHash.put(12L, array("RELSRC"));
        mainColumnHash.put(13L, array("RELDST"));
        mainColumnHash.put(14L, array("RELPOS_SRC"));
        mainColumnHash.put(15L, array("RELPOS_DST"));
        mainColumnHash.put(16L, array("CREATED_BY"));
        mainColumnHash.put(17L, array("CREATED_AT"));
        mainColumnHash.put(18L, array("MODIFIED_BY"));
        mainColumnHash.put(19L, array("MODIFIED_AT"));
        mainColumnHash.put(20L, array("CAPTION"));
    }

    /**
     * {@inheritDoc}
     */
    public String[] getColumns(FxProperty prop) {
        if (prop.isSystemInternal())
            return mainColumnHash.get(prop.getId());
        return detailColumnHash.get(prop.getDataType());
    }

    /**
     * {@inheritDoc}
     */
    public FxPK contentCreate(Connection con, FxEnvironment env, StringBuilder sql, long newId, FxContent content) throws FxCreateException, FxInvalidParameterException {
        content.getRootGroup().removeEmptyEntries();
        content.getRootGroup().compactPositions(true);
        content.checkValidity();
        FxPK pk = createMainEntry(con, newId, 1, content);
        try {
            if (sql == null)
                sql = new StringBuilder(2000);
            createDetailEntries(con, env, sql, pk, content.isMaxVersion(), content.isLiveVersion(), content.getData("/"));
            checkUniqueConstraints(con, env, sql, pk, content.getTypeId());
            content.resolveBinaryPreview();
            updateContentBinaryEntry(con, pk, content.getBinaryPreviewId(), content.getBinaryPreviewACL());
        } catch (FxNotFoundException e) {
            throw new FxCreateException(e);
        } catch (FxDbException e) {
            throw new FxCreateException(e);
        } catch (FxUpdateException e) {
            throw new FxCreateException(e);
        } catch (FxApplicationException e) {
            throw new FxCreateException(e);
        }
        return pk;
    }

    /**
     * Assign correct MAX_VER, LIVE_VER, ISMAX_VER and ISLIVE_VER values for a given content instance
     *
     * @param con an open and valid connection
     * @param id  the id to fix the version statistics for
     * @throws FxUpdateException if a sql error occurs
     */
    protected void fixContentVersionStats(Connection con, long id) throws FxUpdateException {
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(CONTENT_VER_CALC);
            ps.setLong(1, id);
            ResultSet rs = ps.executeQuery();
            if (rs == null || !rs.next())
                return;
            int max_ver = rs.getInt(1);
            if (rs.wasNull())
                return;
            int live_ver = rs.getInt(2);
            if (rs.wasNull() || live_ver < 0)
                live_ver = 0;
            ps.close();
            ps = con.prepareStatement(CONTENT_VER_UPDATE_1);
            ps.setInt(1, max_ver);
            ps.setInt(2, live_ver);
            ps.setLong(3, id);
            ps.executeUpdate();
            ps.close();
            ps = con.prepareStatement(CONTENT_VER_UPDATE_2);
            ps.setLong(1, id);
            ps.executeUpdate();
            ps.close();
            ps = con.prepareStatement(CONTENT_VER_UPDATE_3);
            ps.setInt(1, max_ver);
            ps.setInt(2, live_ver);
            ps.setLong(3, id);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxUpdateException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }


    /**
     * {@inheritDoc}
     */
    public FxPK contentCreateVersion(Connection con, FxEnvironment env, StringBuilder sql, FxContent content) throws FxCreateException, FxInvalidParameterException {
        if (content.getPk().isNew())
            throw new FxInvalidParameterException("content", "ex.content.pk.invalid.newVersion", content.getPk());

        content.getRootGroup().removeEmptyEntries();
        content.getRootGroup().compactPositions(true);
        content.checkValidity();

        FxPK pk;
        try {
            int new_version = getContentVersionInfo(con, content.getPk().getId()).getMaxVersion() + 1;
            updateStepDependencies(con, content.getPk().getId(), new_version, env, content.getStepId());
            pk = createMainEntry(con, content.getPk().getId(), new_version, content);
            if (sql == null)
                sql = new StringBuilder(2000);
            createDetailEntries(con, env, sql, pk, content.isMaxVersion(), content.isLiveVersion(), content.getData("/"));
            checkUniqueConstraints(con, env, sql, pk, content.getTypeId());
        } catch (FxNotFoundException e) {
            throw new FxCreateException(e);
        } catch (FxDbException e) {
            throw new FxCreateException(e);
        } catch (FxUpdateException e) {
            throw new FxCreateException(e);
        } catch (FxApplicationException e) {
            throw new FxCreateException(e);
        }

        try {
            fixContentVersionStats(con, content.getPk().getId());
        } catch (FxUpdateException e) {
            throw new FxCreateException(e);
        }
        return pk;
    }

    /**
     * Handle unique steps and make sure only one unique step per content instance exists
     *
     * @param con           open and valid connection
     * @param id            content id
     * @param ignoreVersion the version to ignore on changes (=current version)
     * @param env           FxEnvironment
     * @param stepId        the step id
     * @throws FxNotFoundException on errors
     * @throws FxUpdateException   on errors
     */
    protected void updateStepDependencies(Connection con, long id, int ignoreVersion, FxEnvironment env, long stepId) throws FxNotFoundException, FxUpdateException {
        Step step = env.getStep(stepId);
        StepDefinition stepDef = env.getStepDefinition(step.getStepDefinitionId());
        if (stepDef.isUnique()) {
            Step fallBackStep = env.getStepByDefinition(step.getWorkflowId(), stepDef.getUniqueTargetId());
            updateStepDependencies(con, id, ignoreVersion, env, fallBackStep.getId()); //handle chained unique steps recursively
            PreparedStatement ps = null;
            try {
                ps = con.prepareStatement(CONTENT_STEP_DEPENDENCIES);
                ps.setLong(1, fallBackStep.getId());
                ps.setLong(2, stepId);
                ps.setLong(3, id);
                ps.setInt(4, ignoreVersion);
                ps.executeUpdate();
            } catch (SQLException e) {
                throw new FxUpdateException(e, "ex.content.step.dependencies.update.failed", id, e.getMessage());
            } finally {
                try {
                    if (ps != null)
                        ps.close();
                } catch (SQLException e) {
                    LOG.error(e, e);
                }
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    public FxContentVersionInfo getContentVersionInfo(Connection con, long id) throws FxNotFoundException {
        PreparedStatement ps = null;
        int min_ver = -1, max_ver = 0, live_ver = 0, lastMod_ver = 0;
        long lastMod_time;
        Map<Integer, LifeCycleInfo> versions = new HashMap<Integer, LifeCycleInfo>(5);
        try {

            ps = con.prepareStatement(CONTENT_VER_INFO);
            ps.setLong(1, id);
            ResultSet rs = ps.executeQuery();
            if (rs == null || !rs.next())
                throw new FxNotFoundException("ex.content.notFound", new FxPK(id));
            max_ver = rs.getInt(3);
            live_ver = rs.getInt(4);
            versions.put(rs.getInt(2), LifeCycleInfoImpl.load(rs, 5, 6, 7, 8));
            min_ver = rs.getInt(2);
            lastMod_ver = rs.getInt(2);
            lastMod_time = versions.get(rs.getInt(2)).getModificationTime();
            while (rs.next()) {
                if (rs.getInt(2) < min_ver)
                    min_ver = rs.getInt(2);
                versions.put(rs.getInt(2), LifeCycleInfoImpl.load(rs, 5, 6, 7, 8));
                if (versions.get(rs.getInt(2)).getModificationTime() >= lastMod_time) {
                    lastMod_ver = rs.getInt(2);
                    lastMod_time = versions.get(rs.getInt(2)).getModificationTime();
                }
            }
        } catch (SQLException e) {
            throw new FxNotFoundException(e, "ex.content.versionInfo.sqlError", id, e.getMessage());
        } finally {
            try {
                if (ps != null)
                    ps.close();
            } catch (SQLException e) {
                LOG.error(e, e);
            }
        }
        return new FxContentVersionInfo(id, min_ver, max_ver, live_ver, lastMod_ver, versions);
    }

    /**
     * Create a new main entry
     *
     * @param con     an open and valid connection
     * @param newId   the id to use
     * @param version the version to use
     * @param content content to create
     * @return primary key of the created content
     * @throws FxCreateException on errors
     */
    protected FxPK createMainEntry(Connection con, long newId, int version, FxContent content) throws FxCreateException {
        PreparedStatement ps = null;
        FxPK pk = new FxPK(newId, version);
        try {
            ps = con.prepareStatement(CONTENT_MAIN_INSERT);
            ps.setLong(1, newId);
            ps.setInt(2, version);
            ps.setLong(3, content.getTypeId());
            ps.setLong(4, content.getAclId());
            ps.setLong(5, content.getStepId());
            ps.setInt(6, 1);  //if creating a new version, max_ver will be fixed in a later step
            ps.setInt(7, content.isLiveVersion() ? 1 : 0);
            ps.setBoolean(8, content.isMaxVersion());
            ps.setBoolean(9, content.isLiveVersion());
            ps.setBoolean(10, content.isActive());
            ps.setInt(11, (int) content.getMainLanguage());
            if (content.isRelation()) {
                ps.setLong(12, content.getRelatedSource().getId());
                ps.setInt(13, content.getRelatedSource().getVersion());
                ps.setLong(14, content.getRelatedDestination().getId());
                ps.setInt(15, content.getRelatedDestination().getVersion());
                ps.setLong(16, content.getRelatedSourcePosition());
                ps.setLong(17, content.getRelatedDestinationPosition());
            } else {
                ps.setNull(12, java.sql.Types.NUMERIC);
                ps.setNull(13, java.sql.Types.NUMERIC);
                ps.setNull(14, java.sql.Types.NUMERIC);
                ps.setNull(15, java.sql.Types.NUMERIC);
                ps.setNull(16, java.sql.Types.NUMERIC);
                ps.setNull(17, java.sql.Types.NUMERIC);
            }
            final long userId = FxContext.get().getTicket().getUserId();
            final long now = System.currentTimeMillis();
            ps.setLong(18, userId);
            ps.setLong(19, now);
            ps.setLong(20, userId);
            ps.setLong(21, now);
            ps.setLong(22, content.getMandatorId());
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxCreateException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
        return pk;
    }

    /**
     * Create all detail entries for a content instance
     *
     * @param con         an open and valid connection
     * @param env         FxEnvironment
     * @param sql         an optional StringBuffer
     * @param pk          primary key of the content
     * @param maxVersion  is this content the maximum available version?
     * @param liveVersion is this content the live version?
     * @param data        FxData to create
     * @throws FxNotFoundException on errors
     * @throws FxDbException       on errors
     * @throws FxCreateException   on errors
     */
    protected void createDetailEntries(Connection con, FxEnvironment env, StringBuilder sql, FxPK pk,
                                       boolean maxVersion, boolean liveVersion, List<FxData> data) throws FxNotFoundException, FxDbException, FxCreateException {
        try {
            FxProperty prop;
            for (FxData curr : data) {
                if (curr.isProperty()) {
                    prop = ((FxPropertyAssignment) env.getAssignment(curr.getAssignmentId())).getProperty();
                    if (!prop.isSystemInternal())
                        insertPropertyData(prop, data, con, sql, pk, ((FxPropertyData) curr), maxVersion, liveVersion);
                } else {
                    insertGroupData(con, sql, pk, ((FxGroupData) curr), maxVersion, liveVersion);
                    createDetailEntries(con, env, sql, pk, maxVersion, liveVersion, ((FxGroupData) curr).getChildren());
                }
            }
        } catch (SQLException e) {
            throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage());
        } catch (FxUpdateException e) {
            throw new FxCreateException(e);
        }
    }

    /**
     * Get the parent group multiplicity for a given XMult
     *
     * @param xMult multiplicity of the element
     * @return parent group xmult
     */
    protected String getParentGroupXMult(String xMult) {
        if (StringUtils.isEmpty(xMult))
            return "1"; //this SHOULD not happen!
        int idx = xMult.lastIndexOf(",");
        if (idx > 0)
            return "1," + xMult.substring(0, idx); //root+parent group
        return "1"; //attached to root
    }

    protected final static int INSERT_LANG_POS = 4;
    protected final static int INSERT_ISDEF_LANG_POS = 13;
    protected final static int INSERT_VALUE_POS = 16;
    protected final static int FT_LANG_POS_INSERT = 3;
    protected final static int FT_VALUE_POS_INSERT = 6;
    protected final static int FT_LANG_POS_UPDATE = 4;
    protected final static int FT_VALUE_POS_UPDATE = 1;

    /**
     * Insert property detail data into the database
     *
     * @param prop      thepropery
     * @param allData   List of all data belonging to this property (for cascaded updates like binaries to avoid duplicates)
     * @param con       an open and valid connection
     * @param sql       an optional StringBuffer
     * @param pk        primary key of the content
     * @param data      the value
     * @param isMaxVer  is this content in the max. version?
     * @param isLiveVer is this content in the live version?
     * @throws SQLException      on errors
     * @throws FxDbException     on errors
     * @throws FxUpdateException on errors
     */
    protected void insertPropertyData(FxProperty prop, List<FxData> allData, Connection con, StringBuilder sql, FxPK pk,
                                      FxPropertyData data, boolean isMaxVer, boolean isLiveVer) throws SQLException, FxDbException, FxUpdateException {
        if (data == null || data.isEmpty())
            return;
        PreparedStatement ps = null, ps_ft = null;
        if (sql == null)
            sql = new StringBuilder(1000);
        else
            sql.setLength(0);
        try {
            String[] columns = getColumns(prop);
            String upperColumn = getUppercaseColumn(prop);
            sql.append("INSERT INTO ").append(TBL_CONTENT_DATA).
                    //                1  2   3   4    5      6     7         8     9      10          11        12                 13      14    15          16
                            append(" (ID,VER,POS,LANG,ASSIGN,XPATH,XPATHMULT,XMULT,XINDEX,PARENTXMULT,ISMAX_VER,ISLIVE_VER,ISGROUP,ISMLDEF,TPROP,PARENTXPATH,XDEPTH,");
            for (String col : columns)
                sql.append(col).append(',');
            if (upperColumn != null)
                sql.append(upperColumn);
            else
                sql.deleteCharAt(sql.length() - 1); //delete last ','
            sql.append(")VALUES(?,?,?,?,?,?,?,?,?,?,?,?,false,?,?,?,");
            //calculate depth
            sql.append(data.getIndices().length).append(',');
            int s = 0;
            while (s < columns.length) {
                sql.append("?,");
                s++;
            }
            if (upperColumn != null)
                sql.append('?');
            else
                sql.deleteCharAt(sql.length() - 1); //delete last ',' if no uppercase column
            sql.append(')');
            ps = con.prepareStatement(sql.toString());
            ps.setLong(1, pk.getId());
            ps.setInt(2, pk.getVersion());
            ps.setInt(3, data.getPos());
            ps.setLong(5, data.getAssignmentId());
            ps.setString(6, XPathElement.stripType(data.getXPath()));
            ps.setString(7, XPathElement.stripType(data.getXPathFull()));
            String xmult = FxArrayUtils.toSeparatedList(data.getIndices(), ',');
            ps.setString(8, xmult);
            ps.setInt(9, data.getIndex());
            ps.setString(10, getParentGroupXMult(xmult));
            ps.setBoolean(11, isMaxVer);
            ps.setBoolean(12, isLiveVer);
            ps.setLong(14, prop.getId());
            ps.setString(15, XPathElement.stripType(data.getParent().getXPathFull()));

            sql.setLength(0);
            sql.append("INSERT INTO ").append(TBL_CONTENT_DATA_FT).
                    append("(ID,VER,LANG,ASSIGN,XMULT,VALUE) VALUES (?,?,?,?,?,?)");
            ps_ft = con.prepareStatement(sql.toString());

            ps_ft.setLong(1, pk.getId());
            ps_ft.setInt(2, pk.getVersion());
            if (!data.getValue().isMultiLanguage()) {
                ps.setBoolean(INSERT_LANG_POS, true);
                ps_ft.setInt(FT_LANG_POS_INSERT, (int) FxLanguage.DEFAULT_ID);
            } else
                ps.setBoolean(INSERT_ISDEF_LANG_POS, false);
            ps_ft.setLong(4, data.getAssignmentId());
            ps_ft.setString(5, xmult);
            setPropertyData(true, -1, prop, allData, con, data, ps, ps_ft, upperColumn);
        } finally {
            if (ps != null)
                ps.close();
            if (ps_ft != null)
                ps_ft.close();
        }
    }

    /**
     * Update a properties data and/or position or a groups position
     *
     * @param change  the change applied
     * @param prop    the property unless change is a group change
     * @param allData all content data unless change is a group change
     * @param con     an open and valid connection
     * @param sql     sql
     * @param pk      primary key
     * @param data    property data unless change is a group change
     * @throws SQLException      on errors
     * @throws FxDbException     on errors
     * @throws FxUpdateException on errors
     */
    protected void updatePropertyData(FxDelta.FxDeltaChange change, FxProperty prop, List<FxData> allData,
                                      Connection con, StringBuilder sql, FxPK pk, FxPropertyData data)
            throws SQLException, FxDbException, FxUpdateException {
        if ((change.isProperty() && (data == null || data.isEmpty())) || !(change.isDataChange() || change.isPositionChange()))
            return;
        PreparedStatement ps = null, ps_ft = null;
        if (sql == null)
            sql = new StringBuilder(1000);
        else
            sql.setLength(0);
        try {

            int pos_idx = 1;
            String upperColumn = null;
            sql.append("UPDATE ").append(TBL_CONTENT_DATA).append(" SET ");

            if (change.isDataChange() && !change.isGroup()) {
                String[] columns = getColumns(prop);
                upperColumn = getUppercaseColumn(prop);
                sql.append("ISMLDEF=?,");
                for (String col : columns)
                    sql.append(col).append("=?,");
                if (upperColumn != null)
                    sql.append(upperColumn).append("=?,");

                pos_idx = columns.length + (upperColumn == null ? 2 : 3);
            }

            if (change.isPositionChange())
                sql.append("POS=?");
            else {
                pos_idx--;
                sql.deleteCharAt(sql.length() - 1); //remove last ","
            }
            //                    1         2          3            4               5
            sql.append(" WHERE ID=? AND VER=? AND LANG=? AND ASSIGN=? AND XPATHMULT=?");

            ps = con.prepareStatement(sql.toString());
            if (change.isPositionChange())
                ps.setInt(pos_idx, change.getNewData().getPos());
            ps.setLong(pos_idx + 1, pk.getId());
            ps.setInt(pos_idx + 2, pk.getVersion());
//            ps.setInt(pos_idx+3, /*lang*/);
            ps.setLong(pos_idx + 4, change.getNewData().getAssignmentId());
            ps.setString(pos_idx + 5, XPathElement.stripType(change.getNewData().getXPathFull()) + (data == null ? "/" : ""));

            if (change.isGroup()) {
                ps.setInt(pos_idx + 3, (int) FxLanguage.SYSTEM_ID);
                ps.executeUpdate();
                return;
            }

            if (change.isPositionChange() && !change.isDataChange()) {
                //just update positions
                for (long lang : data.getValue().getTranslatedLanguages()) {
                    ps.setInt(pos_idx + 3, (int) lang);
                    ps.executeUpdate();
                }
                return;
            }

            sql.setLength(0);
            //                                                                   1          2         3          4            5           6
            sql.append("UPDATE ").append(TBL_CONTENT_DATA_FT).append(" SET VALUE=? WHERE ID=? AND VER=? AND LANG=? AND ASSIGN=? AND XMULT=?");
            ps_ft = con.prepareStatement(sql.toString());

            ps_ft.setLong(2, pk.getId());
            ps_ft.setInt(3, pk.getVersion());
            //3==lang
            ps_ft.setLong(5, data.getAssignmentId());
            ps_ft.setString(6, FxArrayUtils.toSeparatedList(data.getIndices(), ','));
            setPropertyData(false, pos_idx, prop, allData, con, data, ps, ps_ft, upperColumn);
        } finally {
            if (ps != null)
                ps.close();
            if (ps_ft != null)
                ps_ft.close();
        }
    }

    /**
     * Set a properties data for inserts or updates
     *
     * @param insert              perform insert or update?
     * @param update_position_idx position in the prepared statement to start at
     * @param prop                current property
     * @param allData             all data of the instance (might be needed to buld references, etc.)
     * @param con                 an open and valid connection
     * @param data                current property data
     * @param ps                  prepared statement for the data table
     * @param ps_ft               prepared statement for the fulltext table
     * @param upperColumn         name of the uppercase column (if present)
     * @throws SQLException      on errors
     * @throws FxUpdateException on errors
     * @throws FxDbException     on errors
     */
    private void setPropertyData(boolean insert, int update_position_idx, FxProperty prop, List<FxData> allData,
                                 Connection con, FxPropertyData data, PreparedStatement ps, PreparedStatement ps_ft,
                                 String upperColumn) throws SQLException, FxUpdateException, FxDbException {
        FxValue value = data.getValue();
        if (value instanceof FxNoAccess) {
            FxContext.get().runAsSystem();
            value = ((FxNoAccess) value).getWrappedValue();
            FxContext.get().stopRunAsSystem();
        }
        if (value.isMultiLanguage() != ((FxPropertyAssignment) data.getAssignment()).isMultiLang()) {
            if (((FxPropertyAssignment) data.getAssignment()).isMultiLang())
                throw new FxUpdateException("ex.content.value.invalid.multilanguage.ass.multi", data.getXPathFull());
            else
                throw new FxUpdateException("ex.content.value.invalid.multilanguage.ass.single", data.getXPathFull());
        }
        int pos_lang = insert ? INSERT_LANG_POS : update_position_idx + 3;
        int pos_isdef_lang = insert ? INSERT_ISDEF_LANG_POS : 1;
        int pos_value = insert ? INSERT_VALUE_POS : 2;
        if (prop.getDataType().isSingleRowStorage()) {
            //Data types that just use one db row can be handled in a very similar way
            Object translatedValue;
            GregorianCalendar gc = null;
            for (int i = 0; i < value.getTranslatedLanguages().length; i++) {
                translatedValue = value.getTranslation(value.getTranslatedLanguages()[i]);
                if (translatedValue == null) {
                    java.lang.System.err.println("null!");
                }
                ps.setInt(pos_lang, value.getTranslatedLanguages()[i].intValue());
                if (!value.isMultiLanguage())
                    ps.setBoolean(pos_isdef_lang, true);
                else
                    ps.setBoolean(pos_isdef_lang, value.isDefaultLanguage(value.getTranslatedLanguages()[i]));
                if (upperColumn != null)
                    ps.setString(pos_value + 1, translatedValue.toString().toUpperCase());
                ps_ft.setInt(insert ? FT_LANG_POS_INSERT : FT_LANG_POS_UPDATE, value.getTranslatedLanguages()[i].intValue());
                ps_ft.setString(insert ? FT_VALUE_POS_INSERT : FT_VALUE_POS_UPDATE, translatedValue.toString());
                switch (prop.getDataType()) {
                    case Double:
                        checkDataType(FxDouble.class, value, data.getXPathFull());
                        ps.setDouble(pos_value, (Double) translatedValue);
//                        ps.setBigDecimal(pos_value, new BigDecimal((Double) translatedValue));
                        break;
                    case Float:
                        checkDataType(FxFloat.class, value, data.getXPathFull());
                        ps.setFloat(pos_value, (Float) translatedValue);
//                        ps.setBigDecimal(pos_value, new BigDecimal(((Float) translatedValue).doubleValue()));
                        break;
                    case LargeNumber:
                        checkDataType(FxLargeNumber.class, value, data.getXPathFull());
                        ps.setLong(pos_value, (Long) translatedValue);
                        break;
                    case Number:
                        checkDataType(FxNumber.class, value, data.getXPathFull());
                        ps.setInt(pos_value, (Integer) translatedValue);
                        break;
                    case HTML:
                        checkDataType(FxHTML.class, value, data.getXPathFull());
                        boolean useTidy = ((FxHTML) value).isTidyHTML();
                        ps.setBoolean(pos_value + 1, useTidy);
                        if (useTidy)
                            translatedValue = doTidy(data.getXPathFull(), (String) translatedValue);
                        String extracted;
                        ExtractedData xd = HtmlExtractor.extract(useTidy
                                ? (String) translatedValue
                                : doTidy(data.getXPathFull(), (String) translatedValue));
                        if (xd == null) {
                            throw new FxUpdateException("ex.content.value.extraction.failed");
                        } else
                            extracted = xd.getText();
                        ps.setString(pos_value + 2, extracted);
                        ps.setString(pos_value, (String) translatedValue);
                        break;
                    case String1024:
                    case Text:
                        checkDataType(FxString.class, value, data.getXPathFull());
                        ps.setString(pos_value, (String) translatedValue);
                        break;
                    case Boolean:
                        checkDataType(FxBoolean.class, value, data.getXPathFull());
                        ps.setBoolean(pos_value, (Boolean) translatedValue);
                        break;
                    case Date:
                        checkDataType(FxDate.class, value, data.getXPathFull());
                        if (gc == null) gc = new GregorianCalendar();
                        gc.setTime((java.util.Date) translatedValue);
                        //strip all time information, this might not be necessary since ps.setDate() strips them
                        //for most databases but won't hurt either ;)
                        gc.set(GregorianCalendar.HOUR, 0);
                        gc.set(GregorianCalendar.MINUTE, 0);
                        gc.set(GregorianCalendar.SECOND, 0);
                        gc.set(GregorianCalendar.MILLISECOND, 0);
                        ps.setDate(pos_value, new java.sql.Date(gc.getTimeInMillis()));
                        ps.setInt(pos_value + 1, gc.get(GregorianCalendar.YEAR));
                        ps.setInt(pos_value + 2, gc.get(GregorianCalendar.MONTH) + 1);
                        ps.setInt(pos_value + 3, gc.get(GregorianCalendar.DAY_OF_MONTH));
                        break;
                    case DateTime:
                        checkDataType(FxDateTime.class, value, data.getXPathFull());
                        if (gc == null) gc = new GregorianCalendar();
                        gc.setTime((java.util.Date) translatedValue);
                        ps.setTimestamp(pos_value, new Timestamp(gc.getTimeInMillis()));
                        ps.setInt(pos_value + 1, gc.get(GregorianCalendar.YEAR));
                        ps.setInt(pos_value + 2, gc.get(GregorianCalendar.MONTH) + 1);
                        ps.setInt(pos_value + 3, gc.get(GregorianCalendar.DAY_OF_MONTH));
                        ps.setInt(pos_value + 4, gc.get(GregorianCalendar.HOUR_OF_DAY));
                        ps.setInt(pos_value + 5, gc.get(GregorianCalendar.MINUTE));
                        ps.setInt(pos_value + 6, gc.get(GregorianCalendar.SECOND));
                        break;
                    case DateRange:
                        checkDataType(FxDateRange.class, value, data.getXPathFull());
                        if (gc == null) gc = new GregorianCalendar();
                        gc.setTime(((DateRange) translatedValue).getLower());
                        gc.set(GregorianCalendar.HOUR, 0);
                        gc.set(GregorianCalendar.MINUTE, 0);
                        gc.set(GregorianCalendar.SECOND, 0);
                        gc.set(GregorianCalendar.MILLISECOND, 0);
                        ps.setDate(pos_value, new java.sql.Date(gc.getTimeInMillis()));
                        ps.setInt(pos_value + 1, gc.get(GregorianCalendar.YEAR));
                        ps.setInt(pos_value + 2, gc.get(GregorianCalendar.MONTH) + 1);
                        ps.setInt(pos_value + 3, gc.get(GregorianCalendar.DAY_OF_MONTH));
                        gc.setTime(((DateRange) translatedValue).getUpper());
                        gc.set(GregorianCalendar.HOUR, 0);
                        gc.set(GregorianCalendar.MINUTE, 0);
                        gc.set(GregorianCalendar.SECOND, 0);
                        gc.set(GregorianCalendar.MILLISECOND, 0);
                        ps.setDate(pos_value + 4, new java.sql.Date(gc.getTimeInMillis()));
                        ps.setInt(pos_value + 5, gc.get(GregorianCalendar.YEAR));
                        ps.setInt(pos_value + 6, gc.get(GregorianCalendar.MONTH) + 1);
                        ps.setInt(pos_value + 7, gc.get(GregorianCalendar.DAY_OF_MONTH));
                        break;
                    case DateTimeRange:
                        checkDataType(FxDateTimeRange.class, value, data.getXPathFull());
                        if (gc == null) gc = new GregorianCalendar();
                        gc.setTime(((DateRange) translatedValue).getLower());
                        ps.setTimestamp(pos_value, new Timestamp(gc.getTimeInMillis()));
                        ps.setInt(pos_value + 1, gc.get(GregorianCalendar.YEAR));
                        ps.setInt(pos_value + 2, gc.get(GregorianCalendar.MONTH) + 1);
                        ps.setInt(pos_value + 3, gc.get(GregorianCalendar.DAY_OF_MONTH));
                        ps.setInt(pos_value + 4, gc.get(GregorianCalendar.HOUR_OF_DAY));
                        ps.setInt(pos_value + 5, gc.get(GregorianCalendar.MINUTE));
                        ps.setInt(pos_value + 6, gc.get(GregorianCalendar.SECOND));
                        gc.setTime(((DateRange) translatedValue).getUpper());
                        ps.setTimestamp(pos_value + 7, new Timestamp(gc.getTimeInMillis()));
                        ps.setInt(pos_value + 8, gc.get(GregorianCalendar.YEAR));
                        ps.setInt(pos_value + 9, gc.get(GregorianCalendar.MONTH) + 1);
                        ps.setInt(pos_value + 10, gc.get(GregorianCalendar.DAY_OF_MONTH));
                        ps.setInt(pos_value + 11, gc.get(GregorianCalendar.HOUR_OF_DAY));
                        ps.setInt(pos_value + 12, gc.get(GregorianCalendar.MINUTE));
                        ps.setInt(pos_value + 13, gc.get(GregorianCalendar.SECOND));
                        break;
                    case Binary:
                        checkDataType(FxBinary.class, value, data.getXPathFull());
                        BinaryDescriptor binary = (BinaryDescriptor) translatedValue;
                        if (!binary.isNewBinary()) {
                            ps.setLong(pos_value, binary.getId());
                        } else {
                            try {
                                //transfer the binary from the transit table to the binary table
                                BinaryDescriptor created = binaryTransit(con, binary);
                                ps.setLong(pos_value, created.getId());
                                //check all other properties if they contain the same handle
                                //and replace with the data of the new binary
                                for (FxData _curr : allData) {
                                    if (_curr instanceof FxPropertyData && !_curr.isEmpty() &&
                                            ((FxPropertyData) _curr).getValue() instanceof FxBinary) {
                                        FxBinary _val = (FxBinary) ((FxPropertyData) _curr).getValue();
//                                            System.out.println("replacing "+_curr.getXPathFull());
                                        _val._replaceHandle(binary.getHandle(), created);
                                    }
                                }
                            } catch (FxApplicationException e) {
                                throw new FxDbException(e);
                            }
                        }
                        String fulltext = FxXMLUtils.getElementData(binary.getMetadata(), "compressed");
                        if (fulltext != null)
                            ps_ft.setString(insert ? FT_VALUE_POS_INSERT : FT_VALUE_POS_UPDATE, fulltext);
                        break;
                    case SelectOne:
                        checkDataType(FxSelectOne.class, value, data.getXPathFull());
                        ps.setLong(pos_value, ((FxSelectListItem) translatedValue).getId());
                        break;
                    case SelectMany:
                        checkDataType(FxSelectMany.class, value, data.getXPathFull());
                        SelectMany sm = (SelectMany) translatedValue;
                        for (int i1 = 0; i1 < sm.getSelected().size(); i1++) {
                            FxSelectListItem item = sm.getSelected().get(i1);
                            if (i1 > 0)
                                ps.executeUpdate();
                            ps.setLong(pos_value, item.getId());
                            ps.setString(pos_value + 1, sm.getSelectedIdsList());
                        }
                        if (sm.getSelected().size() == 0)
                            ps.setLong(pos_value, 0); //write the virtual item as a marker to have a valid row
                        break;
                    case Reference:
                        checkDataType(FxReference.class, value, data.getXPathFull());
                        checkReference(con, prop.getReferencedType(), ((ReferencedContent) translatedValue), data.getXPathFull());
                        ps.setLong(pos_value, ((ReferencedContent) translatedValue).getId());
                        break;
                    case InlineReference:
                    default:
                        throw new FxDbException(LOG, "ex.db.notImplemented.store", prop.getDataType().getName());
                }
                ps.executeUpdate();
                if (prop.isFulltextIndexed())
                    ps_ft.executeUpdate();
            }
        } else {
            switch (prop.getDataType()) {
                //TODO: implement datatype specific insert

                default:
                    throw new FxDbException(LOG, "ex.db.notImplemented.store", prop.getDataType().getName());
            }

        }
    }

    /**
     * Check if a referenced id is of an expected type and exists
     *
     * @param con          an open and valid connection
     * @param expectedType the expected type
     * @param ref          referenced content
     * @param xpath        the XPath this reference is for (used for error messages only)
     * @throws FxDbException if not exists or wrong type
     */
    private static void checkReference(Connection con, FxType expectedType, FxPK ref, String xpath) throws FxDbException {
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement("SELECT DISTINCT TDEF FROM " + TBL_CONTENT + " WHERE ID=?");
            ps.setLong(1, ref.getId());
            ResultSet rs = ps.executeQuery();
            if (rs == null || !rs.next())
                throw new FxDbException("ex.content.reference.notFound", ref, xpath);
            long type = rs.getLong(1);
            if (type != expectedType.getId())
                throw new FxDbException("ex.content.value.invalid.reftype", expectedType, CacheAdmin.getEnvironment().getType(type));
        } catch (SQLException e) {
            throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
        //To change body of created methods use File | Settings | File Templates.
    }

    /**
     * Check if the given value is of the expected class
     *
     * @param dataClass expected class
     * @param value     value to check
     * @param XPath     xpath with full indices for error message
     * @throws FxUpdateException if the class does not match
     */
    private static void checkDataType(Class dataClass, FxValue value, String XPath) throws FxUpdateException {
        if (!(value.getClass().getSimpleName().equals(dataClass.getSimpleName()))) {
            throw new FxUpdateException("ex.content.value.invalid.class", value.getClass().getSimpleName(), XPath, dataClass.getSimpleName());
        }
    }

    /**
     * Run tidy on the given content
     *
     * @param XPath   XPath with full indices for error messages
     * @param content the string to tidy
     * @return tidied string
     * @throws FxUpdateException if tidy failed
     */
    private static String doTidy(String XPath, String content) throws FxUpdateException {
        Tidy tidy = new Tidy();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        tidy.setDropEmptyParas(true);
        tidy.setMakeClean(true);
        tidy.setHideEndTags(true);
        tidy.setTidyMark(false);
        tidy.setMakeBare(true);
        tidy.setXHTML(true);
//        tidy.setOnlyErrors(true);
        tidy.setShowWarnings(false);
        tidy.setQuiet(true);
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        tidy.setErrout(pw);
        tidy.parse(new StringReader(content), out);

        if (tidy.getParseErrors() > 0) {
            String error = sw.getBuffer().toString();
            throw new FxUpdateException("ex.content.value.tidy.failed", XPath, error);
        }
        content = out.toString();
        return content;
    }


    /**
     * Transfer a binary from the transit to the 'real' binary table
     *
     * @param con    open and valid connection
     * @param binary the binary descriptor
     * @return descriptor of final binary
     * @throws SQLException           on errors
     * @throws FxApplicationException on errors looking up the sequencer
     */
    private BinaryDescriptor binaryTransit(Connection con, BinaryDescriptor binary) throws FxApplicationException, SQLException {
        long id = EJBLookup.getSequencerEngine().getId(SequencerEngine.System.BINARY);
        return binaryTransit(con, binary, id, 1, 1);
    }

    /**
     * Transfer a binary from the transit to the 'real' binary table
     *
     * @param con     open and valid connection
     * @param binary  the binary descriptor
     * @param id      desired id
     * @param version desired version
     * @param quality desired quality
     * @return descriptor of final binary
     * @throws SQLException           on errors
     * @throws FxApplicationException on errors looking up the sequencer
     */
    private BinaryDescriptor binaryTransit(Connection con, BinaryDescriptor binary, long id, int version, int quality) throws FxApplicationException, SQLException {
//        System.out.println("Binary transit: " + binary.getName() + "/" + binary.getHandle());
        PreparedStatement ps = null;
        BinaryDescriptor created;

        try {
            double resolution = 0.0;
            int width = 0;
            int height = 0;
            boolean isImage = binary.getMimeType().startsWith("image/");
            if (isImage) {
                try {
                    width = Integer.parseInt(FxXMLUtils.getElementData(binary.getMetadata(), "width"));
                    height = Integer.parseInt(FxXMLUtils.getElementData(binary.getMetadata(), "height"));
                    resolution = Double.parseDouble(FxXMLUtils.getElementData(binary.getMetadata(), "xResolution"));
                } catch (NumberFormatException e) {
                    //ignore
                    LOG.warn(e, e);
                }
            }
            created = new BinaryDescriptor(CacheAdmin.getStreamServers(), id, version, quality, java.lang.System.currentTimeMillis(),
                    binary.getName(), binary.getSize(), binary.getMetadata(), binary.getMimeType(), isImage, resolution, width, height);
            ps = con.prepareStatement(BINARY_TRANSIT);
            ps.setLong(1, created.getId());
            ps.setInt(2, created.getVersion()); //version
            ps.setInt(3, created.getQuality()); //quality
//            ps.setString(4, binary.getHandle());
            ps.setString(4, created.getName());
            ps.setLong(5, created.getSize());
            ps.setString(6, created.getMetadata());
            ps.setString(7, created.getMimeType());
            ps.setBoolean(8, created.isImage());
            ps.setDouble(9, created.getResolution());
            ps.setInt(10, created.getWidth());
            ps.setInt(11, created.getHeight());
            ps.setString(12, binary.getHandle());
            ps.executeUpdate();
        } finally {
            if (ps != null)
                ps.close();
        }
        return created;
    }

    /**
     * Insert group detail data into the database
     *
     * @param con       an open and valid connection
     * @param sql       an optional StringBuffer
     * @param pk        primary key of the content
     * @param groupData the group
     * @param isMaxVer  is this content in the max. version?
     * @param isLiveVer is this content in the live version?
     * @throws SQLException on errors
     */
    protected void insertGroupData(Connection con, StringBuilder sql, FxPK pk, FxGroupData groupData,
                                   boolean isMaxVer, boolean isLiveVer) throws SQLException {
        if (groupData == null || groupData.isEmpty())
            return;
        PreparedStatement ps = null;
        if (sql == null)
            sql = new StringBuilder(500);
        else
            sql.setLength(0);
        try {
            sql.append("INSERT INTO ").append(TBL_CONTENT_DATA).
                    //                1  2   3   4    5      6     7         8            9           10        11         12
                            append(" (ID,VER,POS,LANG,ASSIGN,XPATH,XPATHMULT,XMULT,XINDEX,PARENTXMULT,ISMAX_VER,ISLIVE_VER,PARENTXPATH,ISGROUP,ISMLDEF,XDEPTH");
            sql.append(")VALUES(?,?,?,?,?,?,?,?,").append(groupData.getIndex()).append(",?,?,?,?,true,false,").append(groupData.getIndices().length).append(")");

            ps = con.prepareStatement(sql.toString());
            ps.setLong(1, pk.getId());
            ps.setInt(2, pk.getVersion());
            ps.setInt(3, groupData.getPos());
            ps.setInt(4, (int) FxLanguage.SYSTEM_ID);
            ps.setLong(5, groupData.getAssignmentId());
            ps.setString(6, groupData.getXPath() + "/");
            ps.setString(7, groupData.getXPathFull() + "/");
            String xmult = FxArrayUtils.toSeparatedList(groupData.getIndices(), ',');
            ps.setString(8, xmult);
            ps.setString(9, getParentGroupXMult(xmult));
            ps.setBoolean(10, isMaxVer);
            ps.setBoolean(11, isLiveVer);
            ps.setString(12, groupData.getParent().getXPathFull());
            ps.executeUpdate();
        } finally {
            if (ps != null)
                ps.close();
        }
    }

    /**
     * Remove a detail data entry (group or property, in all existing languages)
     *
     * @param con  an open and valid Connection
     * @param sql  sql
     * @param pk   primary key
     * @param data the entry to remove
     * @throws SQLException on errors
     */
    private void deleteDetailData(Connection con, StringBuilder sql, FxPK pk, FxData data) throws SQLException {
        if (data == null || data.isEmpty())
            return;
        PreparedStatement ps = null;
        if (sql == null)
            sql = new StringBuilder(500);
        else
            sql.setLength(0);
        try {
            //                                                                    1         2            3               4
            sql.append("DELETE FROM ").append(TBL_CONTENT_DATA).append(" WHERE ID=? AND VER=? AND ASSIGN=? AND XPATHMULT=?");
            ps = con.prepareStatement(sql.toString());
            ps.setLong(1, pk.getId());
            ps.setInt(2, pk.getVersion());
            ps.setLong(3, data.getAssignmentId());
            if (data.isGroup())
                ps.setString(4, data.getXPathFull() + "/");
            else
                ps.setString(4, data.getXPathFull());
            ps.executeUpdate();
            String xmult = FxArrayUtils.toSeparatedList(data.getIndices(), ',');

            ps.close();
            sql.setLength(0);
            //                                                                       1         2            3           4
            sql.append("DELETE FROM ").append(TBL_CONTENT_DATA_FT).append(" WHERE ID=? AND VER=? AND ASSIGN=? AND XMULT=?");
            ps = con.prepareStatement(sql.toString());
            ps.setLong(1, pk.getId());
            ps.setInt(2, pk.getVersion());
            ps.setLong(3, data.getAssignmentId());
            ps.setString(4, xmult);
            ps.executeUpdate();
        } finally {
            if (ps != null)
                ps.close();
        }
    }

    /**
     * {@inheritDoc}
     */
    public FxContent contentLoad(Connection con, FxPK pk, FxEnvironment env, StringBuilder sql) throws FxLoadException, FxInvalidParameterException, FxNotFoundException {
        if (pk.isNew())
            throw new FxInvalidParameterException("pk", "ex.content.load.newPK");
        if (sql == null)
            sql = new StringBuilder(1000);
        sql.append(CONTENT_MAIN_LOAD);
        sql.append(" WHERE ID=? AND ");
        if (pk.isDistinctVersion())
            sql.append(" VER=?");
        else if (pk.getVersion() == FxPK.LIVE)
            sql.append(" ISLIVE_VER=?");
        else if (pk.getVersion() == FxPK.MAX)
            sql.append(" ISMAX_VER=?");
        PreparedStatement ps = null;
        FxPK contentPK, sourcePK = null, destinationPK = null;
        int srcPos = 0, dstPos = 0;
        try {
            ps = con.prepareStatement(sql.toString());
            ps.setLong(1, pk.getId());
            if (pk.isDistinctVersion())
                ps.setInt(2, pk.getVersion());
            else
                ps.setBoolean(2, true);
            ResultSet rs = ps.executeQuery();
            if (rs == null || !rs.next())
                throw new FxNotFoundException("ex.content.notFound", pk);
            contentPK = new FxPK(rs.getLong(1), rs.getInt(2));
            FxType type = env.getType(rs.getLong(3));
            ACL acl = env.getACL(rs.getInt(4));
            Step step = env.getStep(rs.getLong(5));
            Mandator mand = env.getMandator(rs.getInt(22));
            FxGroupData root = loadDetails(con, type, env, contentPK, pk.getVersion());
            rs.getLong(12);
            if (!rs.wasNull()) {
                sourcePK = new FxPK(rs.getLong(12), rs.getInt(13));
                destinationPK = new FxPK(rs.getLong(14), rs.getInt(15));
                srcPos = rs.getInt(16);
                dstPos = rs.getInt(17);
            }
            FxContent content = new FxContent(contentPK, type.getId(), type.isRelation(), mand.getId(), acl.getId(), step.getId(), rs.getInt(6),
                    rs.getInt(7), rs.getBoolean(10), rs.getInt(11), sourcePK, destinationPK, srcPos, dstPos,
                    LifeCycleInfoImpl.load(rs, 18, 19, 20, 21), root, rs.getLong(23), rs.getLong(24)).initSystemProperties();
            if (rs.next())
                throw new FxLoadException("ex.content.load.notDistinct", pk);
            return content;
        } catch (SQLException e) {
            throw new FxLoadException(LOG, e, "ex.db.sqlError", e.getMessage());
        } catch (FxDbException e) {
            throw new FxLoadException(e);
        } finally {
            try {
                if (ps != null)
                    ps.close();
            } catch (SQLException e) {
                LOG.warn(e, e);
            }
        }
    }

    /**
     * Load all detail entries for a content instance
     *
     * @param con              open and valid(!) connection
     * @param type             FxType used
     * @param env              FxEnvironment
     * @param pk               primary key of the content data to load
     * @param requestedVersion the originally requested version (LIVE, MAX or specific version number, needed to resolve references since the pk's version is resoved already)
     * @return a (root) group containing all data
     * @throws com.flexive.shared.exceptions.FxLoadException
     *                                     on errors
     * @throws SQLException                on errors
     * @throws FxInvalidParameterException on errors
     * @throws FxDbException               on errors
     */
    @SuppressWarnings("unchecked")
    protected FxGroupData loadDetails(Connection con, FxType type, FxEnvironment env, FxPK pk, int requestedVersion) throws FxLoadException, SQLException, FxInvalidParameterException, FxDbException {
        FxGroupData root;
        PreparedStatement ps = null;
        try {
            root = type.createEmptyData(type.buildXPathPrefix(pk));
            root.removeEmptyEntries(true);
            root.compactPositions(true);
            ps = con.prepareStatement(CONTENT_DATA_LOAD);
            ps.setLong(1, pk.getId());
            ps.setInt(2, pk.getVersion());
            ResultSet rs = ps.executeQuery();
            String currXPath = null;
            FxAssignment currAssignment = null;
            int currPos = -1;
            long currLang;
            long defLang = FxLanguage.SYSTEM_ID;
            boolean isGroup = true;
            boolean isMLDef;
            boolean multiLang = false;
            FxValue currValue = null;
            String[] columns = null;
            List<ServerLocation> server = CacheAdmin.getStreamServers();
            while (rs != null && rs.next()) {
                if (currXPath != null && !currXPath.equals(rs.getString(5))) {
                    //add this property
                    if (!isGroup)
                        currValue.setDefaultLanguage(defLang);
                    addValue(root, currXPath, currAssignment, currPos, currValue);
                    currValue = null;
                    defLang = FxLanguage.SYSTEM_ID;
                }
                //read next row
                currXPath = rs.getString(5);
                currPos = rs.getInt(1);
                currLang = rs.getInt(2);
                isMLDef = rs.getBoolean(8);
                isGroup = rs.getBoolean(7);
                if (currAssignment == null || currAssignment.getId() != rs.getLong(3)) {
                    //new data type
                    currAssignment = env.getAssignment(rs.getLong(3));
                    if (!isGroup)
                        columns = getColumns(((FxPropertyAssignment) currAssignment).getProperty());
                }


                if (!isGroup) {
                    FxDataType dataType = ((FxPropertyAssignment) currAssignment).getProperty().getDataType();
                    if (currValue == null)
                        multiLang = ((FxPropertyAssignment) currAssignment).isMultiLang();
                    switch (dataType) {
                        case Float:
                            if (currValue == null)
                                currValue = new FxFloat(multiLang, currLang, rs.getFloat(columns[0]));
                            else
                                currValue.setTranslation(currLang, rs.getFloat(columns[0]));
                            break;
                        case Double:
                            if (currValue == null)
                                currValue = new FxDouble(multiLang, currLang, rs.getDouble(columns[0]));
                            else
                                currValue.setTranslation(currLang, rs.getDouble(columns[0]));
                            break;
                        case LargeNumber:
                            if (currValue == null)
                                currValue = new FxLargeNumber(multiLang, currLang, rs.getLong(columns[0]));
                            else
                                currValue.setTranslation(currLang, rs.getLong(columns[0]));
                            break;
                        case Number:
                            if (currValue == null)
                                currValue = new FxNumber(multiLang, currLang, rs.getInt(columns[0]));
                            else
                                currValue.setTranslation(currLang, rs.getInt(columns[0]));
                            break;
                        case HTML:
                            if (currValue == null) {
                                currValue = new FxHTML(multiLang, currLang, rs.getString(columns[0]));
                                ((FxHTML) currValue).setTidyHTML(rs.getBoolean(columns[1]));
                            } else
                                currValue.setTranslation(currLang, rs.getString(columns[0]));
                            break;
                        case String1024:
                        case Text:
                            if (currValue == null) {
                                currValue = new FxString(multiLang, currLang, rs.getString(columns[0]));
                                if (dataType == FxDataType.String1024)
                                    currValue.setMaxInputLength(1024);
                            } else
                                currValue.setTranslation(currLang, rs.getString(columns[0]));
                            break;
                        case Boolean:
                            if (currValue == null)
                                currValue = new FxBoolean(multiLang, currLang, rs.getBoolean(columns[0]));
                            else
                                currValue.setTranslation(currLang, rs.getBoolean(columns[0]));
                            break;
                        case Date:
                            if (currValue == null)
                                currValue = new FxDate(multiLang, currLang, rs.getDate(columns[0]));
                            else
                                currValue.setTranslation(currLang, rs.getDate(columns[0]));
                            break;
                        case DateTime:
                            if (currValue == null)
                                currValue = new FxDateTime(multiLang, currLang, rs.getTimestamp(columns[0]));
                            else
                                currValue.setTranslation(currLang, rs.getTimestamp(columns[0]));
                            break;
                        case DateRange:
                            if (currValue == null)
                                currValue = new FxDateRange(multiLang, currLang,
                                        new DateRange(
                                                rs.getDate(columns[0]),
                                                rs.getDate(getColumns(((FxPropertyAssignment) currAssignment).getProperty())[4]))
                                );
                            else
                                currValue.setTranslation(currLang,
                                        new DateRange(
                                                rs.getDate(columns[0]),
                                                rs.getDate(getColumns(((FxPropertyAssignment) currAssignment).getProperty())[4]))
                                );
                            break;
                        case DateTimeRange:
                            if (currValue == null)
                                currValue = new FxDateTimeRange(multiLang, currLang,
                                        new DateRange(
                                                rs.getTimestamp(columns[0]),
                                                rs.getTimestamp(getColumns(((FxPropertyAssignment) currAssignment).getProperty())[7]))
                                );
                            else
                                currValue.setTranslation(currLang,
                                        new DateRange(
                                                rs.getTimestamp(columns[0]),
                                                rs.getTimestamp(getColumns(((FxPropertyAssignment) currAssignment).getProperty())[7]))
                                );
                            break;
                        case Binary:
                            BinaryDescriptor desc = loadBinaryDescriptor(server, con, rs.getLong(columns[0]));
                            if (currValue == null)
                                currValue = new FxBinary(multiLang, currLang, desc);
                            else
                                currValue.setTranslation(currLang, desc);
                            break;
                        case SelectOne:
                            FxSelectListItem singleItem = env.getSelectListItem(rs.getLong(columns[0]));
                            if (currValue == null)
                                currValue = new FxSelectOne(multiLang, currLang, singleItem);
                            else
                                currValue.setTranslation(currLang, singleItem);
                            break;
                        case SelectMany:
                            long itemId = rs.getLong(columns[0]);
                            FxSelectList list = ((FxPropertyAssignment) currAssignment).getProperty().getReferencedList();
                            if (currValue == null)
                                currValue = new FxSelectMany(multiLang, currLang, new SelectMany(list));
                            FxSelectMany sm = (FxSelectMany) currValue;
                            if (sm.isTranslationEmpty(currLang))
                                sm.setTranslation(currLang, new SelectMany(list));
                            if (itemId > 0)
                                sm.getTranslation(currLang).selectItem(list.getItem(itemId));
                            break;
                        case Reference:
                            if (currValue == null)
//                                currValue = new FxReference(multiLang, currLang, new ReferencedContent(rs.getLong(columns[0])));
                                currValue = new FxReference(multiLang, currLang, resolveReference(con, requestedVersion, rs.getLong(columns[0])));
                            else
                                currValue.setTranslation(currLang, resolveReference(con, requestedVersion, rs.getLong(columns[0])));
                            break;
                        default:
                            throw new FxDbException(LOG, "ex.db.notImplemented.load", dataType.getName());
                    }
                    if (isMLDef)
                        defLang = currLang;
                }
            }
            if (currValue != null) {
                //add last property
                currValue.setDefaultLanguage(defLang);
                addValue(root, currXPath, currAssignment, currPos, currValue);
            }
        } catch (FxCreateException e) {
            throw new FxLoadException(e);
        } catch (FxNotFoundException e) {
            throw new FxLoadException(e);
        } finally {
            if (ps != null)
                ps.close();
        }
        return root;
    }

    /**
     * Resolve a reference from <code>pk</code> to <code>referencedId</code>
     *
     * @param con            an open and valid connection
     * @param contentVersion the version of the referencing content
     * @param referencedId   the referenced id
     * @return the referenced content
     * @throws SQLException on errors
     */
    private ReferencedContent resolveReference(Connection con, int contentVersion, long referencedId) throws SQLException {
        String sql = contentVersion == FxPK.LIVE ? CONTENT_REFERENCE_LIVE : CONTENT_REFERENCE_MAX;
        PreparedStatement ps = null;
        int referencedVersion;
        long stepId, aclId, typeId, ownerId;
        String caption;
        try {
            ps = con.prepareStatement(sql);
            ps.setLong(1, referencedId);
            ResultSet rs = ps.executeQuery();
            if (rs != null && rs.next()) {
                referencedVersion = rs.getInt(1);
                aclId = rs.getLong(2);
                stepId = rs.getLong(3);
                typeId = rs.getLong(4);
                ownerId = rs.getLong(5);
            } else if (contentVersion == FxPK.LIVE) {
                ps.close();
                ps = con.prepareStatement(CONTENT_REFERENCE_MAX);
                ps.setLong(1, referencedId);
                rs = ps.executeQuery();
                if (rs != null && rs.next()) {
                    referencedVersion = rs.getInt(1);
                    aclId = rs.getLong(2);
                    stepId = rs.getLong(3);
                    typeId = rs.getLong(4);
                    ownerId = rs.getLong(5);
                } else {
                    LOG.error("Failed to resolve a reference with id " + referencedId + ": no max. version found! (in fallback already!)");
                    return new ReferencedContent(referencedId);
                }
            } else {
                LOG.error("Failed to resolve a reference with id " + referencedId + ": no max. version found!");
                return new ReferencedContent(referencedId);
            }
            ps.close();
            ps = con.prepareStatement(CONTENT_REFERENCE_CAPTION);
            ps.setLong(1, referencedId);
            ps.setInt(2, referencedVersion);
            rs = ps.executeQuery();
            if (rs != null && rs.next())
                caption = rs.getString(1);
            else
                caption = "";
            FxEnvironment env = CacheAdmin.getEnvironment();

            ReferencedContent ref = new ReferencedContent(new FxPK(referencedId, referencedVersion), caption, env.getStep(stepId), env.getACL(aclId));
            try {
                ref.setAccessGranted(
                        FxPermissionUtils.checkPermission(
                                FxContext.get().getTicket(),
                                ownerId, ACL.Permission.READ,
                                env.getType(typeId),
                                ref.getStep().getAclId(),
                                ref.getAcl().getId(),
                                false));
            } catch (FxNoAccessException e) {
                ref.setAccessGranted(false);
            }
            if (LOG.isDebugEnabled()) {
                LOG.debug("ReferencedContent: " + ref.toStringExtended());
            }
            return ref;
        } finally {
            if (ps != null)
                ps.close();
        }
    }

    /**
     * Load a binary descriptor
     *
     * @param server server side StreamServers that can provide the binary
     * @param con    open connection
     * @param id     id of the binary (in FX_BINARY table)
     * @return BinaryDescriptor
     * @throws SQLException  on errors
     * @throws FxDbException on errors
     */
    private BinaryDescriptor loadBinaryDescriptor(List<ServerLocation> server, Connection con, long id) throws SQLException, FxDbException {
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(BINARY_DESC_LOAD);
            ps.setLong(1, id);
            ps.setInt(2, 1); //ver
            ps.setInt(3, 1); //ver
            ResultSet rs = ps.executeQuery();
            if (rs != null && rs.next()) {
                return new BinaryDescriptor(server, id, 1, 1, rs.getLong(4), rs.getString(1), rs.getLong(2), rs.getString(3),
                        rs.getString(5), rs.getBoolean(6), rs.getDouble(7), rs.getInt(8), rs.getInt(9));
            }
        } finally {
            if (ps != null)
                ps.close();
        }
        throw new FxDbException("ex.content.binary.loadDescriptor.failed", id);
    }

    /**
     * Helper method to add a value of a detail entry with a given XPath to the instance being loaded
     *
     * @param root       the root group
     * @param xPath      XPath of the entry
     * @param assignment assignment used
     * @param pos        position in hierarchy
     * @param value      the value to add
     * @throws FxInvalidParameterException on errors
     * @throws FxNotFoundException         on errors
     * @throws FxCreateException           if failed to create group entries
     */
    protected void addValue(FxGroupData root, String xPath, FxAssignment assignment,
                            int pos, FxValue value) throws FxInvalidParameterException, FxNotFoundException, FxCreateException {
        if (assignment instanceof FxGroupAssignment) {
            root.addGroup(xPath, (FxGroupAssignment) assignment, pos);
            root.getGroup(xPath.substring(0, xPath.length() - 1)).removeEmptyEntries(true);
        } else {
            root.addProperty(xPath, (FxPropertyAssignment) assignment, value, pos);
        }
    }

    /**
     * {@inheritDoc}
     */
    public FxPK contentSave(Connection con, FxEnvironment env, StringBuilder sql, FxContent content, long fqnPropertyId) throws FxInvalidParameterException, FxUpdateException {
        content.getRootGroup().removeEmptyEntries();
        content.getRootGroup().compactPositions(true);
        content.checkValidity();
        FxPK pk = content.getPk();
        if (pk.isNew() || !pk.isDistinctVersion())
            throw new FxInvalidParameterException("PK", "ex.content.pk.invalid.save", pk);
        FxDelta delta;
        FxContent original;
        try {
            original = contentLoad(con, content.getPk(), env, sql);
            original.getRootGroup().removeEmptyEntries();
            original.getRootGroup().compactPositions(true);
            delta = FxDelta.processDelta(original, content);
        } catch (FxLoadException e) {
            throw new FxUpdateException(e);
        } catch (FxNotFoundException e) {
            throw new FxUpdateException(e);
        }
        if (original.getStepId() != content.getStepId()) {
            Workflow wf = env.getWorkflow(env.getStep(content.getStepId()).getWorkflowId());
            if (!wf.isRouteValid(original.getStepId(), content.getStepId())) {
                throw new FxInvalidParameterException("STEP", "ex.content.step.noRoute",
                        env.getStepDefinition(env.getStep(original.getStepId()).getStepDefinitionId()).getLabel().getBestTranslation(),
                        env.getStepDefinition(env.getStep(content.getStepId()).getStepDefinitionId()).getLabel().getBestTranslation());
            }
        }
        if (!delta.changes()) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("====== NO CHANGES =======");
            }
            return pk;
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug(delta.dump());
            }
        }

        if (delta.isInternalPropertyChanged())
            updateMainEntry(con, content);
        try {
            disableDetailUniqueChecks(con);
            //full replace code start
//            removeDetailEntriesVersion(con, pk);
//            createDetailEntries(con, env, sql, pk, content.isMaxVersion(), content.isLiveVersion(), content.getData("/"));
            //full replace code end
            boolean checkScripting = env.getType(content.getTypeId()).hasScriptedAssignments();
            FxScriptBinding binding = null;
            ScriptingEngine scripting = null;
            if (checkScripting) {
                scripting = EJBLookup.getScriptingEngine();
                binding = new FxScriptBinding();
                binding.setVariable("content", content);
            }
            //delta-deletes:
            for (FxDelta.FxDeltaChange change : delta.getRemoves()) {
                if (checkScripting)
                    for (long scriptId : change.getOriginalData().getAssignment().
                            getScriptMapping(FxScriptEvent.BeforeDataChangeDelete)) {
                        binding.setVariable("change", change);
                        scripting.runScript(scriptId, binding);
                    }
                if (!change.getOriginalData().isSystemInternal())
                    deleteDetailData(con, sql, pk, change.getOriginalData());
                if (checkScripting)
                    for (long scriptId : change.getOriginalData().getAssignment().
                            getScriptMapping(FxScriptEvent.AfterDataChangeDelete)) {
                        binding.setVariable("change", change);
                        scripting.runScript(scriptId, binding);
                    }
            }

            //delta-updates:
            List<FxDelta.FxDeltaChange> updatesRemaining = new ArrayList<FxDelta.FxDeltaChange>(delta.getUpdates());

            while (updatesRemaining.size() > 0) {
                FxDelta.FxDeltaChange change = updatesRemaining.get(0);
                //noinspection CaughtExceptionImmediatelyRethrown
                try {
                    if (checkScripting)
                        for (long scriptId : change.getOriginalData().getAssignment().
                                getScriptMapping(FxScriptEvent.BeforeDataChangeUpdate)) {
                            binding.setVariable("change", change);
                            scripting.runScript(scriptId, binding);
                        }
                    if (!change.getOriginalData().isSystemInternal()) {
                        if (change.isGroup()) {
                            if (change.isPositionChange() && !change.isDataChange()) {
                                //groups can only change position
                                updatePropertyData(change, null, null, con, sql, pk, null);
                            }
                        } else {
                            FxProperty prop = env.getProperty(((FxPropertyData) change.getNewData()).getPropertyId());
                            if (!change._isUpdateable()) {
                                deleteDetailData(con, sql, pk, change.getOriginalData());
                                insertPropertyData(prop, content.getData("/"), con, sql, pk,
                                        ((FxPropertyData) change.getNewData()),
                                        content.isMaxVersion(), content.isLiveVersion());
                            } else {
                                updatePropertyData(change, prop, content.getData("/"), con, sql, pk,
                                        ((FxPropertyData) change.getNewData()));
                            }
                            //check if the property changed is a FQN
                            if (prop.getId() == fqnPropertyId) {
                                FxValue val = ((FxPropertyData) change.getNewData()).getValue();
                                if (!val.isEmpty() && val instanceof FxString)
                                    StorageManager.getTreeStorage().syncFQNName(con, pk.getId(), content.isMaxVersion(), content.isLiveVersion(), (String) val.getBestTranslation());
                            }
                        }
                    }
                    if (checkScripting)
                        for (long scriptId : change.getOriginalData().getAssignment().
                                getScriptMapping(FxScriptEvent.AfterDataChangeUpdate)) {
                            binding.setVariable("change", change);
                            scripting.runScript(scriptId, binding);
                        }
                    updatesRemaining.remove(0);
                } catch (SQLException e) {
                    change._increaseRetries();
                    if (change._getRetryCount() > 100)
                        throw e;
                    updatesRemaining.remove(0);
                    updatesRemaining.add(change); //add as last
                }
            }

            //delta-adds:
            for (FxDelta.FxDeltaChange change : delta.getAdds()) {
                if (checkScripting)
                    for (long scriptId : change.getNewData().getAssignment().
                            getScriptMapping(FxScriptEvent.BeforeDataChangeAdd)) {
                        binding.setVariable("change", change);
                        scripting.runScript(scriptId, binding);
                    }
                if (!change.getNewData().isSystemInternal()) {
                    if (change.isGroup())
                        insertGroupData(con, sql, pk, (FxGroupData) change.getNewData(), content.isMaxVersion(), content.isLiveVersion());
                    else
                        insertPropertyData(env.getProperty(((FxPropertyData) change.getNewData()).getPropertyId()),
                                content.getData("/"), con, sql, pk, ((FxPropertyData) change.getNewData()),
                                content.isMaxVersion(), content.isLiveVersion());
                }
                if (checkScripting)
                    for (long scriptId : change.getNewData().getAssignment().
                            getScriptMapping(FxScriptEvent.AfterDataChangeAdd)) {
                        binding.setVariable("change", change);
                        scripting.runScript(scriptId, binding);
                    }
            }

            checkUniqueConstraints(con, env, sql, pk, content.getTypeId());
            if (delta.isInternalPropertyChanged()) {
                updateStepDependencies(con, content.getPk().getId(), content.getPk().getVersion(), env, content.getStepId());
                fixContentVersionStats(con, content.getPk().getId());
            }
            content.resolveBinaryPreview();
            if (original.getBinaryPreviewId() != content.getBinaryPreviewId() ||
                    original.getBinaryPreviewACL() != content.getBinaryPreviewACL())
                updateContentBinaryEntry(con, pk, content.getBinaryPreviewId(), content.getBinaryPreviewACL());
            enableDetailUniqueChecks(con);
            LifeCycleInfoImpl.updateLifeCycleInfo(TBL_CONTENT, "ID", "VER",
                    content.getPk().getId(), content.getPk().getVersion(), false, false);
        } catch (FxCreateException e) {
            throw new FxUpdateException(e);
        } catch (FxApplicationException e) {
            throw new FxUpdateException(e);
        } catch (SQLException e) {
            throw new FxUpdateException(LOG, e, "ex.db.sqlError", e.getMessage());
        } catch (Exception e) {
            throw new FxUpdateException(LOG, e, "ex.content.save.error", pk, e);
        }
        return content.getPk();
    }

    private void enableDetailUniqueChecks(Connection con) throws SQLException {
        Statement stmt = null;
        try {
            stmt = con.createStatement();
            stmt.executeUpdate("SET UNIQUE_CHECKS=1");
        } finally {
            if (stmt != null)
                stmt.close();
        }
    }

    private void disableDetailUniqueChecks(Connection con) throws SQLException {
        Statement stmt = null;
        try {
            stmt = con.createStatement();
            stmt.executeUpdate("SET UNIQUE_CHECKS=0");
        } finally {
            if (stmt != null)
                stmt.close();
        }
    }

    protected void updateContentBinaryEntry(Connection con, FxPK pk, long binaryId, long binaryACL) throws FxUpdateException {
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(CONTENT_MAIN_BINARY_UPDATE);
            ps.setLong(1, binaryId);
            ps.setInt(2, 1); //ver
            ps.setInt(3, 1); //quality
            ps.setLong(4, binaryACL);
            ps.setLong(5, pk.getId());
            ps.setInt(6, pk.getVersion());
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxUpdateException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * Update the main entry
     *
     * @param con     an open and valid connection
     * @param content content to create
     * @throws FxUpdateException on errors
     */
    protected void updateMainEntry(Connection con, FxContent content) throws FxUpdateException {
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(CONTENT_MAIN_UPDATE);
            ps.setLong(18, content.getPk().getId());
            ps.setInt(19, content.getPk().getVersion());
            ps.setLong(1, content.getTypeId());
            ps.setLong(2, content.getAclId());
            ps.setLong(3, content.getStepId());
            ps.setInt(4, content.getVersion());
            ps.setInt(5, content.isLiveVersion() ? 1 : 0);
            ps.setBoolean(6, content.isMaxVersion());
            ps.setBoolean(7, content.isLiveVersion());
            ps.setBoolean(8, content.isActive());
            ps.setInt(9, (int) content.getMainLanguage());
            if (content.isRelation()) {
                ps.setLong(10, content.getRelatedSource().getId());
                ps.setInt(11, content.getRelatedSource().getVersion());
                ps.setLong(12, content.getRelatedDestination().getId());
                ps.setInt(13, content.getRelatedDestination().getVersion());
                ps.setLong(14, content.getRelatedSourcePosition());
                ps.setLong(15, content.getRelatedDestinationPosition());
            } else {
                ps.setNull(10, java.sql.Types.NUMERIC);
                ps.setNull(11, java.sql.Types.NUMERIC);
                ps.setNull(12, java.sql.Types.NUMERIC);
                ps.setNull(13, java.sql.Types.NUMERIC);
                ps.setNull(14, java.sql.Types.NUMERIC);
                ps.setNull(15, java.sql.Types.NUMERIC);
            }
            long userId = FxContext.get().getTicket().getUserId();
            ps.setLong(16, userId);
            ps.setLong(17, System.currentTimeMillis());
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxUpdateException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * Remove all detail entries of a distinct version
     *
     * @param con an open and valid connection
     * @param pk  primary key of the detail data
     * @throws FxUpdateException on errors
     */
    protected void removeDetailEntriesVersion(Connection con, FxPK pk) throws FxUpdateException {
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(CONTENT_DATA_FT_REMOVE_VERSION);
            ps.setLong(1, pk.getId());
            ps.setInt(2, pk.getVersion());
            ps.executeUpdate();
            ps.close();
            ps = con.prepareStatement(CONTENT_DATA_REMOVE_VERSION);
            ps.setLong(1, pk.getId());
            ps.setInt(2, pk.getVersion());
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxUpdateException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * {@inheritDoc}
     */
    public FxContentSecurityInfo getContentSecurityInfo(Connection con, FxPK pk) throws FxLoadException, FxNotFoundException {
        PreparedStatement ps = null;
        try {
            switch (pk.getVersion()) {
                case FxPK.MAX:
                    ps = con.prepareStatement(SECURITY_INFO_MAXVER);
                    break;
                case FxPK.LIVE:
                    ps = con.prepareStatement(SECURITY_INFO_LIVEVER);
                    break;
                default:
                    ps = con.prepareStatement(SECURITY_INFO_VER);
                    ps.setInt(2, pk.getVersion());
            }
            ps.setLong(1, pk.getId());
            byte typePerm;
            int typeACL, contentACL, stepACL, previewACL;
            long previewId, typeId, ownerId, mandatorId;
            long[] propertyPerm;
            ResultSet rs = ps.executeQuery();
            if (rs == null || !rs.next())
                throw new FxNotFoundException("ex.content.notFound", pk);
            contentACL = rs.getInt(1);
            typeACL = rs.getInt(2);
            stepACL = rs.getInt(3);
            typePerm = rs.getByte(4);
            typeId = rs.getLong(5);
            previewId = rs.getLong(6);
            previewACL = rs.getInt(7);
            ownerId = rs.getLong(8);
            mandatorId = rs.getLong(9);
            if (rs.next())
                throw new FxLoadException("ex.db.resultSet.tooManyRows");
            if ((typePerm & 0x02) == 0x02) {
                //use property permissions
                ps.close();
                switch (pk.getVersion()) {
                    case FxPK.MAX:
                        ps = con.prepareStatement(SECURITY_INFO_PROP_MAXVER);
                        break;
                    case FxPK.LIVE:
                        ps = con.prepareStatement(SECURITY_INFO_PROP_LIVEVER);
                        break;
                    default:
                        ps = con.prepareStatement(SECURITY_INFO_PROP_VER);
                        ps.setInt(2, pk.getVersion());
                }
                ps.setLong(1, pk.getId());
                ArrayList<Long> alPropACL = new ArrayList<Long>(10);
                ResultSet rsProp = ps.executeQuery();
                while (rsProp != null && rsProp.next())
                    alPropACL.add(rsProp.getLong(1));
                propertyPerm = new long[alPropACL.size()];
                int cnt = 0;
                for (long acl : alPropACL)
                    propertyPerm[cnt++] = acl;
            } else
                propertyPerm = new long[0];
            return new FxContentSecurityInfo(pk, ownerId, previewId, typeId, mandatorId, typePerm, typeACL, stepACL, contentACL, previewACL, propertyPerm);
        } catch (SQLException e) {
            throw new FxLoadException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }


    /**
     * {@inheritDoc}
     */
    public void contentRemove(Connection con, FxPK pk) throws FxRemoveException {
        PreparedStatement ps = null;
        try {
            //sync with tree
            StorageManager.getTreeStorage().contentRemoved(con, pk.getId(), false);
            ps = con.prepareStatement(CONTENT_DATA_FT_REMOVE);
            ps.setLong(1, pk.getId());
            ps.executeUpdate();
            ps.close();
            List<Long> binaries = null;
            ps = con.prepareStatement(CONTENT_BINARY_REMOVE_GET);
            ps.setLong(1, pk.getId());
            ResultSet rs = ps.executeQuery();
            while (rs != null && rs.next()) {
                if (binaries == null)
                    binaries = new ArrayList<Long>(20);
                binaries.add(rs.getLong(1));
            }
            ps.close();
            ps = con.prepareStatement(CONTENT_DATA_REMOVE);
            ps.setLong(1, pk.getId());
            ps.executeUpdate();
            ps.close();
            if (binaries != null) {
                ps = con.prepareStatement(CONTENT_BINARY_REMOVE_ID);
                for (Long id : binaries) {
                    ps.setLong(1, id);
                    try {
                        ps.executeUpdate();
                    } catch (SQLException e) {
                        //ok, might still be in use elsewhere
                    }
                }
                ps.close();
            }
            ps = con.prepareStatement(CONTENT_MAIN_REMOVE);
            ps.setLong(1, pk.getId());
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxRemoveException(LOG, e, "ex.db.sqlError", e.getMessage());
        } catch (FxApplicationException e) {
            throw new FxRemoveException(e);
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void contentRemoveVersion(Connection con, FxPK pk) throws FxRemoveException, FxNotFoundException {
        FxContentVersionInfo cvi = getContentVersionInfo(con, pk.getId());
        if (!cvi.containsVersion(pk))
            return;

        int ver = pk.getVersion();
        if (!pk.isDistinctVersion())
            ver = cvi.getDistinctVersion(pk.getVersion());
        PreparedStatement ps = null;
        try {
            //if its the live version - sync with live tree
            if (cvi.hasLiveVersion() && cvi.getLiveVersion() == ver)
                StorageManager.getTreeStorage().contentRemoved(con, pk.getId(), true);
            ps = con.prepareStatement(CONTENT_DATA_FT_REMOVE_VER);
            ps.setLong(1, pk.getId());
            ps.setInt(2, ver);
            ps.executeUpdate();
            ps.close();
            ps = con.prepareStatement(CONTENT_BINARY_REMOVE_VER);
            ps.setLong(1, pk.getId());
            ps.setInt(2, ver);
            ps.executeUpdate();
            ps.close();
            ps = con.prepareStatement(CONTENT_DATA_REMOVE_VER);
            ps.setLong(1, pk.getId());
            ps.setInt(2, ver);
            ps.executeUpdate();
            ps.close();
            ps = con.prepareStatement(CONTENT_MAIN_REMOVE_VER);
            ps.setLong(1, pk.getId());
            ps.setInt(2, ver);
            if (ps.executeUpdate() > 0)
                fixContentVersionStats(con, pk.getId());
        } catch (SQLException e) {
            throw new FxRemoveException(LOG, e, "ex.db.sqlError", e.getMessage());
        } catch (FxApplicationException e) {
            throw new FxRemoveException(e);
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * {@inheritDoc}
     */
    public int contentRemoveForType(Connection con, FxType type) throws FxRemoveException {
        PreparedStatement ps = null;
        try {
            //FX-96 - select all contents that are referenced from the tree
            ps = con.prepareStatement("SELECT DISTINCT c.ID FROM " + DatabaseConst.TBL_CONTENT + " c, " +
                    DatabaseConst.TBL_TREE + " te, " + DatabaseConst.TBL_TREE +
                    "_LIVE tl WHERE (te.REF=c.ID or tl.REF=c.ID) AND c.TDEF=?");
            ps.setLong(1, type.getId());
            ResultSet rs = ps.executeQuery();
            while (rs != null && rs.next())
                StorageManager.getTreeStorage().contentRemoved(con, rs.getLong(1), false);
            ps.close();

            ps = con.prepareStatement(CONTENT_DATA_FT_REMOVE_TYPE);
            ps.setLong(1, type.getId());
            ps.executeUpdate();
            ps.close();

            List<Long> binaries = null;
            ps = con.prepareStatement(CONTENT_BINARY_REMOVE_TYPE_GET);
            ps.setLong(1, type.getId());
            rs = ps.executeQuery();
            while (rs != null && rs.next()) {
                if (binaries == null)
                    binaries = new ArrayList<Long>(20);
                binaries.add(rs.getLong(1));
            }
            ps.close();
            ps = con.prepareStatement(CONTENT_DATA_REMOVE_TYPE);
            ps.setLong(1, type.getId());
            ps.executeUpdate();
            ps.close();
            if (binaries != null) {
                ps = con.prepareStatement(CONTENT_BINARY_REMOVE_ID);
                for (Long id : binaries) {
                    ps.setLong(1, id);
                    try {
                        ps.executeUpdate();
                    } catch (SQLException e) {
                        //ok, might still be in use elsewhere
                    }
                }
                ps.close();
            }
            ps = con.prepareStatement(CONTENT_MAIN_REMOVE_TYPE);
            ps.setLong(1, type.getId());
            return ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxRemoveException(LOG, e, "ex.db.sqlError", e.getMessage());
        } catch (FxApplicationException e) {
            throw new FxRemoveException(e);
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * {@inheritDoc}
     */
    public List<FxPK> getPKsForType(Connection con, FxType type, boolean onePkPerInstance) throws FxDbException {
        PreparedStatement ps = null;
        List<FxPK> pks = new ArrayList<FxPK>(50);
        try {
            if (onePkPerInstance)
                ps = con.prepareStatement(CONTENT_TYPE_PK_RETRIEVE_IDS);
            else
                ps = con.prepareStatement(CONTENT_TYPE_PK_RETRIEVE_VERSIONS);

            ps.setLong(1, type.getId());
            ResultSet rs = ps.executeQuery();
            while (rs != null && rs.next()) {
                if (onePkPerInstance)
                    pks.add(new FxPK(rs.getLong(1)));
                else
                    pks.add(new FxPK(rs.getLong(1), rs.getInt(2)));
            }
            return pks;
        } catch (SQLException e) {
            throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void maintenance(Connection con) {
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(CONTENT_BINARY_TRANSIT_CLEANUP);
            ps.setLong(1, System.currentTimeMillis());
            int count = ps.executeUpdate();
            if (count > 0)
                LOG.info(count + " expired binary transit entries removed");
        } catch (SQLException e) {
            LOG.error(e, e);
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }


    /**
     * {@inheritDoc}
     */
    public void prepareSave(Connection con, FxContent content) throws FxInvalidParameterException, FxDbException {
        // key: handle, value: [mimeType,metaData]
        Map<String, String[]> mimeMetaMap = new HashMap<String, String[]>(5);
        try {
            for (FxData data : content.getRootGroup().getChildren())
                _prepareSave(mimeMetaMap, con, data);
        } catch (SQLException e) {
            throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage());
        }
    }

    private void _prepareSave(Map<String, String[]> mimeMetaMap, Connection con, FxData data) throws SQLException {
        if (data instanceof FxGroupData)
            for (FxData sub : ((FxGroupData) data).getChildren())
                _prepareSave(mimeMetaMap, con, sub);
        else {
            if (!data.isEmpty() && ((FxPropertyData) data).getValue() instanceof FxBinary) {
                FxBinary bin = (FxBinary) ((FxPropertyData) data).getValue();
                for (long lang : bin.getTranslatedLanguages()) {
                    BinaryDescriptor bd = bin.getTranslation(lang);
                    if (!bd.isNewBinary())
                        continue;
                    if (mimeMetaMap.containsKey(bd.getHandle())) {
                        String[] mm = mimeMetaMap.get(bd.getHandle());
                        BinaryDescriptor bdNew = new BinaryDescriptor(bd.getHandle(), bd.getName(), bd.getSize(), mm[0], mm[1]);
                        bin.setTranslation(lang, bdNew);
                    } else {
                        BinaryDescriptor bdNew = identifyAndTransferTransitBinary(con, bd);
                        bin.setTranslation(lang, bdNew);
                        mimeMetaMap.put(bdNew.getHandle(), new String[]{bdNew.getMimeType(), bdNew.getMetadata()});
                    }
                }

            }
        }
    }

    /**
     * Identifies a binary in the transit table and generates previews etc.
     *
     * @param con    an open and valid Connection
     * @param binary the binary to identify
     * @return BinaryDescriptor
     * @throws SQLException on errors
     */
    private BinaryDescriptor identifyAndTransferTransitBinary(Connection con, BinaryDescriptor binary) throws SQLException {
        PreparedStatement ps;
        //check if already identified
        if (!StringUtils.isEmpty(binary.getMetadata()))
            return binary;
        ps = con.prepareStatement(BINARY_TRANSIT_HEADER);
        ps.setString(1, binary.getHandle());
        ResultSet rs = ps.executeQuery();
        String mimeType = "unknown";
        if (rs != null && rs.next()) {
            byte[] header = null;
            try {
                header = rs.getBlob(1).getBytes(1, 48);
            } catch (Throwable t) {
                // ignore, header migth be smaller than 48
            }
            mimeType = FxMediaEngine.detectMimeType(header, binary.getName());
        }
        String metaData = "<empty/>";

        File binaryFile = null;
        File previewFile1 = null, previewFile2 = null, previewFile3 = null;
        InputStream in = null;
        FileOutputStream fos = null;
        FileInputStream pin1 = null, pin2 = null, pin3 = null;
        int[] dimensionsPreview1 = {0, 0};
        int[] dimensionsPreview2 = {0, 0};
        int[] dimensionsPreview3 = {0, 0};
        try {
            binaryFile = File.createTempFile("FXBIN_", "_TEMP");
            in = rs.getBlob(1).getBinaryStream();
            fos = new FileOutputStream(binaryFile);
            byte[] buffer = new byte[4096];
            int read;
            while ((read = in.read(buffer)) != -1)
                fos.write(buffer, 0, read);
            fos.close();
            fos = null;
            in.close();
            in = null;
            boolean processed = false;
            boolean useDefaultPreview = true;
            int defaultId = BinaryDescriptor.SYS_UNKNOWN;

            FxScriptBinding binding;
            ScriptingEngine scripting = EJBLookup.getScriptingEngine();
            for (long script : scripting.getByScriptType(FxScriptEvent.BinaryPreviewProcess)) {
                binding = new FxScriptBinding();
                binding.setVariable("processed", processed);
                binding.setVariable("useDefaultPreview", useDefaultPreview);
                binding.setVariable("defaultId", defaultId);
                binding.setVariable("mimeType", mimeType);
                binding.setVariable("metaData", metaData);
                binding.setVariable("binaryFile", binaryFile);
                binding.setVariable("previewFile1", previewFile1);
                binding.setVariable("previewFile2", previewFile2);
                binding.setVariable("previewFile3", previewFile3);
                binding.setVariable("dimensionsPreview1", dimensionsPreview1);
                binding.setVariable("dimensionsPreview2", dimensionsPreview2);
                binding.setVariable("dimensionsPreview3", dimensionsPreview3);
                try {
                    FxScriptResult result = scripting.runScript(script, binding);
                    binding = result.getBinding();
                    processed = (Boolean) binding.getVariable("processed");
                    if (processed) {
                        useDefaultPreview = (Boolean) binding.getVariable("useDefaultPreview");
                        defaultId = (Integer) binding.getVariable("defaultId");
                        previewFile1 = (File) binding.getVariable("previewFile1");
                        previewFile2 = (File) binding.getVariable("previewFile2");
                        previewFile3 = (File) binding.getVariable("previewFile3");
                        dimensionsPreview1 = (int[]) binding.getVariable("dimensionsPreview1");
                        dimensionsPreview2 = (int[]) binding.getVariable("dimensionsPreview2");
                        dimensionsPreview3 = (int[]) binding.getVariable("dimensionsPreview3");
                        metaData = (String) binding.getVariable("metaData");
                        break;
                    }
                } catch (Throwable e) {
                    LOG.error("Error running binary processing script: " + e.getMessage());
                }
            }
            //check if values for preview are valid
            if (!useDefaultPreview) {
                if (previewFile1 == null || !previewFile1.exists() ||
                        previewFile2 == null || !previewFile2.exists() ||
                        previewFile3 == null || !previewFile3.exists() ||
                        dimensionsPreview1 == null || dimensionsPreview1.length != 2 || dimensionsPreview1[0] < 0 || dimensionsPreview1[1] < 0 ||
                        dimensionsPreview1 == null || dimensionsPreview2.length != 2 || dimensionsPreview2[0] < 0 || dimensionsPreview2[1] < 0 ||
                        dimensionsPreview1 == null || dimensionsPreview3.length != 2 || dimensionsPreview3[0] < 0 || dimensionsPreview3[1] < 0) {
                    LOG.warn("Invalid preview parameters! Setting to default/unknown!");
                    useDefaultPreview = true;
                    defaultId = BinaryDescriptor.SYS_UNKNOWN;
                }
            } else {
                //only negative values are allowed
                if (defaultId >= 0) {
                    defaultId = BinaryDescriptor.SYS_UNKNOWN;
                    LOG.warn("Only default preview id's that are negative and defined in BinaryDescriptor as constants are allowed!");
                }
            }

            if (!useDefaultPreview) {
                ps.close();
                ps = con.prepareStatement(BINARY_TRANSIT_PREVIEWS);
                pin1 = new FileInputStream(previewFile1);
                pin2 = new FileInputStream(previewFile2);
                pin3 = new FileInputStream(previewFile3);
                ps.setBinaryStream(1, pin1, (int) previewFile1.length());
                ps.setInt(2, dimensionsPreview1[0]);
                ps.setInt(3, dimensionsPreview1[1]);
                ps.setBinaryStream(4, pin2, (int) previewFile2.length());
                ps.setInt(5, dimensionsPreview2[0]);
                ps.setInt(6, dimensionsPreview2[1]);
                ps.setBinaryStream(7, pin3, (int) previewFile3.length());
                ps.setInt(8, dimensionsPreview3[0]);
                ps.setInt(9, dimensionsPreview3[1]);
                ps.setInt(10, (int) previewFile1.length());
                ps.setInt(11, (int) previewFile2.length());
                ps.setInt(12, (int) previewFile3.length());
                ps.setString(13, binary.getHandle());
                ps.executeUpdate();
            } else {
                ps.close();
                ps = con.prepareStatement(BINARY_TRANSIT_PREVIEWS_REF);
                ps.setLong(1, defaultId);
                ps.setString(2, binary.getHandle());
                ps.executeUpdate();
            }
        } catch (IOException e) {
            LOG.error("Stream reading failed:" + e.getMessage(), e);
//        } catch (XMLStreamException e) {
//            LOG.error("XMLStream processing failed: " + e.getMessage(), e);
        } finally {
            try {
                if (fos != null)
                    fos.close();
                if (in != null)
                    in.close();
                if (pin1 != null)
                    pin1.close();
                if (pin2 != null)
                    pin2.close();
                if (pin3 != null)
                    pin3.close();
            } catch (IOException e) {
                LOG.error("Stream closing failed: " + e.getMessage(), e);
            }
            if (binaryFile != null && !binaryFile.delete())
                binaryFile.deleteOnExit();
            if (previewFile1 != null && !previewFile1.delete())
                previewFile1.deleteOnExit();
            if (previewFile2 != null && !previewFile2.delete())
                previewFile2.deleteOnExit();
            if (previewFile3 != null && !previewFile3.delete())
                previewFile3.deleteOnExit();
            if (ps != null)
                ps.close();
        }
        //TODO: if we have a word, excel or powerpoint extract all possible infos and put them into the metaData
        return new BinaryDescriptor(binary.getHandle(), binary.getName(), binary.getSize(), mimeType, metaData);
    }

    /**
     * {@inheritDoc}
     */
    public void storeBinary(Connection con, long id, int version, int quality, String name, long length, InputStream binary) throws FxApplicationException {
//        System.out.println("INSTALLING ["+name+"] length="+length);
        try {
            BinaryUploadPayload payload = FxStreamUtils.uploadBinary(length, binary);
            BinaryDescriptor desc = new BinaryDescriptor(payload.getHandle(), name, length, null, null);
            desc = identifyAndTransferTransitBinary(con, desc);
            binaryTransit(con, desc, id, version, quality);
        } catch (SQLException e) {
            throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage());
        }
    }

    /**
     * {@inheritDoc}
     */
    public void updateBinaryPreview(Connection con, long id, int version, int quality, int preview, int width, int height, long length, InputStream binary) {
        //TODO: code me!
    }

    /**
     * Check all unique constraints for an instance
     *
     * @param con    an open and valid connection
     * @param env    environment
     * @param sql    StringBuilder for performance
     * @param pk     primary key of the affected instance
     * @param typeId affected FxType
     * @throws FxApplicationException on errors
     */
    private void checkUniqueConstraints(Connection con, FxEnvironment env, StringBuilder sql, FxPK pk, long typeId) throws FxApplicationException {
        FxType type = env.getType(typeId);
        if (!type.hasUniqueProperties())
            return;
        List<FxProperty> uniques = type.getUniqueProperties();
        if (sql == null)
            sql = new StringBuilder(500);
        else
            sql.setLength(0);
        try {
            for (FxProperty prop : uniques) {
                sql.setLength(0);
                uniqueConditionsMet(con, env, sql, prop.getUniqueMode(), prop, typeId, pk, true);
            }
        } catch (SQLException e) {
            throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage());
        }
    }

    /**
     * Check if unique constraints are met
     *
     * @param con            an open and valid Connection
     * @param env            environment
     * @param sql            a StringBuilder instance
     * @param mode           UniqueMode
     * @param prop           the propery to check
     * @param typeId         type to check
     * @param pk             primary key (optional)
     * @param throwException should an exception be thrown if conditions are not met?
     * @return conditions met
     * @throws SQLException           on errors
     * @throws FxApplicationException on errors
     */
    private boolean uniqueConditionsMet(Connection con, FxEnvironment env, StringBuilder sql, UniqueMode mode,
                                        FxProperty prop, long typeId, FxPK pk, boolean throwException)
            throws SQLException, FxApplicationException {
        String typeChecks = null;
        sql.setLength(0);
        switch (mode) {
            case Global:
                sql.append("SELECT tcd.XPATHMULT, COUNT(DISTINCT ccd.ID) FROM ").append(TBL_CONTENT_DATA).
                        append(" ccd, ").append(TBL_CONTENT_DATA).append(" tcd WHERE ccd.TPROP=").
                        append(prop.getId()).append(" AND ccd.TPROP=tcd.TPROP AND ccd.ID<>tcd.ID AND tcd.ID=").append(pk.getId()).
                        append(" AND ccd.LANG=tcd.LANG");
                break;
            case DerivedTypes:
                //gen list of parent and derived types
                typeChecks = buildTypeHierarchy(env.getType(typeId));
            case Type:
                if (typeChecks == null)
                    typeChecks = "" + typeId;
                sql.append("SELECT tcd.XPATHMULT, COUNT(DISTINCT ccd.ID) FROM ").append(TBL_CONTENT_DATA).
                        append(" ccd, ").append(TBL_CONTENT_DATA).append(" tcd, ").append(TBL_CONTENT).
                        append(" cc, ").append(TBL_CONTENT).
                        append(" tc WHERE cc.ID=ccd.ID AND tc.ID=tcd.ID AND cc.TDEF IN (").
                        append(typeChecks).append(") AND tc.TDEF IN (").append(typeChecks).
                        append(") AND ccd.TPROP=").append(prop.getId()).
                        append(" AND ccd.TPROP=tcd.TPROP AND ccd.LANG=tcd.LANG").
                        //prevent checks across versions
                        append(" AND NOT(ccd.ID=tcd.ID AND ccd.VER<>tcd.VER)").
                        //prevent self-references
                        append(" AND NOT(ccd.ID=tcd.ID AND ccd.VER=tcd.VER AND ccd.ASSIGN=tcd.ASSIGN AND tcd.XMULT=ccd.XMULT)");
                break;
            case Instance:
                sql.append("SELECT tcd.XPATHMULT FROM ").append(TBL_CONTENT_DATA).append(" ccd, ").
                        append(TBL_CONTENT_DATA).append(" tcd WHERE ccd.TPROP=").append(prop.getId()).
                        append(" AND ccd.TPROP=tcd.TPROP AND ccd.ID=tcd.ID AND ccd.VER=tcd.VER AND ccd.XPATHMULT<>tcd.XPATHMULT");
                if (pk != null)
                    sql.append(" AND tcd.ID=").append(pk.getId());
                sql.append(" AND ccd.LANG=tcd.LANG");
                break;
        }
        if (sql.length() == 0)
            return true;
        addColumnComparator(sql, prop, "ccd", "tcd");
        sql.append(" GROUP BY tcd.XPATHMULT");
        //noinspection CaughtExceptionImmediatelyRethrown
        try {
            doCheckUniqueConstraint(con, sql.toString(), prop.getUniqueMode());
        } catch (FxApplicationException e) {
            LOG.warn("SQL:\n"+sql);
            if (throwException)
                throw e;
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public boolean uniqueConditionValid(Connection con, UniqueMode mode, FxProperty prop, long typeId, FxPK pk) {
        try {
            return uniqueConditionsMet(con, CacheAdmin.getEnvironment(), new StringBuilder(500), mode, prop, typeId, pk, false);
        } catch (SQLException e) {
            throw new FxApplicationException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } catch (FxApplicationException e) {
            throw e.asRuntimeException();
        }
    }

    /**
     * Helper to build a comma seperated list of all parent and child types and the current type
     *
     * @param type current type to examine
     * @return comma seperated list of all parent and child types and the current type
     */
    private String buildTypeHierarchy(FxType type) {
        StringBuilder th = new StringBuilder(100);
        FxType parent = type.getParent();
        th.append(type.getId());
        while (parent != null) {
            th.append(",").append(parent.getId());
            parent = parent.getParent();
        }
        buildTypeChildren(th, type);
        return th.toString();
    }

    /**
     * Build a list of all derived types and the current type
     *
     * @param th   StringBuilder that should contain the result
     * @param type current type
     */
    private void buildTypeChildren(StringBuilder th, FxType type) {
        for (FxType child : type.getDerivedTypes()) {
            th.append(',').append(child.getId());
            buildTypeChildren(th, child);
        }
    }

    private void doCheckUniqueConstraint(Connection con, String sql, UniqueMode uniqueMode) throws SQLException, FxApplicationException {
        Statement s = null;
        try {
            s = con.createStatement();
            ResultSet rs = s.executeQuery(sql);
            if (rs != null && rs.next()) {
                if (uniqueMode == UniqueMode.Instance || rs.getInt(2) > 0)
                    throw new FxConstraintViolationException("ex.content.contraint.unique.xpath", rs.getString(1), uniqueMode).setAffectedXPath(rs.getString(1));
            }
        } finally {
            if (s != null)
                try {
                    s.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * Compare a property for two database aliases
     *
     * @param sql       StringBuilder to append the comparison to
     * @param prop      propery to compare
     * @param compAlias compare alias
     * @param ownAlias  own alias
     */
    private void addColumnComparator(StringBuilder sql, FxProperty prop, String compAlias, String ownAlias) {
        String ucol = getUppercaseColumn(prop);
        if (ucol == null)
            for (String col : getColumns(prop))
                sql.append(" AND ").append(compAlias).append(".").append(col).append("=").append(ownAlias).append(".").append(col);
        else
            sql.append(" AND ").append(compAlias).append(".").append(ucol).append("=").append(ownAlias).append(".").append(ucol);
    }

    /**
     * {@inheritDoc}
     */
    public int getReferencedContentCount(Connection con, long id) throws FxDbException {
        Statement s = null;
        int count = 0;
        try {
            s = con.createStatement();
            //references within contents
            ResultSet rs = s.executeQuery("SELECT DISTINCT ID FROM " + TBL_CONTENT_DATA + " WHERE FREF=" + id);
            while (rs != null && rs.next())
                count++;
            //Edit tree references
            rs = s.executeQuery("SELECT DISTINCT ID FROM " + TBL_TREE + " WHERE REF=" + id);
            while (rs != null && rs.next())
                count++;
            //Live tree references
            rs = s.executeQuery("SELECT DISTINCT ID FROM " + TBL_TREE + "_LIVE WHERE REF=" + id);
            while (rs != null && rs.next())
                count++;
            //Contact Data references
            rs = s.executeQuery("SELECT DISTINCT ID FROM " + TBL_ACCOUNTS + " WHERE CONTACT_ID=" + id);
            while (rs != null && rs.next())
                count++;
            //Briefcase references
            rs = s.executeQuery("SELECT DISTINCT BRIEFCASE_ID FROM " + TBL_BRIEFCASE_DATA + " WHERE ID=" + id);
            while (rs != null && rs.next())
                count++;
            return count;
        } catch (SQLException e) {
            throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            if (s != null)
                try {
                    s.close();
                } catch (SQLException e) {
                    //ignore
                }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void updateXPath(Connection con, long assignmentId, String originalXPath, String newXPath) throws FxUpdateException, FxInvalidParameterException {
        LOG.info("Updating all instances from [" + originalXPath + "] to [" + newXPath + "]...");
        PreparedStatement psRead = null, psWrite = null;
//        int count = 0;
        List<XPathElement> xorg = XPathElement.split(originalXPath);
        List<XPathElement> xnew = XPathElement.split(newXPath);
        if (xorg.size() != xnew.size())
            throw new FxInvalidParameterException("newXPath", "ex.content.xpath.update.mismatch.size", originalXPath, newXPath);
        try {
            //                                    1     2  3   4    5   6
            psRead = con.prepareStatement("SELECT XMULT,ID,VER,LANG,POS,TPROP FROM " + TBL_CONTENT_DATA + " WHERE ASSIGN=?");
            //                                                                        1           2             3          4         5          6         7           8            9
            psWrite = con.prepareStatement("UPDATE " + TBL_CONTENT_DATA + " SET XPATH=?,XPATHMULT=?,PARENTXPATH=? WHERE ID=? AND VER=? AND LANG=? AND POS=? AND XMULT=? AND ASSIGN=?");

            psRead.setLong(1, assignmentId);
            ResultSet rs = psRead.executeQuery();
            boolean isGroup;
            while (rs != null && rs.next()) {
                rs.getLong(6);
                isGroup = rs.wasNull();
                int[] idx = FxArrayUtils.toIntArray(rs.getString(1), ',');
                for (int i = 0; i < xnew.size(); i++)
                    xnew.get(i).setIndex(idx[i]);
                String xm = XPathElement.toXPath(xnew);
                psWrite.setString(1, newXPath + (isGroup ? "/" : ""));
                psWrite.setString(2, xm + (isGroup ? "/" : ""));
                String parentXP = xm.substring(0, xm.lastIndexOf('/'));
                if ("".equals(parentXP))
                    parentXP = "/";
                psWrite.setString(3, parentXP);
                psWrite.setLong(4, rs.getLong(2));
                psWrite.setInt(5, rs.getInt(3));
                psWrite.setInt(6, rs.getInt(4));
                psWrite.setInt(7, rs.getInt(5));
                psWrite.setString(8, rs.getString(1));
                psWrite.setLong(9, assignmentId);
                psWrite.executeUpdate();
            }
        } catch (SQLException e) {
            throw new FxUpdateException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            try {
                if (psRead != null)
                    psRead.close();
            } catch (SQLException e) {
                //ignore
            }
            try {
                if (psWrite != null)
                    psWrite.close();
            } catch (SQLException e) {
                //ignore
            }
        }
    }
}

java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.