Java tutorial
/*************************************************************** * This file is part of the [fleXive](R) framework. * * Copyright (c) 1999-2014 * 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 Lesser General Public * License version 2.1 or higher as published by the Free Software Foundation. * * The GNU Lesser General Public License can be found at * http://www.gnu.org/licenses/lgpl.html. * A copy is found in the textfile LGPL.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.Database; import com.flexive.core.DatabaseConst; import com.flexive.core.LifeCycleInfoImpl; import com.flexive.core.storage.DBStorage; import com.flexive.core.storage.FxTreeNodeInfo; import com.flexive.core.storage.StorageManager; import com.flexive.core.storage.TreeStorage; import com.flexive.shared.*; import com.flexive.shared.configuration.SystemParameters; import com.flexive.shared.content.*; import com.flexive.shared.exceptions.*; import com.flexive.shared.interfaces.ContentEngine; import com.flexive.shared.interfaces.ScriptingEngine; import com.flexive.shared.interfaces.SequencerEngine; import com.flexive.shared.scripting.FxScriptBinding; import com.flexive.shared.scripting.FxScriptEvent; import com.flexive.shared.security.ACL; import com.flexive.shared.security.ACLPermission; import com.flexive.shared.security.UserGroup; import com.flexive.shared.security.UserTicket; import com.flexive.shared.structure.FxEnvironment; import com.flexive.shared.structure.FxPropertyAssignment; import com.flexive.shared.structure.FxType; import com.flexive.shared.structure.TypeState; import com.flexive.shared.tree.FxTreeMode; import com.flexive.shared.tree.FxTreeNode; import com.flexive.shared.value.FxString; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.*; import java.util.*; import static com.flexive.core.DatabaseConst.*; /** * Generic tree storage implementation for nested set model trees * * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at) * @author Gregor Schober (gregor.schober@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at) */ public abstract class GenericTreeStorage implements TreeStorage { /** * Partition size to be used for large IN(...) queries to workaround limitations of DBMS drivers */ protected static final int SQL_IN_PARTSIZE = 500; /** * Information to be processed when creating a new node (return value helper) */ protected static class NodeCreateInfo { final long id; final String name; final FxPK reference; public NodeCreateInfo(long id, String name, FxPK reference) { this.id = id; this.name = name; this.reference = reference; } } private static final Log LOG = LogFactory.getLog(GenericTreeStorage.class); private static final String TREE_TOTAL_COUNT_LIVE = "(SELECT COUNT(*) FROM " + getTable(FxTreeMode.Live) + " WHERE LFT > t.LFT AND RGT < t.RGT)"; private static final String TREE_TOTAL_COUNT_EDIT = "(SELECT COUNT(*) FROM " + getTable(FxTreeMode.Edit) + " WHERE LFT > t.LFT AND RGT < t.RGT)"; /** * Placeholder for dynamic child count */ protected static final String COMPUTE_TOTAL_CHILDCOUNT = "__TOTAL_CHILD_COUNT"; // 1 2 3 4 protected static final String TREE_LIVE_NODEINFO = "SELECT LFT,RGT,PARENT,TOTAL_CHILDCOUNT," + // 5 "(SELECT LFT FROM " + getTable(FxTreeMode.Live) + " WHERE ID=NODE.PARENT) AS PARENTLEFT, " + // 6 "(SELECT RGT FROM " + getTable(FxTreeMode.Live) + " WHERE ID=NODE.PARENT) AS PARENTRIGHT," + //7 8 9 10 11 12 13 14 15 16 17 18 19 "DEPTH,CHILDCOUNT,REF,TEMPLATE,MODIFIED_AT,NAME,tree_getPosition(?,ID,PARENT),ACL,TDEF,VER,STEP,CREATED_BY,MANDATOR " + "FROM (SELECT t.LFT,t.RGT,t.PARENT,t.DEPTH," + COMPUTE_TOTAL_CHILDCOUNT + ",t.CHILDCOUNT,t.REF,t.TEMPLATE,t.MODIFIED_AT,t.NAME,t.ID,c.ACL,c.TDEF,c.VER,c.STEP,c.CREATED_BY,c.MANDATOR FROM " + getTable(FxTreeMode.Live) + " t, " + TBL_CONTENT + " c WHERE t.ID=? AND c.ID=t.REF AND c.ISLIVE_VER=?) NODE"; protected static final String TREE_NODEINFO_ACLS = "SELECT acl FROM " + TBL_CONTENT + " WHERE id=? AND ver=? and acl != " + ACL.NULL_ACL_ID + " UNION " + "SELECT acl FROM " + TBL_CONTENT_ACLS + " WHERE id=? AND ver=? "; // 1 2 3 4 protected static final String TREE_EDIT_NODEINFO = "SELECT LFT,RGT,PARENT,TOTAL_CHILDCOUNT," + // 5 "(SELECT LFT FROM " + getTable(FxTreeMode.Edit) + " WHERE ID=NODE.PARENT) AS PARENTLEFT, " + // 6 "(SELECT RGT FROM " + getTable(FxTreeMode.Edit) + " WHERE ID=NODE.PARENT) AS PARENTRIGHT," + //7 8 9 10 11 12 13 14 15 16 17 18 19 "DEPTH,CHILDCOUNT,REF,TEMPLATE,MODIFIED_AT,NAME,tree_getPosition(?,ID,PARENT),ACL,TDEF,VER,STEP,CREATED_BY,MANDATOR " + "FROM (SELECT t.LFT,t.RGT,t.PARENT,t.DEPTH," + COMPUTE_TOTAL_CHILDCOUNT + ",t.CHILDCOUNT,t.REF,t.TEMPLATE,t.MODIFIED_AT,t.NAME,t.ID,c.ACL,c.TDEF,c.VER,c.STEP,c.CREATED_BY,c.MANDATOR FROM " + getTable(FxTreeMode.Edit) + " t, " + TBL_CONTENT + " c WHERE t.ID=? AND c.ID=t.REF AND c.ISMAX_VER=?) NODE"; // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private static final String TREE_LIVE_GETNODE = "SELECT t.ID,t.REF,t.DEPTH," + TREE_TOTAL_COUNT_LIVE + ",t.CHILDCOUNT,t.NAME,t.PARENT,t.DIRTY,t.TEMPLATE,t.MODIFIED_AT,tree_getPosition(?,t.ID,t.PARENT) AS POS,c.ACL,c.TDEF,c.VER,c.STEP,c.CREATED_BY,c.MANDATOR,l.USER_ID,l.LOCKTYPE,l.CREATED_AT,l.EXPIRES_AT,c.CREATED_AT,c.MODIFIED_BY,c.MODIFIED_AT,tree_idToPath(t.id, ?) " + "FROM " + getTable(FxTreeMode.Live) + " t, " + TBL_CONTENT + " c LEFT JOIN " + TBL_LOCKS + " l ON (l.LOCK_ID=c.ID AND l.LOCK_VER=c.VER)WHERE t.ID=? AND c.ID=t.REF AND c.ISLIVE_VER=?"; // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private static final String TREE_EDIT_GETNODE = "SELECT t.ID,t.REF,t.DEPTH,0,t.CHILDCOUNT,t.NAME,t.PARENT,t.DIRTY,t.TEMPLATE,t.MODIFIED_AT,tree_getPosition(?,t.ID,t.PARENT) AS POS,c.ACL,c.TDEF,c.VER,c.STEP,c.CREATED_BY,c.MANDATOR,l.USER_ID,l.LOCKTYPE,l.CREATED_AT,l.EXPIRES_AT,c.CREATED_AT,c.MODIFIED_BY,c.MODIFIED_AT,tree_idToPath(t.id, ?) " + "FROM " + getTable(FxTreeMode.Edit) + " t, " + TBL_CONTENT + " c LEFT JOIN " + TBL_LOCKS + " l ON (l.LOCK_ID=c.ID AND l.LOCK_VER=c.VER) WHERE t.ID=? AND c.ID=t.REF AND c.ISMAX_VER=?"; // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static final String TREE_LIVE_GETNODE_REF = "SELECT t.ID,t.REF,t.DEPTH,0,t.CHILDCOUNT,t.NAME,t.PARENT,t.DIRTY,t.TEMPLATE,t.MODIFIED_AT,tree_getPosition(?,t.ID,t.PARENT) AS POS,c.ACL,c.TDEF,c.VER,c.STEP,c.CREATED_BY,c.MANDATOR,l.USER_ID,l.LOCKTYPE,l.CREATED_AT,l.EXPIRES_AT,c.CREATED_AT,c.MODIFIED_BY,c.MODIFIED_AT " + "FROM " + getTable(FxTreeMode.Live) + " t, " + TBL_CONTENT + " c LEFT JOIN " + TBL_LOCKS + " l ON (l.LOCK_ID=c.ID AND l.LOCK_VER=c.VER) WHERE t.REF=? AND c.ID=t.REF AND c.ISLIVE_VER=?"; // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static final String TREE_EDIT_GETNODE_REF = "SELECT t.ID,t.REF,t.DEPTH,0,t.CHILDCOUNT,t.NAME,t.PARENT,t.DIRTY,t.TEMPLATE,t.MODIFIED_AT,tree_getPosition(?,t.ID,t.PARENT) AS POS,c.ACL,c.TDEF,c.VER,c.STEP,c.CREATED_BY,c.MANDATOR,l.USER_ID,l.LOCKTYPE,l.CREATED_AT,l.EXPIRES_AT,c.CREATED_AT,c.MODIFIED_BY,c.MODIFIED_AT " + "FROM " + getTable(FxTreeMode.Edit) + " t, " + TBL_CONTENT + " c LEFT JOIN " + TBL_LOCKS + " l ON (l.LOCK_ID=c.ID AND l.LOCK_VER=c.VER)WHERE t.REF=? AND c.ID=t.REF AND c.ISMAX_VER=?"; //1=id private static final String TREE_REF_USAGE_EDIT = "SELECT DISTINCT c.TDEF, t.ID FROM " + TBL_CONTENT + " c," + getTable(FxTreeMode.Edit) + " t WHERE c.ID=? AND t.REF=c.ID"; //1=id private static final String TREE_REF_USAGE_LIVE = "SELECT DISTINCT c.TDEF, t.ID FROM " + TBL_CONTENT + " c," + getTable(FxTreeMode.Live) + " t WHERE c.ID=? AND t.REF=c.ID"; /** * Get the tree table for the requested mode * * @param mode requested mode * @return database table to use */ public static String getTable(FxTreeMode mode) { if (mode == FxTreeMode.Live) return TBL_TREE + "_LIVE"; else return TBL_TREE; } /** * Check if data contains valid characters * * @param data the data to check * @throws FxTreeException on errors */ protected void checkDataValue(String data) throws FxTreeException { if (!StringUtils.isEmpty(data) && data.indexOf(',') >= 0) { throw new FxTreeException("Data may not contain the comma character [,]"); } } /** * Create a new folder instance to be used as reference for nodes * * @param ce a reference to the ContentEngine * @param name name * @param label label * @return FxPK * @throws FxApplicationException on errors */ protected FxPK createFolderInstance(ContentEngine ce, String name, FxString label) throws FxApplicationException { FxPK reference; FxType type = CacheAdmin.getEnvironment().getType(FxType.FOLDER); FxContent content = ce.initialize(type.getId()); List<FxPropertyAssignment> fqns = type.getAssignmentsForProperty( EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_FQN_PROPERTY)); if (fqns.size() > 0) content.setValue(fqns.get(0).getXPath(), new FxString(fqns.get(0).isMultiLang(), name)); List<FxPropertyAssignment> captions = type.getAssignmentsForProperty( EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_CAPTION_PROPERTY)); if (captions.size() > 0) { FxString captionValue; if (label != null) { if (captions.get(0).isMultiLang() && label.isMultiLanguage()) { captionValue = label; } else if (!captions.get(0).isMultiLang() && label.isMultiLanguage()) { captionValue = new FxString(false, label.getBestTranslation()); } else if (captions.get(0).isMultiLang() && !label.isMultiLanguage()) { captionValue = new FxString(true, label.getBestTranslation()); } else captionValue = new FxString(captions.get(0).isMultiLang(), name); //unreachable fallback } else captionValue = new FxString(captions.get(0).isMultiLang(), name); content.setValue(captions.get(0).getXPath(), captionValue); } //make sure we have a live version content.setStepId(type.getWorkflow().getLiveStep().getId()); reference = ce.save(content); return reference; } /** * Resolve information about a node to be created like creating a folder if no reference is provided or check * permissions on existing references and availability of live instance if operated on the live tree * * @param mode tree mode * @param seq reference to the sequencer * @param ce reference to the content engine * @param nodeId desired node id * @param name name * @param label label * @param reference reference * @param activateContent change the step of contents that have no live step to live in the max version? * @return NodeCreateInfo * @throws FxApplicationException on errors */ protected NodeCreateInfo getNodeCreateInfo(FxTreeMode mode, SequencerEngine seq, ContentEngine ce, long nodeId, String name, FxString label, FxPK reference, boolean activateContent) throws FxApplicationException { if (nodeId < 0) nodeId = seq.getId(mode.getSequencer()); if (StringUtils.isEmpty(name) || "_".equals(name)) { if (!StringUtils.isEmpty(label.getBestTranslation())) name = label.getBestTranslation(); else if (reference != null && !reference.isNew()) { try { final FxString caption = ce.load(reference).getCaption(); if (caption != null && !StringUtils.isEmpty(caption.getBestTranslation())) name = caption.getBestTranslation(); else name = "" + nodeId; } catch (FxApplicationException e) { name = "" + nodeId; } } else name = "" + nodeId; } name = FxFormatUtils.escapeTreePath(name); if (name.contains("/")) { throw new FxTreeException("ex.tree.create.failed.name", name); } //check or optionally create the reference if (reference == null || reference.isNew()) { //create a folder instance if no reference is supplied reference = createFolderInstance(ce, name, label); } else { //check read permission FxContent co = ce.load(reference); if (mode == FxTreeMode.Live) { //if mode==Live(activation), check if a live version exists and if not try to change the step of the max version to live (create a new version?) FxContentVersionInfo versionInfo = ce.getContentVersionInfo(reference); if (versionInfo.hasLiveVersion()) { //check read permission FxContentSecurityInfo si = ce .getContentSecurityInfo(new FxPK(reference.getId(), versionInfo.getLiveVersion())); FxPermissionUtils.checkPermission(FxContext.getUserTicket(), ACLPermission.READ, si, true); } else { if (activateContent) { //create a Live version reference = createContentLiveVersion(ce, co); LOG.info("Created new live version " + reference + " to activate node " + nodeId); } else { throw new FxTreeException("ex.tree.activate.failed.noLiveContent", nodeId); } } } List<FxPropertyData> fqns = co.getPropertyData( EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_FQN_PROPERTY), false); if (fqns.size() > 0) name = "" + fqns.get(0).getValue().getBestTranslation(); } return new NodeCreateInfo(nodeId, name, reference); } /** * Create a live version of the given content, taking over existing locks and unlocking them after the version is created * * @param ce ContentEngine reference * @param co FxContent * @return primary key of the new version * @throws FxApplicationException on errors */ protected FxPK createContentLiveVersion(ContentEngine ce, FxContent co) throws FxApplicationException { FxPK pk; if (co.isLocked()) ce.takeOverLock(co.getLock()); co.setStepId(CacheAdmin.getEnvironment().getType(co.getTypeId()).getWorkflow().getLiveStep().getId()); //save will throw an exception if we are not allowed to save a live version pk = ce.save(co); if (co.isLocked()) ce.unlock(pk); return pk; } /** * {@inheritDoc} */ @Override public long[] createNodes(Connection con, SequencerEngine seq, ContentEngine ce, FxTreeMode mode, long parentNodeId, String path, int position, boolean activateContent) throws FxApplicationException { if ("/".equals(path)) return new long[] { FxTreeNode.ROOT_NODE }; final List<Long> result = new ArrayList<Long>(); final Scanner scanner = new Scanner(path); long currentParent = parentNodeId; scanner.useDelimiter("/"); if (parentNodeId != -1) { acquireLocksForUpdate(con, getTreeNodeInfo(con, mode, parentNodeId), false); } while (scanner.hasNext()) { String name = scanner.next(); final FxString label = new FxString(true, name); name = FxFormatUtils.escapeTreePath(name); if (StringUtils.isEmpty(name)) continue; long nodeId = getIdByFQNPath(con, mode, currentParent, "/" + name); if (nodeId == -1) nodeId = createNode(con, seq, ce, mode, nodeId, currentParent, name, label, position, null, null, activateContent); result.add(nodeId); currentParent = nodeId; } return ArrayUtils.toPrimitive(result.toArray(new Long[result.size()])); } /** * {@inheritDoc} */ @Override public void updateName(Connection con, FxTreeMode mode, ContentEngine ce, long nodeId, String name) throws FxApplicationException { FxTreeNode node = getNode(con, mode, nodeId); name = FxFormatUtils.escapeTreePath(name); if (node.getName().equals(name)) return; FxContent co = ce.load(node.getReference()); FxType type = CacheAdmin.getEnvironment().getType(co.getTypeId()); List<FxPropertyAssignment> fqns = type.getAssignmentsForProperty( EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_FQN_PROPERTY)); if (fqns.size() > 0) { co.setValue(fqns.get(0).getXPath(), new FxString(fqns.get(0).isMultiLang(), name)); ce.save(co); //we're done here since the content engine itself will trigger the tree table update return; } //no fqn - update the name column PreparedStatement ps = null; try { // 1 2 3 ps = con.prepareStatement("UPDATE " + getTable(mode) + " SET NAME=?, DIRTY=? WHERE ID=?"); ps.setString(1, name); ps.setBoolean(2, mode != FxTreeMode.Live); ps.setLong(3, nodeId); ps.executeUpdate(); } catch (SQLException e) { throw new FxUpdateException(LOG, e, "ex.tree.update.failed", mode.name(), e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (SQLException e) { //ignore } } } /** * {@inheritDoc} */ @Override public void updateReference(Connection con, FxTreeMode mode, long nodeId, long referenceId) throws FxApplicationException { PreparedStatement ps = null; try { // 1 2 3 ps = con.prepareStatement("UPDATE " + getTable(mode) + " SET REF=?, DIRTY=? WHERE ID=?"); ps.setLong(1, referenceId); ps.setBoolean(2, mode != FxTreeMode.Live); //live tree is never dirty ps.setLong(3, nodeId); ps.executeUpdate(); FxContext.get().setTreeWasModified(); } catch (SQLException e) { throw new FxUpdateException(LOG, e, "ex.tree.update.failed", mode.name(), e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (SQLException e) { //ignore } } } /** * {@inheritDoc} */ @Override public void syncFQNName(Connection con, long referenceId, boolean maxVersion, boolean liveVersion, String name) throws FxApplicationException { // if (StringUtils.isEmpty(name)) // return; if (liveVersion) updateName(con, FxTreeMode.Live, referenceId, name); if (maxVersion || liveVersion) updateName(con, FxTreeMode.Edit, referenceId, name); } /** * Update all nodes that reference the given id and set the name * * @param con an open and valid connection * @param mode tree mode * @param referenceId id of the referenced content * @param name the new name * @throws FxApplicationException on errors */ private void updateName(Connection con, FxTreeMode mode, long referenceId, String name) throws FxApplicationException { PreparedStatement ps = null; try { // 1 2 3 ps = con.prepareStatement("UPDATE " + getTable(mode) + " SET NAME=?, DIRTY=? WHERE REF=?"); ps.setString(1, FxFormatUtils.escapeTreePath(name)); ps.setBoolean(2, mode != FxTreeMode.Live); //live tree is never dirty ps.setLong(3, referenceId); ps.executeUpdate(); FxContext.get().setTreeWasModified(); } catch (SQLException e) { throw new FxUpdateException(LOG, e, "ex.tree.update.failed", mode.name(), e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (SQLException e) { //ignore } } } /** * Clear a tree nodes dirty flag * * @param con an open and valid connection * @param mode tree mode * @param nodeId node id * @throws FxUpdateException on errors */ protected void clearDirtyFlag(Connection con, FxTreeMode mode, long nodeId) throws FxUpdateException { PreparedStatement ps = null; try { // 1 2 ps = con.prepareStatement("UPDATE " + getTable(mode) + " SET DIRTY=?,MODIFIED_AT=" + StorageManager.getTimestampFunction() + " WHERE ID=?"); ps.setBoolean(1, false); //live tree is never dirty ps.setLong(2, nodeId); ps.executeUpdate(); } catch (SQLException e) { throw new FxUpdateException(LOG, e, "ex.tree.update.failed", mode.name(), e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (SQLException e) { //ignore } } } /** * Flag a tree node dirty * * @param con an open and valid connection * @param mode tree mode * @param nodeId node id * @throws FxUpdateException on errors */ protected void flagDirty(Connection con, FxTreeMode mode, long nodeId) throws FxUpdateException { PreparedStatement ps = null; try { // 1 2 ps = con.prepareStatement("UPDATE " + getTable(mode) + " SET DIRTY=?,MODIFIED_AT=" + StorageManager.getTimestampFunction() + " WHERE ID=?"); ps.setBoolean(1, mode != FxTreeMode.Live); //live tree is never dirty ps.setLong(2, nodeId); ps.executeUpdate(); } catch (SQLException e) { throw new FxUpdateException(LOG, e, "ex.tree.update.failed", mode.name(), e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (SQLException e) { //ignore } } } /** * {@inheritDoc} */ @Override public long getIdByFQNPath(Connection con, FxTreeMode mode, long startNode, String path) throws FxApplicationException { path = path.replaceAll("/+", "/"); if ("/".equals(path)) return FxTreeNode.ROOT_NODE; PreparedStatement ps = null; try { final DBStorage storage = StorageManager.getStorageImpl(); ps = con.prepareStatement("SELECT tree_pathToID(?,?,?) " + storage.getFromDual()); ps.setLong(1, startNode); ps.setString(2, FxFormatUtils.escapeTreePath(path)); ps.setBoolean(3, mode == FxTreeMode.Live); final ResultSet rs = ps.executeQuery(); long result = -1; if (rs.next()) { result = rs.getLong(1); if (rs.wasNull()) { result = -1; } } return result; } catch (SQLException e) { throw new FxLoadException(LOG, e, "ex.db.sqlError", e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (Exception e) { //ignore } } } /** * {@inheritDoc} */ @Override public long getIdByLabelPath(Connection con, FxTreeMode mode, long startNode, String path) throws FxApplicationException { path = path.replaceAll("/+", "/"); if ("/".equals(path)) return FxTreeNode.ROOT_NODE; PreparedStatement ps = null; try { final DBStorage storage = StorageManager.getStorageImpl(); ps = con.prepareStatement("SELECT tree_captionPathToID(?, ?, ?, ?, ?) " + storage.getFromDual()); ps.setLong(1, startNode); ps.setString(2, path); ps.setLong(3, EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_CAPTION_PROPERTY)); ps.setLong(4, FxContext.getUserTicket().getLanguage().getId()); ps.setBoolean(5, mode == FxTreeMode.Live); final ResultSet rs = ps.executeQuery(); long result = -1; if (rs.next()) { result = rs.getLong(1); if (rs.wasNull()) result = -1; } return result; } catch (SQLException e) { throw new FxLoadException(LOG, e, "ex.db.sqlError", e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (Exception e) { //ignore } } } /** * {@inheritDoc} */ @Override public List<String> getLabels(Connection con, FxTreeMode mode, long labelPropertyId, FxLanguage language, boolean stripNodeInfos, long... nodeIds) throws FxApplicationException { List<String> ret = new ArrayList<String>(nodeIds.length); if (nodeIds.length == 0) return ret; PreparedStatement ps = null; ResultSet rs; try { ps = con.prepareStatement("SELECT tree_FTEXT1024_Chain(?,?,?,?)" + StorageManager.getFromDual()); ps.setInt(2, (int) language.getId()); ps.setLong(3, labelPropertyId); ps.setBoolean(4, mode == FxTreeMode.Live); for (long id : nodeIds) { ps.setLong(1, id); rs = ps.executeQuery(); if (rs != null && rs.next()) { final String path = rs.getString(1); if (!StringUtils.isEmpty(path)) ret.add(stripNodeInfos ? stripNodeInfos(path) : path); else addUnknownNodeId(ret, id); } else addUnknownNodeId(ret, id); } return ret; } catch (SQLException e) { throw new FxLoadException(LOG, e, "ex.db.sqlError", e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (Exception e) { //ignore } } } /** * Helper to add an unknown node id * * @param ret result list * @param id unknown node id */ protected void addUnknownNodeId(List<String> ret, long id) { ret.add("/<unknown:" + id + ">"); } /** * Strip any node infos from the path. * Node infos have the format path:nodeId:ref:tdef - strip all but path which can not contain ':' as it is replaced * with a space in the stored procedure. * * @param path path to strip * @return stripped path */ protected String stripNodeInfos(String path) { if (StringUtils.isEmpty(path)) return ""; StringBuilder sbRet = new StringBuilder(path.length()); for (String p : path.split("\\/")) if (p.indexOf(':') > 0) sbRet.append('/').append(p.substring(0, p.indexOf(':'))); return sbRet.toString(); } /** * {@inheritDoc} */ @Override public String getPathById(Connection con, FxTreeMode mode, long id) throws FxTreeException { Statement ps = null; try { DBStorage storage = StorageManager.getStorageImpl(); ps = con.createStatement(); ResultSet rs = ps.executeQuery("SELECT tree_idToPath(" + id + "," + storage.getBooleanExpression(mode == FxTreeMode.Live) + ")" + storage.getFromDual()); String result = null; if (rs.next()) { result = rs.getString(1); if (rs.wasNull()) result = ""; } return result != null ? FxFormatUtils.escapeTreePath(result) : ""; } catch (SQLException e) { throw new FxTreeException(LOG, "ex.db.sqlError", e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (Exception exc) { //ignore } } } /** * {@inheritDoc} */ @Override public long[] getIdChain(Connection con, FxTreeMode mode, long id) throws FxTreeException { Statement ps = null; try { ps = con.createStatement(); DBStorage storage = StorageManager.getStorageImpl(); ResultSet rs = ps.executeQuery("SELECT tree_idchain(" + id + "," + storage.getBooleanExpression(mode == FxTreeMode.Live) + ")" + storage.getFromDual()); String result = ""; if (rs.next()) { result = rs.getString(1); if (rs.wasNull()) result = ""; } if (result.length() == 0) { return null; } String ids[] = result.split("/"); long[] lres = new long[ids.length - 1]; int pos = 0; for (String sid : ids) { if (pos != 0) lres[pos - 1] = Long.parseLong(sid); pos++; } return lres; } catch (SQLException e) { throw new FxTreeException(LOG, "ex.db.sqlError", e.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (Exception exc) { //ignore } } } /** * {@inheritDoc} */ @Override public boolean exists(Connection con, FxTreeMode mode, long id) throws FxApplicationException { try { getTreeNodeInfo(con, mode, id); } catch (FxNotFoundException e) { return false; } return true; } /** * {@inheritDoc} */ @Override public FxTreeNode getNode(Connection con, FxTreeMode mode, long nodeId) throws FxApplicationException { PreparedStatement ps = null; final FxEnvironment env = CacheAdmin.getEnvironment(); try { ps = con.prepareStatement( prepareSql(mode, mode == FxTreeMode.Live ? TREE_LIVE_GETNODE : TREE_EDIT_GETNODE)); ps.setBoolean(1, mode == FxTreeMode.Live); ps.setBoolean(2, mode == FxTreeMode.Live); ps.setLong(3, nodeId); ps.setBoolean(4, true); ResultSet rs = ps.executeQuery(); if (rs.next()) { long _id = rs.getLong(1); FxPK _ref = new FxPK(rs.getLong(2), rs.getInt(14)); int _depth = rs.getInt(3); //int _totalChilds = rs.getInt(4); int _directChilds = rs.getInt(5); String _name = rs.getString(6); long _parent = rs.getLong(7); boolean _dirty = rs.getBoolean(8); String _data = rs.getString(9); if (rs.wasNull()) _data = null; long _modified = rs.getLong(10); int _pos = rs.getInt(11); long _acl = rs.getLong(12); FxType _type = env.getType(rs.getLong(13)); long _stepACL = env.getStep(rs.getLong(15)).getAclId(); long _createdBy = rs.getLong(16); long _mandator = rs.getLong(17); UserTicket ticket = FxContext.getUserTicket(); boolean _edit; boolean _create; boolean _delete; boolean _export; boolean _relate; final boolean _system = FxContext.get().getRunAsSystem() || ticket.isGlobalSupervisor(); final List<Long> acls = fetchACLs(con, _ref, _acl); FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.READ, _type, _stepACL, acls, true); FxPermissionUtils.checkMandatorExistance(_mandator); FxPermissionUtils.checkTypeAvailable(_type.getId(), true); if (_system || ticket.isMandatorSupervisor() && _mandator == ticket.getMandatorId() || !_type.isUsePermissions() || ticket.isInGroup((int) UserGroup.GROUP_OWNER) && _createdBy == ticket.getUserId()) { _edit = _create = _delete = _export = _relate = true; } else { //throw exception if read is forbidden FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.READ, _type, _stepACL, acls, true); _edit = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.EDIT, _type, _stepACL, acls, false); _relate = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.RELATE, _type, _stepACL, acls, false); _delete = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.DELETE, _type, _stepACL, acls, false); _export = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.EXPORT, _type, _stepACL, acls, false); _create = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.CREATE, _type, _stepACL, acls, false); } final String TRUE = StorageManager.getBooleanTrueExpression(); FxString label = Database.loadContentDataFxString(con, "FTEXT1024", "ID=" + _ref.getId() + " AND " + (mode == FxTreeMode.Live ? "ISMAX_VER=" + TRUE : "ISLIVE_VER=" + TRUE) + " AND TPROP=" + EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_CAPTION_PROPERTY)); final FxLock lock; final long lockUser = rs.getLong(18); if (rs.wasNull()) lock = FxLock.noLockPK(); else lock = new FxLock(FxLockType.getById(rs.getInt(19)), rs.getLong(20), rs.getLong(21), lockUser, _ref); return new FxTreeNode(mode, lock, _id, _parent, _ref, LifeCycleInfoImpl.load(rs, 16, 22, 23, 24), _type.getId(), acls, _name, rs.getString(25), label, _pos, new ArrayList<FxTreeNode>(0), new ArrayList<Long>(0), _depth, _directChilds, _directChilds == 0, _dirty, _modified, _data, _edit, _create, _delete, _relate, _export); } else throw new FxLoadException("ex.tree.node.notFound", nodeId, mode); } catch (SQLException exc) { throw new FxLoadException(LOG, "ex.tree.load.failed.node", nodeId, mode, exc.getMessage()); } finally { Database.closeObjects(GenericTreeStorage.class, null, ps); } } /** * {@inheritDoc} */ @Override public List<FxTreeNode> getNodesWithReference(Connection con, FxTreeMode mode, long referenceId) throws FxApplicationException { PreparedStatement ps = null; List<FxTreeNode> ret = new ArrayList<FxTreeNode>(20); long _id = -1; try { ps = con.prepareStatement( prepareSql(mode, mode == FxTreeMode.Live ? TREE_LIVE_GETNODE_REF : TREE_EDIT_GETNODE_REF)); List<Long> acls = null; ps.setBoolean(1, mode == FxTreeMode.Live); ps.setLong(2, referenceId); ps.setBoolean(3, true); ResultSet rs = ps.executeQuery(); while (rs.next()) { _id = rs.getLong(1); FxPK _ref = new FxPK(rs.getLong(2), rs.getInt(14)); int _depth = rs.getInt(3); int _totalChilds = rs.getInt(4); int _directChilds = rs.getInt(5); String _name = rs.getString(6); long _parent = rs.getLong(7); boolean _dirty = rs.getBoolean(8); String _data = rs.getString(9); if (rs.wasNull()) _data = null; long _modified = rs.getLong(10); int _pos = rs.getInt(11); long _acl = rs.getLong(12); FxType _type = CacheAdmin.getEnvironment().getType(rs.getLong(13)); long _stepACL = CacheAdmin.getEnvironment().getStep(rs.getLong(15)).getAclId(); long _createdBy = rs.getLong(16); long _mandator = rs.getLong(17); if (acls == null) { // fetch ACLs only once, since they are only dependent on the reference instance acls = fetchACLs(con, _ref, _acl); } UserTicket ticket = FxContext.getUserTicket(); boolean _read; boolean _edit; boolean _create; boolean _delete; boolean _export; boolean _relate; final boolean _system = FxContext.get().getRunAsSystem() || ticket.isGlobalSupervisor(); FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.READ, _type, _stepACL, acls, true); if (_system || ticket.isMandatorSupervisor() && _mandator == ticket.getMandatorId() || !_type.isUsePermissions() || ticket.isInGroup((int) UserGroup.GROUP_OWNER) && _createdBy == ticket.getUserId()) { _read = _edit = _create = _delete = _export = _relate = true; } else { //throw exception if read is forbidden _read = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.READ, _type, _stepACL, acls, false); _edit = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.EDIT, _type, _stepACL, acls, false); _relate = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.RELATE, _type, _stepACL, acls, false); _delete = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.DELETE, _type, _stepACL, acls, false); _export = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.EXPORT, _type, _stepACL, acls, false); _create = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.CREATE, _type, _stepACL, acls, false); } if (!_read) continue; final String TRUE = StorageManager.getBooleanTrueExpression(); FxString label = Database.loadContentDataFxString(con, "FTEXT1024", "ID=" + _ref.getId() + " AND " + (mode == FxTreeMode.Live ? "ISMAX_VER=" + TRUE : "ISLIVE_VER=" + TRUE) + " AND TPROP=" + EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_CAPTION_PROPERTY)); final FxLock lock; final long lockUser = rs.getLong(18); if (rs.wasNull()) lock = FxLock.noLockPK(); else lock = new FxLock(FxLockType.getById(rs.getInt(19)), rs.getLong(20), rs.getLong(21), lockUser, _ref); FxTreeNode node = new FxTreeNode(mode, lock, _id, _parent, _ref, LifeCycleInfoImpl.load(rs, 16, 22, 23, 24), _type.getId(), acls, _name, getPathById(con, mode, _id), label, _pos, new ArrayList<FxTreeNode>(0), new ArrayList<Long>(0), _depth, _directChilds, _directChilds == 0, _dirty, _modified, _data, _edit, _create, _delete, _relate, _export); ret.add(node); } } catch (SQLException exc) { throw new FxLoadException(LOG, "ex.tree.load.failed.node", _id, mode, exc.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (Exception exc) { /*ignore*/} } return ret; } /** * {@inheritDoc} */ @Override public FxTreeNode getTree(Connection con, ContentEngine ce, FxTreeMode mode, long nodeId, int depth, boolean loadPartial, FxLanguage partialLoadLanguage) throws FxApplicationException { Statement ps = null; // long time = System.currentTimeMillis(); // long nodes = 0; try { final long captionProperty = EJBLookup.getConfigurationEngine() .get(SystemParameters.TREE_CAPTION_PROPERTY); final DBStorage storage = StorageManager.getStorageImpl(); final String TRUE = storage.getBooleanTrueExpression(); final String FALSE = storage.getBooleanFalseExpression(); final String version = mode == FxTreeMode.Live ? "ISLIVE_VER=" + TRUE : "ISMAX_VER=" + TRUE; String label_pos = loadPartial ? "COALESCE(COALESCE(" + "(SELECT f.FTEXT1024 FROM " + TBL_CONTENT_DATA + " f WHERE f.TPROP=" + captionProperty + " AND LANG IN(" + partialLoadLanguage.getId() + ",0)AND f." + version + " AND f.ID=n.REF " + storage.getLimit(true, 1) + ")," + "(SELECT f.FTEXT1024 from " + TBL_CONTENT_DATA + " f where f.tprop=" + captionProperty + " AND ISMLDEF=" + TRUE + " AND f." + version + " AND f.ID=n.REF " + storage.getLimit(true, 1) + ")" + "),n.NAME)" : "tree_getPosition(" + (mode == FxTreeMode.Live ? TRUE : FALSE) + ",n.ID,n.PARENT)"; // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 String sql = "SELECT n.ID,n.REF,n.DEPTH,-1 AS TOTAL_CHILDCOUNT,n.CHILDCOUNT,n.NAME,n.PARENT,n.DIRTY,n.TEMPLATE,n.MODIFIED_AT," + label_pos + ",c.ACL,c.TDEF,c.VER,c.STEP,c.CREATED_BY,c.MANDATOR,l.USER_ID,l.LOCKTYPE,l.CREATED_AT,l.EXPIRES_AT,c.CREATED_AT,c.MODIFIED_BY,c.MODIFIED_AT " + "FROM " + getTable(mode) + " r, " + getTable(mode) + " n, " + TBL_CONTENT + " c LEFT JOIN " + TBL_LOCKS + " l ON (l.LOCK_ID=c.ID AND l.LOCK_VER=c.VER) WHERE r.ID=" + nodeId + " AND n.LFT>=r.LFT AND n.LFT<=r.RGT AND n.DEPTH<=(r.DEPTH+" + depth + ") AND c.ID=n.REF AND c." + version + " ORDER BY n.LFT"; ps = con.createStatement(); ResultSet rs = ps.executeQuery(sql); Map<Long, FxTreeNode> data = new HashMap<Long, FxTreeNode>(100, 2.0f); //prepare infos needed to compute permissions UserTicket ticket = FxContext.getUserTicket(); final boolean _system = FxContext.get().getRunAsSystem() || ticket.isGlobalSupervisor(); //StepId->step ACL Id lookup Map<Long, Long> step2stepACLId = new HashMap<Long, Long>(10); //typeId->FxType lookup Map<Long, FxType> typeId2Type = new HashMap<Long, FxType>(10); while (rs.next()) { // nodes++; long _id = rs.getLong(1); FxPK _ref = new FxPK(rs.getLong(2), rs.getInt(14)); int _depth = rs.getInt(3); int _totalChilds = rs.getInt(4); int _directChilds = rs.getInt(5); String _name = rs.getString(6); long _parent = rs.getLong(7); boolean _dirty = rs.getBoolean(8); String _data = rs.getString(9); if (rs.wasNull()) _data = null; long _modified = rs.getLong(10); int _pos; FxString label; if (loadPartial) { _pos = FxTreeNode.PARTIAL_LOADED_POS; label = new FxString(true, partialLoadLanguage.getId(), rs.getString(11)); } else { _pos = rs.getInt(11); label = Database.loadContentDataFxString(con, "FTEXT1024", "ID=" + _ref.getId() + " AND " + version + " AND TPROP=" + EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_CAPTION_PROPERTY)); } if (label.isEmpty()) label = new FxString(true, _name); //TODO: check if label should be multilanguage! long _acl = rs.getLong(12); long _tdef = rs.getLong(13); long _step = rs.getLong(15); long _createdBy = rs.getLong(16); long _mandator = rs.getLong(17); final List<Long> acls = fetchACLs(con, _ref, _acl); boolean _read; boolean _edit; boolean _create; boolean _delete; boolean _export; boolean _relate; FxType type = typeId2Type.get(_tdef); final FxEnvironment env = CacheAdmin.getEnvironment(); if (type == null) { type = env.getType(_tdef); typeId2Type.put(_tdef, type); } Long _stepACL = step2stepACLId.get(_step); if (_stepACL == null) { _stepACL = env.getStep(_step).getAclId(); step2stepACLId.put(_step, _stepACL); } if (_system || ticket.isMandatorSupervisor() && _mandator == ticket.getMandatorId() || !type.isUsePermissions() || ticket.isInGroup((int) UserGroup.GROUP_OWNER) && _createdBy == ticket.getUserId()) { _read = _edit = _create = _delete = _export = _relate = true; } else { _read = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.READ, type, _stepACL, acls, false); _edit = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.EDIT, type, _stepACL, acls, false); _relate = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.RELATE, type, _stepACL, acls, false); _delete = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.DELETE, type, _stepACL, acls, false); _export = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.EXPORT, type, _stepACL, acls, false); _create = FxPermissionUtils.checkPermission(ticket, _createdBy, ACLPermission.CREATE, type, _stepACL, acls, false); } if (_read && (!env.getMandator(_mandator).isActive() || type.getState() == TypeState.Unavailable)) _read = false; //filter out inactive mandators and unavailable types if (_read) { final FxLock lock; final long lockUser = rs.getLong(18); if (rs.wasNull()) lock = FxLock.noLockPK(); else lock = new FxLock(FxLockType.getById(rs.getInt(19)), rs.getLong(20), rs.getLong(21), lockUser, _ref); FxTreeNode node = new FxTreeNode(mode, lock, _id, _parent, _ref, LifeCycleInfoImpl.load(rs, 16, 22, 23, 24), _tdef, acls, _name, FxTreeNode.PATH_NOT_LOADED, label, _pos, new ArrayList<FxTreeNode>(10), new ArrayList<Long>(10), _depth, _directChilds, _directChilds == 0, _dirty, _modified, _data, _edit, _create, _delete, _relate, _export); FxTreeNode parent = data.get(node.getParentNodeId()); data.put(_id, node); if (parent != null) parent._addChild(node); } } final FxTreeNode root = data.get(nodeId); if (root == null) throw new FxNotFoundException(LOG, "ex.tree.node.notFound", nodeId, mode); FxTreeNode _root = getNode(con, mode, root.getId()); root._applyPath(_root.getPath()); root._applyPosition(_root.getPosition()); return root; } catch (SQLException exc) { throw new FxLoadException(LOG, "ex.tree.load.failed.tree", nodeId, mode, exc.getMessage(), depth); } finally { try { if (ps != null) ps.close(); } catch (Exception exc) { //ignore } // System.out.println(nodes + " nodes loaded in " + (System.currentTimeMillis() - time) + "[ms]. Partial:" + loadPartial); } } /** * {@inheritDoc} */ @Override public void clearTree(Connection con, ContentEngine ce, FxTreeMode mode) throws FxApplicationException { Statement stmt = null; try { FxPK rootPK; FxTreeMode rootCheckMode = mode == FxTreeMode.Live ? FxTreeMode.Edit : FxTreeMode.Live; // Savepoint sp = null; // final boolean needSavePoints = StorageManager.isRollbackOnConstraintViolation(); try { // if (needSavePoints) sp = con.setSavepoint(); rootPK = getTreeNodeInfo(con, rootCheckMode, FxTreeNode.ROOT_NODE).getReference(); // if (needSavePoints) con.releaseSavepoint(sp); } catch (FxApplicationException e) { LOG.info("Creating a new root node for the " + mode.name() + " tree."); // if (needSavePoints && sp != null) con.rollback(sp); //create the root folder instance FxType type = CacheAdmin.getEnvironment().getType(FxType.FOLDER); FxContent content = ce.initialize(type.getId()); List<FxPropertyAssignment> captions = type.getAssignmentsForProperty( EJBLookup.getConfigurationEngine().get(SystemParameters.TREE_CAPTION_PROPERTY)); if (captions.size() > 0) { FxPropertyAssignment paCaption = captions.get(0); FxString label = new FxString(paCaption.isMultiLang(), "Root"); content.setValue(paCaption.getXPath(), label); } rootPK = ce.save(content); } stmt = con.createStatement(); List<Long> folderIds = new ArrayList<Long>(50); //fetch all referenced folders ResultSet rs = stmt.executeQuery("SELECT DISTINCT c.ID FROM " + TBL_CONTENT + " c WHERE c.TDEF=" + CacheAdmin.getEnvironment().getType(FxType.FOLDER).getId() + " AND c.ID IN (SELECT DISTINCT t.REF FROM " + getTable(mode) + " t)"); while (rs != null && rs.next()) { if (rootPK.getId() != rs.getLong(1)) folderIds.add(rs.getLong(1)); } wipeTree(mode, stmt, rootPK); //reset sequencer StorageManager.getSequencerStorage().setSequencerId(mode.getSequencer().getSequencerName(), 1); if (mode == FxTreeMode.Live) { //flag all edit nodes back to dirty stmt.executeUpdate("UPDATE " + getTable(FxTreeMode.Edit) + " SET DIRTY=" + StorageManager.getBooleanTrueExpression() + " WHERE ID<>" + ROOT_NODE); } // stmt.executeBatch(); if (folderIds.size() > 0) { //remove all referenced folders (or try to at least) int removed = 0; for (long folderId : folderIds) { if (folderId == rootPK.getId()) continue; //no need to test reference count for the root node as it IS used FxPK folderPK = new FxPK(folderId); if (ce.getReferencedContentCount(folderPK) == 0) { ce.remove(folderPK); removed++; } } LOG.info("Removed " + removed + "/" + folderIds.size() + " Folder instances"); } } catch (Exception e) { throw new FxTreeException(LOG, e, "ex.tree.clear.failed", mode.name(), e.getMessage()); } finally { try { if (stmt != null) stmt.close(); } catch (Throwable t) { /*ignore*/ } } } /** * Remove all tree nodes and create a new root node * * @param mode tree mode * @param stmt statement * @param rootPK primary key of the root content instance * @throws SQLException on errors */ protected abstract void wipeTree(FxTreeMode mode, Statement stmt, FxPK rootPK) throws SQLException; /** * Lock tree nodes for updates. * * @param con an existing connection * @param table the table name to be locked * @param nodeIds the node ID(s) to be locked. If null or empty, the entire table will be locked. * @return true if a lock could be required, false if the DBMS indicated a deadlock * @throws FxDbException on non-deadlock DB errors */ protected abstract boolean lockForUpdate(Connection con, String table, Iterable<Long> nodeIds) throws FxDbException; /** * Lock tree nodes for updates. * * @param con an existing connection * @param table the table name to be locked * @param referenceIds the reference ID(s) to be locked. If null or empty, the entire table will be locked. * @return true if a lock could be required, false if the DBMS indicated a deadlock * @throws FxDbException on non-deadlock DB errors */ protected abstract boolean lockForUpdateReference(Connection con, String table, Iterable<Long> referenceIds) throws FxDbException; /** * Acquire update locks for updating tree nodes. To lock the entire tree, pass {@code null} * or an empty list for {@code nodeIds}. * <p> * If (some of) the nodes could not be locked due to a rollback by the DBMS, the method waits for a * short period of time before it tries to acquire the lock again. Non-deadlock exceptions * are not caught and are thrown to the caller. * </p> * * @param con an existing connection * @param mode the tree mode * @param nodeIds the tree node IDs * @throws FxDbException on database errors */ protected void acquireLocksForUpdate(Connection con, FxTreeMode mode, Iterable<Long> nodeIds) throws FxDbException { while (!lockForUpdate(con, getTable(mode), nodeIds)) { if (LOG.isInfoEnabled()) { LOG.info("Failed to lock " + getTable(mode) + ", waiting 100ms and retrying..."); } try { Thread.sleep(100); } catch (InterruptedException e) { // ignore } } } /** * Acquire update locks for a node and all of its parents and/or children. * <p> * If the subtree could not be locked due to a rollback by the DBMS, the method waits for a * short period of time before it tries to acquire the lock again. Non-deadlock exceptions * are not caught and are thrown to the called. * </p> * * @param con an existing connection * @param nodeInfo the node to be locked * @param lockParent if the node's parent should also be locked * @throws FxDbException on database errors */ protected void acquireLocksForUpdate(Connection con, FxTreeNodeInfo nodeInfo, boolean lockParent) throws FxDbException { final List<Long> ids = new ArrayList<Long>(2); ids.add(nodeInfo.getId()); if (lockParent) { ids.add(nodeInfo.getParentId()); } acquireLocksForUpdate(con, nodeInfo.getMode(), ids); } protected void acquireLocksByReference(Connection con, FxTreeMode mode, Iterable<Long> referenceIds) throws FxDbException { while (!lockForUpdateReference(con, getTable(mode), referenceIds)) { if (LOG.isInfoEnabled()) { LOG.info("Failed to lock " + getTable(mode) + " for reference list " + referenceIds + ", waiting 100ms and retrying..."); } try { Thread.sleep(100); } catch (InterruptedException e) { // ignore } } } /** * Acquire update locks for the entire node tree. * <p> * If the node could not be locked due to a rollback by the DBMS, the method waits for a * short period of time before it tries to acquire the lock again. Non-deadlock exceptions * are not caught and are thrown to the called. * </p> * * @param con an existing connection * @param mode the tree mode (edit or live) * @throws FxDbException on database errors */ protected void acquireLocksForUpdate(Connection con, FxTreeMode mode) throws FxDbException { acquireLocksForUpdate(con, mode, null); } /** * {@inheritDoc} */ @Override public void removeNode(Connection con, FxTreeMode mode, ContentEngine ce, long nodeId, boolean removeChildren) throws FxApplicationException { if (mode == FxTreeMode.Live) removeChildren = true; //always delete child nodes in live mode Statement stmt = null; if (nodeId == FxTreeNode.ROOT_NODE) throw new FxNoAccessException("ex.tree.delete.root"); FxTreeNodeInfo nodeInfo = getTreeNodeInfo(con, mode, nodeId); ScriptingEngine scripting = EJBLookup.getScriptingEngine(); final List<Long> scriptBeforeIds = scripting.getByScriptEvent(FxScriptEvent.BeforeTreeNodeRemoved); final List<Long> scriptAfterIds = scripting.getByScriptEvent(FxScriptEvent.AfterTreeNodeRemoved); //warning: removedNodes will only be available if script mappings for event AfterTreeNodeRemoved exist! List<FxTreeNode> removedNodes = scriptAfterIds.size() > 0 ? new ArrayList<FxTreeNode>(100) : null; final String TRUE = StorageManager.getBooleanTrueExpression(); try { stmt = con.createStatement(); if (StorageManager.isDisableIntegrityTransactional()) { stmt.execute(StorageManager.getReferentialIntegrityChecksStatement(false)); } List<FxPK> references = new ArrayList<FxPK>(50); UserTicket ticket = FxContext.getUserTicket(); // lock all affected rows final List<Long> removeNodeIds = selectAllChildNodeIds(con, mode, nodeInfo.getLeft(), nodeInfo.getRight(), true); acquireLocksForUpdate(con, mode, Iterables.concat(removeNodeIds, Arrays.asList(nodeInfo.getParentId()))); final Map<FxPK, FxContentSecurityInfo> securityInfos = Maps .newHashMapWithExpectedSize(removeNodeIds.size()); if (removeChildren) { //FX-102: edit permission checks on references ResultSet rs = stmt.executeQuery("SELECT DISTINCT REF FROM " + getTable(mode) + " WHERE " + " LFT>=" + nodeInfo.getLeft() + " AND RGT<=" + nodeInfo.getRight() + " "); while (rs != null && rs.next()) { try { if (ce != null) { final FxPK pk = new FxPK(rs.getLong(1)); final FxContentSecurityInfo info = ce.getContentSecurityInfo(pk); FxPermissionUtils.checkPermission(ticket, ACLPermission.EDIT, info, true); securityInfos.put(pk, info); } references.add(new FxPK(rs.getLong(1))); } catch (FxLoadException e) { //ignore, might have been removed meanwhile } } // call BeforeTreeNodeRemoved scripts if (scriptBeforeIds.size() > 0 || scriptAfterIds.size() > 0) { final FxScriptBinding binding = new FxScriptBinding(); for (long removedId : removeNodeIds) { final FxTreeNode n = getNode(con, mode, removedId); if (removedNodes != null) removedNodes.add(n); for (long scriptId : scriptBeforeIds) { binding.setVariable("node", n); scripting.runScript(scriptId, binding); } } } for (List<Long> removeIds : Iterables.partition(removeNodeIds, SQL_IN_PARTSIZE)) { stmt.addBatch("DELETE FROM " + getTable(mode) + " WHERE id IN (" + StringUtils.join(removeIds, ',') + ")"); } } else { //FX-102: edit permission checks on references try { if (ce != null) { final FxContentSecurityInfo info = ce.getContentSecurityInfo(nodeInfo.getReference()); FxPermissionUtils.checkPermission(FxContext.getUserTicket(), ACLPermission.EDIT, info, true); securityInfos.put(nodeInfo.getReference(), info); } references.add(nodeInfo.getReference()); } catch (FxLoadException e) { //ignore, might have been removed meanwhile } stmt.addBatch("UPDATE " + getTable(mode) + " SET PARENT=" + nodeInfo.getParentId() + " WHERE PARENT=" + nodeId); for (List<Long> part : Iterables.partition(removeNodeIds, SQL_IN_PARTSIZE)) { stmt.addBatch("UPDATE " + getTable(mode) + " SET DEPTH=DEPTH-1,DIRTY=" + StorageManager.getBooleanExpression(mode != FxTreeMode.Live) + " WHERE id IN (" + StringUtils.join(part, ',') + ") AND DEPTH>0"); } stmt.addBatch("DELETE FROM " + getTable(mode) + " WHERE ID=" + nodeId); } // Update the childcount of the parents if (removeChildren) { stmt.addBatch("UPDATE " + getTable(mode) + " SET CHILDCOUNT=CHILDCOUNT-1 WHERE ID=" + nodeInfo.getParentId()); } else { stmt.addBatch("UPDATE " + getTable(mode) + " SET CHILDCOUNT=CHILDCOUNT+" + (nodeInfo.getDirectChildCount() - 1) + " WHERE ID=" + nodeInfo.getParentId()); } // Set the dirty flag for the parent if needed if (mode != FxTreeMode.Live) { stmt.addBatch( "UPDATE " + getTable(mode) + " SET DIRTY=" + TRUE + " WHERE ID=" + nodeInfo.getParentId()); } if (mode == FxTreeMode.Live && exists(con, FxTreeMode.Edit, nodeId)) { //check if a node with the same id that has been removed in the live tree exists in the edit tree, //the node and all its children will be flagged as dirty in the edit tree FxTreeNodeInfo editNode = getTreeNodeInfo(con, FxTreeMode.Edit, nodeId); List<Long> editNodeIds = selectAllChildNodeIds(con, FxTreeMode.Edit, editNode.getLeft(), editNode.getRight(), true); acquireLocksForUpdate(con, FxTreeMode.Edit, editNodeIds); for (List<Long> part : Iterables.partition(editNodeIds, SQL_IN_PARTSIZE)) { stmt.addBatch("UPDATE " + getTable(FxTreeMode.Edit) + " SET DIRTY=" + TRUE + " WHERE ID IN (" + StringUtils.join(part, ',') + ")"); } } stmt.executeBatch(); if (ce != null) { //if the referenced content is a folder, remove it final Set<Long> folderTypeIds = Sets.newHashSet(FxSharedUtils.getSelectableObjectIdList( CacheAdmin.getEnvironment().getType(FxType.FOLDER).getDerivedTypes(true, true))); for (FxPK ref : references) { FxContentSecurityInfo si = securityInfos.get(ref); if (si == null) { si = ce.getContentSecurityInfo(ref); } if (folderTypeIds.contains(si.getTypeId())) { final int contentCount = ce.getReferencedContentCount(si.getPk()); if (contentCount == 0) { ce.remove(ref); } } } } afterNodeRemoved(con, nodeInfo, removeChildren); if (removedNodes != null) { final FxScriptBinding binding = new FxScriptBinding(); for (long scriptId : scriptAfterIds) { for (FxTreeNode n : removedNodes) { binding.setVariable("node", n); scripting.runScript(scriptId, binding); } } } } catch (SQLException exc) { String next = ""; if (exc.getNextException() != null) next = " next:" + exc.getNextException().getMessage(); throw new FxRemoveException(LOG, exc, "ex.tree.delete.failed", nodeId, exc.getMessage() + next); } finally { try { if (stmt != null) { if (StorageManager.isDisableIntegrityTransactional()) { try { stmt.execute(StorageManager.getReferentialIntegrityChecksStatement(true)); } catch (SQLException e) { LOG.error(e); } } stmt.close(); } } catch (Exception exc) { //ignore } } } /** * Get all node ids between the left and right boundaries * * @param con an open and valid connection * @param mode tree mode * @param left left boundary * @param right right boundary * @param includeRoot if the root node (of the subtree) itself should be included * @return node id's between the boundaries * @throws SQLException on errors */ protected List<Long> selectAllChildNodeIds(Connection con, FxTreeMode mode, Number left, Number right, boolean includeRoot) throws SQLException { PreparedStatement stmt = null; try { final String rangeIncl = includeRoot ? "=" : ""; stmt = con.prepareStatement("SELECT id FROM " + getTable(mode) + " WHERE lft >" + rangeIncl + " ? AND rgt <" + rangeIncl + " ?"); setNodeBounds(stmt, 1, new BigInteger(left.toString())); setNodeBounds(stmt, 2, new BigInteger(right.toString())); return collectNodeIds(stmt); } finally { Database.closeObjects(GenericTreeStorage.class, null, stmt); } } /** * Get all node ids that are a direct child of <code>parentId</code> * * @param con an open and valid connection * @param mode tree mode * @param parentId the parent node id * @param includeParent include the parent id in the returned node id list? * @return found node id's * @throws SQLException on errors */ protected List<Long> selectDirectChildNodeIds(Connection con, FxTreeMode mode, long parentId, boolean includeParent) throws SQLException { PreparedStatement stmt = null; try { stmt = con.prepareStatement( "SELECT id FROM " + getTable(mode) + " WHERE PARENT=? " + (includeParent ? "OR ID=?" : "")); stmt.setLong(1, parentId); if (includeParent) { stmt.setLong(2, parentId); } return collectNodeIds(stmt); } finally { Database.closeObjects(GenericTreeStorage.class, null, stmt); } } /** * Collect all long values of the first column in the result set as a list of long's * * @param stmt the prepared statement that contains the query, ready to be executed * @return list of found values * @throws SQLException on errors */ private List<Long> collectNodeIds(PreparedStatement stmt) throws SQLException { final ResultSet rs = stmt.executeQuery(); final List<Long> ids = new ArrayList<Long>(); while (rs.next()) { ids.add(rs.getLong(1)); } return ids; } /** * Perform needed cleanups like closing gaps after a node has been removed * * @param con an open and valid connection * @param nodeInfo info about the removed node * @param removeChildren children removed or moved up a level? * @throws FxApplicationException on errors */ protected abstract void afterNodeRemoved(Connection con, FxTreeNodeInfo nodeInfo, boolean removeChildren) throws FxApplicationException; /** * {@inheritDoc} */ @Override public void setData(Connection con, FxTreeMode mode, long nodeId, String data) throws FxApplicationException { checkDataValue(data); FxTreeNodeInfo node = getTreeNodeInfo(con, mode, nodeId); // Any changes at all? if (node.hasData(data)) return; // Set the data PreparedStatement ps = null; try { DBStorage st = StorageManager.getStorageImpl(); ps = con.prepareStatement( "UPDATE " + getTable(mode) + " SET TEMPLATE=?,DIRTY=" + st.getBooleanTrueExpression() + ",MODIFIED_AT=" + st.getTimestampFunction() + " WHERE ID=" + nodeId); if (data == null) ps.setNull(1, java.sql.Types.VARCHAR); else ps.setString(1, data); ps.executeUpdate(); } catch (Throwable t) { throw new FxUpdateException(LOG, "ex.tree.setData.failed", data, nodeId, t.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (Throwable t) { //ignore } } } /** * {@inheritDoc} */ @Override public void activateAll(Connection con, FxTreeMode mode) throws FxTreeException { Statement stmt = null; PreparedStatement ps = null; try { final String FALSE = StorageManager.getBooleanFalseExpression(); acquireLocksForUpdate(con, FxTreeMode.Live); stmt = con.createStatement(); stmt.addBatch(StorageManager.getReferentialIntegrityChecksStatement(false)); stmt.addBatch("DELETE FROM " + getTable(FxTreeMode.Live)); stmt.addBatch("INSERT INTO " + getTable(FxTreeMode.Live) + " SELECT * FROM " + getTable(mode)); stmt.addBatch("UPDATE " + getTable(mode) + " SET DIRTY=" + FALSE); stmt.addBatch("UPDATE " + getTable(FxTreeMode.Live) + " SET DIRTY=" + FALSE); stmt.addBatch(StorageManager.getReferentialIntegrityChecksStatement(true)); stmt.executeBatch(); //FX-793: activate nodes that are not in the live version ps = con.prepareStatement("SELECT DISTINCT c.ID, c.MAX_VER FROM " + getTable(FxTreeMode.Live) + " l, " + DatabaseConst.TBL_CONTENT + " c WHERE l.REF IS NOT NULL AND c.ID=l.REF AND c.LIVE_VER=0"); ResultSet rs = ps.executeQuery(); final ContentEngine ce = EJBLookup.getContentEngine(); while (rs != null && rs.next()) { FxPK pk = new FxPK(rs.getLong(1), rs.getInt(2)); FxContent co = ce.load(pk); //create a Live version pk = createContentLiveVersion(ce, co); LOG.info("Created new live version " + pk + " during tree activation"); } if (rs != null) rs.close(); } catch (Throwable t) { throw new FxTreeException(LOG, t, "ex.tree.activate.all.failed", mode.name(), t.getMessage()); } finally { Database.closeObjects(GenericTreeStorage.class, stmt, ps); } } /** * Gets the next CopyOf___(Id) * * @param con an open and valid connection * @param mode tree mode * @param copyOfPrefix the prefix to search for * @param parentNodeId parent node id * @param nodeId node id * @return the CopyOf number * @throws FxTreeException on errors */ protected int getCopyOfCount(Connection con, FxTreeMode mode, String copyOfPrefix, long parentNodeId, long nodeId) throws FxTreeException { Statement stmt = null; try { // Update the childcount of the new parents stmt = con.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT COUNT(*) FROM " + getTable(mode) + " WHERE PARENT=" + parentNodeId + " AND NAME LIKE " + // "(SELECT CONCAT(CONCAT('" + copyOfPrefix + "%',NAME),'%') FROM " + getTable(mode) + " WHERE ID=" + nodeId + ")"); "(SELECT " + StorageManager.concat("'" + copyOfPrefix + "%'", "NAME", "'%'") + " FROM " + getTable(mode) + " WHERE ID=" + nodeId + ")"); int result = 0; if (rs.next()) { result = rs.getInt(1); } return result; } catch (SQLException e) { throw new FxTreeException(LOG, "ex.tree.copyOf.failed", nodeId, parentNodeId, mode.name(), copyOfPrefix, e.getMessage()); } finally { try { if (stmt != null) stmt.close(); } catch (Exception exc) { //ignore } } } /** * {@inheritDoc} */ @Override public void populate(Connection con, SequencerEngine seq, ContentEngine ce, FxTreeMode mode, int maxNodeChildren) throws FxApplicationException { try { for (int i = 0; i < maxNodeChildren; i++) { LOG.info("Creating level [" + (i + 1) + "/" + maxNodeChildren + "]"); String name = "Level2_[" + i + "]"; FxString desc = new FxString(true, "Level2 Node " + i); long newIdLevel1 = createNode(con, seq, ce, mode, -1, ROOT_NODE, name, desc, 0, null, null, true); for (int y = 0; y < 10; y++) { String name2 = "Level3_[" + i + "|" + y + "]"; FxString desc2 = new FxString(true, "Level3 Node " + y); long newIdLevel2 = createNode(con, seq, ce, mode, -1, newIdLevel1, name2, desc2, 0, null, null, true); for (int t = 0; t < 100; t++) { String name3 = "Level4_[" + i + "|" + y + "|" + t + "]"; FxString desc3 = new FxString(true, "Level4 Node " + y); createNode(con, seq, ce, mode, -1, newIdLevel2, name3, desc3, 0, null, null, true); } } LOG.info("Created level [" + (i + 1) + "/" + maxNodeChildren + "]"); } } catch (Exception e) { throw new FxApplicationException(LOG, e, "ex.tree.populate", mode.name(), e.getMessage()); } } /** * {@inheritDoc} */ @Override public void contentRemoved(Connection con, long contentId, boolean liveVersionRemovedOnly) throws FxApplicationException { PreparedStatement ps = null; try { // lock affected nodes acquireLocksByReference(con, FxTreeMode.Edit, Arrays.asList(contentId)); acquireLocksByReference(con, FxTreeMode.Live, Arrays.asList(contentId)); List<Long> edit = new ArrayList<Long>(10), live = new ArrayList<Long>(10); long typeId = -1; ps = con.prepareStatement(TREE_REF_USAGE_EDIT); ps.setLong(1, contentId); ResultSet rs = ps.executeQuery(); while (rs != null && rs.next()) { edit.add(rs.getLong(2)); if (typeId == -1) typeId = rs.getLong(1); } ps.close(); ps = con.prepareStatement(TREE_REF_USAGE_LIVE); ps.setLong(1, contentId); rs = ps.executeQuery(); while (rs != null && rs.next()) { live.add(rs.getLong(2)); if (typeId == -1) typeId = rs.getLong(1); } if (typeId == -1) return; //nothing found ContentEngine ce = EJBLookup.getContentEngine(); FxPK folderPK = null; for (long liveNode : live) { boolean inEdit = edit.contains(liveNode); if (liveVersionRemovedOnly || !inEdit) try { removeNode(con, FxTreeMode.Live, ce, liveNode, true); } catch (FxNotFoundException e) { //ok, may be an already removed childnode } else { //if the node is a leaf then remove the node, else replace the referenced content with a new folder instance try { if (inEdit) folderPK = handleContentDeleted(con, ce, folderPK, typeId, getTreeNodeInfo(con, FxTreeMode.Edit, liveNode)); } catch (FxNotFoundException e) { //ok, may be an already removed childnode } try { folderPK = handleContentDeleted(con, ce, folderPK, typeId, getTreeNodeInfo(con, FxTreeMode.Live, liveNode)); } catch (FxNotFoundException e) { try { //try to remove even if not found since the node might exist but the //content may not exist in the live step removeNode(con, FxTreeMode.Live, ce, liveNode, true); } catch (FxApplicationException e1) { //ignore } //ok, may be an already removed childnode } } } if (!liveVersionRemovedOnly) { edit.removeAll(live); //remaining nodes only exist in edit tree for (long editNode : edit) try { folderPK = handleContentDeleted(con, ce, folderPK, typeId, getTreeNodeInfo(con, FxTreeMode.Edit, editNode)); } catch (FxNotFoundException e) { //ok, may be an already removed childnode } } } catch (SQLException se) { throw new FxTreeException(LOG, "ex.db.sqlError", se.getMessage()); } finally { FxContext.get().setTreeWasModified(); try { if (ps != null) ps.close(); } catch (Throwable t) { //ignore } } } /** * Handle removal of a content instance * * @param con an open and valid connection * @param ce a reference to the content engine * @param folderPK folder pk (if null a new one will be created if needed) * @param typeId type of the content * @param nodeInfo node info * @return folder PK * @throws FxApplicationException on errors */ protected FxPK handleContentDeleted(Connection con, ContentEngine ce, FxPK folderPK, long typeId, FxTreeNodeInfo nodeInfo) throws FxApplicationException { if (!nodeInfo.hasChildren()) { //leaf node - remove it removeNode(con, nodeInfo.getMode(), null, nodeInfo.getId(), false); return folderPK; } //if the referenced content is a folder, remove it without removing its children in edit mode and remove it with children in live mode if (CacheAdmin.getEnvironment().getType(typeId).isDerivedFrom(FxType.FOLDER)) { removeNode(con, nodeInfo.getMode(), null, nodeInfo.getId(), nodeInfo.getMode() == FxTreeMode.Live); return folderPK; } //No folder and no leaf - replace the content with a new folder instance FxTreeNode node = getNode(con, nodeInfo.getMode(), nodeInfo.getId()); if (folderPK == null) folderPK = createFolderInstance(ce, node.getName(), node.getLabel()); PreparedStatement ps = null; try { ps = con.prepareStatement("UPDATE " + getTable(nodeInfo.getMode()) + " SET REF=? WHERE ID=?"); ps.setLong(1, folderPK.getId()); ps.setLong(2, node.getId()); ps.executeUpdate(); } catch (SQLException se) { throw new FxTreeException(LOG, "ex.db.sqlError", se.getMessage()); } finally { try { if (ps != null) ps.close(); } catch (SQLException e) { //ignore } } //scripting ScriptingEngine scripting = EJBLookup.getScriptingEngine(); List<Long> scriptIds = scripting.getByScriptEvent(FxScriptEvent.AfterTreeNodeFolderReplacement); if (scriptIds.size() == 0) return folderPK; FxScriptBinding binding = new FxScriptBinding(); binding.setVariable("node", getNode(con, nodeInfo.getMode(), nodeInfo.getId())); binding.setVariable("content", nodeInfo.getReference()); for (long scriptId : scriptIds) scripting.runScript(scriptId, binding); return folderPK; } /** * {@inheritDoc} */ @Override public String[] beforeContentVersionRemoved(Connection con, long id, int version, FxContentVersionInfo cvi) throws FxApplicationException { // if (cvi.hasLiveVersion() && version == cvi.getLiveVersion()) { // contentRemoved(con, id, version != cvi.getMaxVersion(), true); // } if (cvi.getVersionCount() > 1) { //deactivate ref. integrity to allow removal Statement stmt = null; try { stmt = con.createStatement(); ResultSet rs = stmt .executeQuery("SELECT DISTINCT ID FROM " + getTable(FxTreeMode.Edit) + " WHERE REF=" + id); StringBuilder sb = new StringBuilder(SQL_IN_PARTSIZE); while (rs != null && rs.next()) { if (sb.length() > 0) sb.append(","); sb.append(rs.getLong(1)); } String[] nodes = new String[2]; nodes[0] = sb.toString(); sb.setLength(0); rs = stmt.executeQuery("SELECT DISTINCT ID FROM " + getTable(FxTreeMode.Live) + " WHERE REF=" + id); while (rs != null && rs.next()) { if (sb.length() > 0) sb.append(","); sb.append(rs.getLong(1)); } nodes[1] = sb.toString(); if (nodes[0].length() == 0 && nodes[1].length() == 0) return null; stmt.executeUpdate("UPDATE " + getTable(FxTreeMode.Edit) + " SET REF=(SELECT MIN(ID) FROM " + TBL_CONTENT + ") WHERE REF=" + id); stmt.executeUpdate("UPDATE " + getTable(FxTreeMode.Live) + " SET REF=(SELECT MIN(ID) FROM " + TBL_CONTENT + ") WHERE REF=" + id); return nodes; } catch (SQLException e) { throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage()); } finally { try { if (stmt != null) stmt.close(); } catch (SQLException e) { //ignore } } } return null; } /** * {@inheritDoc} */ @Override public void afterContentVersionRemoved(String[] nodes, Connection con, long id, int version, FxContentVersionInfo cvi) throws FxApplicationException { if (nodes == null || nodes.length == 0) return; if (nodes.length == 2) { //reactivate ref. integrity Statement stmt = null; try { stmt = con.createStatement(); if (!StringUtils.isEmpty(nodes[0])) stmt.executeUpdate("UPDATE " + getTable(FxTreeMode.Edit) + " SET REF=" + id + " WHERE ID IN (" + nodes[0] + ")"); if (!StringUtils.isEmpty(nodes[1])) stmt.executeUpdate("UPDATE " + getTable(FxTreeMode.Live) + " SET REF=" + id + " WHERE ID IN (" + nodes[1] + ")"); } catch (SQLException e) { throw new FxDbException(LOG, e, "ex.db.sqlError", e.getMessage()); } finally { try { if (stmt != null) stmt.close(); } catch (SQLException e) { //ignore } } } } /** * {@inheritDoc} */ @Override public void checkTreeIfEnabled(Connection con, FxTreeMode mode) throws FxApplicationException { if (!EJBLookup.getDivisionConfigurationEngine().get(SystemParameters.TREE_CHECKS_ENABLED)) return; checkTree(con, mode); } protected List<Long> fetchACLs(Connection con, FxPK reference, long mainTableACL) throws SQLException { final List<Long> acls; if (mainTableACL == ACL.NULL_ACL_ID) { acls = fetchNodeACLs(con, reference); } else { acls = Arrays.asList(mainTableACL); } return acls; } protected List<Long> fetchNodeACLs(Connection con, FxPK reference) throws SQLException { PreparedStatement ps2 = null; try { ps2 = con.prepareStatement(TREE_NODEINFO_ACLS); ps2.setLong(1, reference.getId()); ps2.setInt(2, reference.getVersion()); ps2.setLong(3, reference.getId()); ps2.setInt(4, reference.getVersion()); final ResultSet rs2 = ps2.executeQuery(); final List<Long> aclIds = Lists.newArrayList(); while (rs2.next()) { aclIds.add(rs2.getLong(1)); } return aclIds; } finally { Database.closeObjects(GenericTreeStorage.class, null, ps2); } } /** * Check a tree for invalid nodes, will throw a FxTreeException on errors * * @param con an open and valid connection * @param mode the tree to check * @throws com.flexive.shared.exceptions.FxTreeException * on errors */ public abstract void checkTree(Connection con, FxTreeMode mode) throws FxApplicationException; /** * Replace dynamic placeholders in the given select statement. * * @param sql the SQL statement * @return the final SQL statement * @since 3.1 */ protected String prepareSql(FxTreeMode mode, String sql) { String result = sql; if (sql.contains(COMPUTE_TOTAL_CHILDCOUNT)) { // compute total child count of a node only when childcount > 0 result = sql.replace(COMPUTE_TOTAL_CHILDCOUNT, StorageManager.getIfFunction("CHILDCOUNT = 0", "0", mode == FxTreeMode.Live ? TREE_TOTAL_COUNT_LIVE : TREE_TOTAL_COUNT_EDIT) + " AS TOTAL_CHILDCOUNT"); } return result; } protected BigInteger getNodeBounds(ResultSet rs, int index) throws SQLException { final String value = rs.getString(index); return value == null ? BigInteger.ZERO : new BigInteger(value); } protected void setNodeBounds(PreparedStatement stmt, int index, BigInteger value) throws SQLException { // Postgres 8.x does not perform automatic conversion from String (unlike other DBMS) stmt.setBigDecimal(index, new BigDecimal(value.toString())); } }