ShadowRemapper.java
/*
** Module :ShadowRemapper.java
** Abstract :Back-end for tracking batch edit session changes.
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description-----------------
** 001 SIY 20050506 @21205 Created initial version.
** 002 SIY 20050524 @21261 Fixed moving nodes.
** 003 SIY 20090514 @42180 Added refresh() method, some minor cleanups.
** 004 SIY 20090531 @42503 Reworked and cleaned up implementation. Fixed
** node deletion issue.
** 005 SIY 20090712 @43138 Fixed moving nodes. Also, changes now are applied
** in logically consistent order to make sure that
** all prerequisites are satisfied.
** 006 SIY 20090910 @43867 Fixed chained node moves of nodes at the same
** parent.
** 007 SIY 20091007 @44126 Committing changes is moved from DirectoryService
** to this class. Completely reworked entire
** committing algorithm to make it as safe as
** possible in regard to possible directory corruption.
** Moved rollback algorithm into this class.
** 008 HC 20170612 More logging added.
** 009 TJD 20220504 Java 11 compatibility minor changes
** 010 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 011 SP 20250416 Used CaseInsensitive maps for changed, removed, moved. Removed toLowerCase().
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.directory;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;
import java.util.*;
import java.util.function.*;
/**
* This class implements an <code>Remapper</code> interface which is used to
* hold changes made by application during batch editing session. It uses real
* back-end to retrieve unchanged data and holds changed data in memory. When
* application accesses the data they are present as if all changes are
* actually made in the real back-end.
* <p>
* General concept of used approach is rather simple: hold all changes which
* are necessary to convert backend into new state. While changes are not
* applied, information about these changes is used to handle requests so
* users of this class see backend as if all changes are already applied.
* <p>
* Internally all changes are stored in three maps - changed nodes, removed
* nodes and moved nodes. Changed nodes are those which were created or their
* content was changed. Removed nodes contains all nodes which were explicitly
* or implicitly (for example by moving) removed. And moved nodes map contains
* nodes which were moved on user request to the new location.
* <p>
* When changes are applied implementation performs operations in particular
* order which guarantees that operations are compatible. For example, when it
* moves node to the new location it adds all necessary intermediate nodes
* present in changed nodes map or when subtree is deleted, leaf nodes are
* deleted first, then deleted their parents, then parents of parents and so
* forth.
*
* @author SIY
* @version 1.0
*/
class ShadowRemapper
implements Remapper
{
/** The logger instance. */
private static final CentralLogger LOG = CentralLogger.get(ShadowRemapper.class);
/** Object which is used for instance data access synchronization */
protected final Object mutex = new Object();
/** Real back-end with data */
private Remapper backend = null;
/** Changed nodes */
private Map<String, DirNode> changed = new CaseInsensitiveHashMap<>();
/** Removed nodes */
private Map<String, DirNode> removed = new CaseInsensitiveHashMap<>();
/** Moved nodes */
private Map<String, String> moved = new CaseInsensitiveLinkedHashMap<>();
/**
* Construct an instance of the XmlRemapper for a given file.
*
* @param backend
* Real back-end with data.
*/
ShadowRemapper(Remapper backend)
{
this.backend = backend;
}
/**
* Logs a warning message.
*
* @param nodeId
* Node name that will become part of the resulting warning message.
* @param msg
* The message to log.
*/
private static void logWarning(String nodeId, String msg)
{
LOG.warning(msg + " [nodeId=" + nodeId + "]");
}
/**
* Add new node of specified class with specified ID. For node
* initialization list of attributes and corresponding list of attribute
* values should be provided. The presence of the mandatory attributes in
* the lists is obligatory.
*
* @param nodeId
* Node name.
* @param nodeClass
* Node class name.
* @param names
* A list of names of the attributes.
* @param values
* A list of values of the attributes.
*
* @return <code>true</code> if operation was successful and
* <code>false</code> otherwise.
*/
public boolean addNode(String nodeId, String nodeClass, String[] names,
Object[] values)
{
nodeId = IdUtils.normalize(nodeId);
String[] path = IdUtils.splitId(nodeId);
Consumer<String> trace = msg -> LOG.fine("Failed to add node. " + msg);
if (path == null)
{
trace.accept("Invalid node path.");
return false;
}
DirNode node = locateNode(path[0], false);
if (node == null)
{
trace.accept("Parent does not exist.");
return false; //Parent does not exists
}
node = locateNode(nodeId, false);
if (node != null)
{
trace.accept("A node of the same path already exists.");
return false; //Already exists
}
node = validateNode(path[1], nodeClass, names, values);
if (node == null)
{
LOG.warning("The node is not valid.");
return false;
}
//Load intermediate nodes
locateNode(path[0], true);
synchronized (mutex)
{
DirNode tmpNode = changed.get(path[0]);
if (tmpNode != null)
{
tmpNode.addChild(path[1]);
}
removed.remove(nodeId); //Clear node just in case
changed.put(nodeId, node); //Add it to list of changed
}
return true;
}
/**
* Add new <code>BitField</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeBitField(String nodeId, String name, BitField value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Add new <code>BitSelector</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeBitSelector(String nodeId, String name,
BitSelector value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Add new <code>boolean</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeBoolean(String nodeId, String name, boolean value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Add new <code>byte array</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeByteArray(String nodeId, String name, byte[] value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Add new <code>DateValue</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeDate(String nodeId, String name, DateValue value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Add new <code>double</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeDouble(String nodeId, String name, double value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Add new <code>int</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeInteger(String nodeId, String name, int value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Add new <code>String</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeString(String nodeId, String name, String value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Add new <code>TimeValue</code> value to the attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean addNodeTime(String nodeId, String name, TimeValue value)
{
return addNodeValue(nodeId, name, value);
}
/**
* Initialise back-end and open session.
*
* @return <code>true</code> if operation was successful.
*/
public boolean bind()
{
return true;
}
/**
* No-op implementation. This class does not support refreshing and always
* assumes that refreshing was successful.
*
* @return <code>true</code> if operation was successful.
*/
public boolean refresh()
{
return true;
}
/**
* Remove specified node from the directory (unless node has children).
*
* @param nodeId
* Node identifier.
*
* @return <code>true</code> if operation was successful and
* <code>false</code> otherwise.
*/
public boolean deleteNode(String nodeId)
{
String finalNodeId = nodeId;
Consumer<String> trace = msg -> LOG.fine("Failed to delete node '" + finalNodeId + "'. " + msg);
//Check if node is already in the list
DirNode node = locateNode(nodeId, false);
if (node == null)
{
trace.accept("It could not be located.");
return false;
}
nodeId = IdUtils.normalize(nodeId);
if (node.getChildCount() != 0)
{
// node can be deleted if all its child nodes also listed for deletion
// Note, that we don't need to check child nodes recursively, because
// they also can be marked for deletion only if their child nodes are
// ready for deletion.
String[] childNames = node.getChildNames();
for (String child : childNames)
{
if(!removed.containsKey(nodeId + "/" + child))
{
trace.accept("The child node '" + child + "' is not marked for deletion.");
return false;
}
}
}
synchronized (mutex)
{
String[] path = IdUtils.splitId(nodeId);
if (path != null)
{
DirNode tmpNode = changed.get(path[0]);
if (tmpNode != null)
{
tmpNode.removeChild(path[1]);
}
}
changed.remove(nodeId);
removed.put(nodeId, node);
}
return true;
}
/**
* Remove whole node attribute. Note that call will fail if attribute is
* marked mandatory.
*
* @param nodeId
* Node name.
* @param name
* Attribute name.
*
* @return <code>true</code> if operation was successful and
* <code>false</code> otherwise.
*/
public boolean deleteNodeAttribute(String nodeId, String name)
{
//Check if node is already in the list
DirNode node = locateNode(nodeId, true);
if (node == null)
{
return false;
}
return node.removeAttribute(name);
}
/**
* Remove particular value from the node attribute. Note that removing last
* value from the attribute deletes entire attribute unless attribute is
* marked mandatory.
*
* @param nodeId
* Node name.
* @param name
* Attribute name.
* @param index
* Attribute value index.
*
* @return <code>true</code> if operation was successful and
* <code>false</code> otherwise.
*/
public boolean deleteNodeAttributeValue(String nodeId, String name,
int index)
{
//Check if node is already in the list
DirNode node = locateNode(nodeId, true);
if (node == null)
{
return false;
}
Attribute attr = node.getAttribute(name);
if (attr == null)
{
return false;
}
if (!attr.deleteValue(index))
{
return false;
}
if (attr.getCount() == 0)
{
return node.removeAttribute(name);
}
return true;
}
/**
* Return list of all node attribute definitions.
*
* @param nodeId
* Node for which list of attributes is requested.
*
* @return Array of attribute definitions.
*/
public AttributeDefinition[] enumerateNodeAttributes(String nodeId)
{
synchronized (mutex)
{
DirNode node = locateNode(nodeId, true);
if (node == null)
{
return null;
}
AttributeDefinition[] src = node.getNodeClass().getClassDefinition();
if (src == null)
{
return src;
}
//Create a copy of the array but not elements.
//There is no need to copy elements since
//AttributeDefinition is "read only".
AttributeDefinition[] res = new AttributeDefinition[src.length];
for (int i = 0; i < src.length; i++)
{
res[i] = src[i];
}
return res;
}
}
/**
* Get direct list of children nodes for current node.
*
* @param nodeId
* Node for which list of nodes is requested.
*
* @return An array of names of child nodes.
*/
public String[] enumerateNodes(String nodeId)
{
synchronized (mutex)
{
DirNode node = locateNode(nodeId, true);
if (node == null)
{
return null;
}
return node.getChildNames();
}
}
/**
* Get definitions for all class attributes.
*
* @param className
* Name of the class.
*
* @return An array of objects describing class attributes.
*/
public AttributeDefinition[] getClassDefinition(String className)
{
ObjectClass objClass = getObjClass(className);
if (objClass == null)
{
return null;
}
return objClass.getClassDefinition();
}
/**
* Get list of names of all defined classes.
*
* @return Array of names of the classes.
*/
public String[] getClassNames()
{
return SchemaStorage.getSchema().getClassNames();
}
/**
* Get <code>BitField</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>BitField</code> then <code>null</code>
* is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public BitField getNodeBitField(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Get <code>BitSelector</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>BitSelector</code> then
* <code>null</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public BitSelector getNodeBitSelector(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Get <code>Boolean</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>Boolean</code> then <code>null</code>
* is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public Boolean getNodeBoolean(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Get <code>byte array</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>byte array</code> then
* <code>null</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public byte[] getNodeByteArray(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Return class name for the specified node.
*
* @param nodeId
* Node for which class name is requested.
*
* @return Node class name.
*/
public String getNodeClassName(String nodeId)
{
DirNode node = locateNode(nodeId, false);
if (node == null)
{
return null;
}
return node.getNodeClass().getName();
}
/**
* Get <code>DateValue</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>DateValue</code> then <code>null</code>
* is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public DateValue getNodeDate(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Get <code>Double</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>Double</code> then <code>null</code>
* is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public Double getNodeDouble(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Get <code>Integer</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>Integer</code> then <code>null</code>
* is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public Integer getNodeInteger(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Get <code>String</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>String</code> then <code>null</code>
* is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public String getNodeString(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Get <code>TimeValue</code> attribute value for specified node ID and
* index. If wrong parameter (node ID, attribute name or index) are passed
* or attribute type is not <code>TimeValue</code> then <code>null</code>
* is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return Value of the attribute for the specified attribute name and
* index.
*/
public TimeValue getNodeTime(String nodeId, String name, int index)
{
return getNodeAttributeValue(nodeId, name, index);
}
/**
* Class is immutable if the back-end does not allow any change to its
* state.
*
* @param className
* Name of the class.
*
* @return <code>true</code> if class is read-only.
*/
public boolean isClassImmutable(String className)
{
ObjectClass objClass = getObjClass(className);
if (objClass == null)
{
return true;
}
return objClass.isClassImmutable();
}
/**
* Class is leaf if no other object can be created under objects of the
* class. Also known as terminal.
*
* @param className
* Name of the class.
*
* @return <code>true</code> if class is leaf.
*/
public boolean isClassLeaf(String className)
{
ObjectClass objClass = getObjClass(className);
if (objClass == null)
{
return false;
}
return objClass.isClassLeaf();
}
/**
* Moves or renames the existing object, specified by nodeId to different
* location. The parent object of the newId should exist and allow for
* children creation.
*
* @param nodeId
* An ID of the node to move.
* @param newId
* The desired new location of the node.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean moveNode(String nodeId, String newId)
{
String finalNodeId = nodeId;
String finalNewId = newId;
Consumer<String> trace = msg -> LOG.fine("Failed to move node from '" + finalNodeId +
"' to '" + finalNewId + "'. ");
DirNode node = locateNode(nodeId, false);
if (node == null)
{
trace.accept("The source node could not be located.");
return false;
}
String[] newPath = IdUtils.splitId(newId);
if (newPath == null)
{
trace.accept("The target node path is invalid.");
return false;
}
DirNode newParent = locateNode(newPath[0], false);
if (newParent == null)
{
trace.accept("Parent does not exist.");
return false;
}
if (newParent.isLeaf())
{
trace.accept("Leaf node is not a valid parent.");
return false;
}
newId = IdUtils.normalize(newId);
if (newParent.hasChild(newPath[1]) && !removed.containsKey(newId))
{
trace.accept("Cannot move to an existing node.");
return false;
}
nodeId = IdUtils.normalize(nodeId);
synchronized (mutex)
{
//Check for existing node
if (changed.get(newId) != null)
{
trace.accept("The target node has pending changes.");
return false;
}
changed.remove(nodeId);
node.setName(newPath[1]);
changed.put(newId, node);
moved.put(newId, nodeId);
removed.put(nodeId, new DirNode(node));
// move all child nodes
List<String> tmp = selectChildNodes(nodeId, changed.keySet());
if (tmp.size() > 1)
{
int sz = nodeId.length();
// now remove nodes with old names and put them back with new names
for (String key : tmp)
{
node = changed.remove(key);
changed.put(newId + key.substring(sz), node);
removed.put(key, new DirNode(node));
}
}
}
return true;
}
/**
* Set <code>BitField</code> attribute value for specified node ID,
* attribute name and index. If wrong parameter is passed or attribute
* read-only then <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeBitField(String nodeId, String name, int index,
BitField value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Set <code>BitSelector</code> attribute value for specified node ID,
* attribute name and index. If wrong parameter is passed or attribute
* read-only then <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeBitSelector(String nodeId, String name, int index,
BitSelector value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Set <code>boolean</code> attribute value for specified node ID,
* attribute name and index. If wrong parameter is passed or attribute
* read-only then <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeBoolean(String nodeId, String name, int index,
boolean value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Set <code>byte array</code> attribute value for specified node ID,
* attribute name and index. If wrong parameter is passed or attribute
* read-only then <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeByteArray(String nodeId, String name, int index,
byte[] value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Set <code>DateValue</code> attribute value for specified node ID,
* attribute name and index. If wrong parameter is passed or attribute
* read-only then <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeDate(String nodeId, String name, int index,
DateValue value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Set <code>double</code> attribute value for specified node ID,
* attribute name and index. If wrong parameter is passed or attribute
* read-only then <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeDouble(String nodeId, String name, int index,
double value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Set <code>int</code> attribute value for specified node ID, attribute
* name and index. If wrong parameter is passed or attribute read-only then
* <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeInteger(String nodeId, String name, int index,
int value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Set <code>String</code> attribute value for specified node ID,
* attribute name and index. If wrong parameter is passed or attribute
* read-only then <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeString(String nodeId, String name, int index,
String value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Set <code>TimeValue</code> attribute value for specified node ID,
* attribute name and index. If wrong parameter is passed or attribute
* read-only then <code>false</code> is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
public boolean setNodeTime(String nodeId, String name, int index,
TimeValue value)
{
return setNodeValue(nodeId, name, index, value);
}
/**
* Close session and shutdown back-end.
*
* @return <code>true</code> if operation was successful.
*/
public boolean unbind()
{
return true;
}
/**
* Update external storage. This method is invoked by the DirectoryService
* to commit the changes made by batch editing into external storage.
*
* @return <code>true</code> if operation was successful.
*/
public boolean update()
{
return true;
}
/**
* Verifies that calling thread is properly bound.
*
* @return <code>true</code> if thread is properly bound.
*/
protected boolean isBound()
{
return true;
}
/**
* Create backup of all affected nodes.
*
* @param baseId
* ID of the base node (root) of the backup.
*
* @return <code>true</code> if operation was successful.
*/
boolean createBackup(String baseId)
{
synchronized (mutex)
{
List<String> newNames = new ArrayList<>();
List<String> newValues = new ArrayList<>();
List<String> changedNames = new ArrayList<>();
List<String> changedValues = new ArrayList<>();
//Go through "changed" list and choose new nodes
for (String nodeId : getChangedList())
{
if (backend.getNodeClassName(nodeId) == null &&
moved.get(nodeId) == null)
{
newNames.add("values");
newValues.add(nodeId);
}
else
{
changedNames.add("values");
changedValues.add(nodeId);
}
}
//List new nodes
boolean rc;
rc = backend.addNode(baseId + "/added", "strings",
toArray(newNames),
toArray(newValues));
if (!rc)
{
return rc;
}
//List deleted nodes
newNames.clear();
newValues.clear();
for (String nodeId : getDeletedList())
{
newNames.add("values");
newValues.add(nodeId);
}
rc = backend.addNode(baseId + "/deleted", "strings",
toArray(newNames),
toArray(newValues));
if (!rc)
{
return rc;
}
//List moved nodes
newNames.clear();
newValues.clear();
for (String nodeId : getMovedList())
{
newNames.add("values");
//<new ID> <old ID>
newValues.add(nodeId + " " + moved.get(nodeId));
}
rc = backend.addNode(baseId + "/moved", "strings",
toArray(newNames),
toArray(newValues));
if (!rc)
{
return rc;
}
//List changed nodes
rc = backend.addNode(baseId + "/changed", "strings",
toArray(changedNames),
toArray(changedValues));
if (!rc)
{
return rc;
}
//Save content of the changed nodes
for (String nodeId : getChangedList())
{
DirNode node = loadNode(nodeId);
if (node == null)
{
if (moved.get(nodeId) == null)
{
// this is a new node, not added nor moved
continue;
}
if (backend.getNodeClassName(moved.get(nodeId)) == null)
{
// this is a new node moved on top of new node, there is
// no backing data in real backend
continue;
}
node = loadNode(moved.get(nodeId));
if (node == null)
{
return false;
}
}
//Create subtree, if necessary
String[] list = IdUtils.listPaths(baseId + "/nodes" + nodeId);
// skip root and nodeId nodes when creating intermediate nodes
for (int j = 2; list != null && j < (list.length - 1); j++)
{
String res = backend.getNodeClassName(list[j]);
if (res == null)
{
rc = backend.addNode(list[j], "container", null, null);
}
//Ignore error here, if something is really wrong
//DirectoryService.addNode() will tell us anyway
}
rc = DirectoryService.addNode(backend,
baseId + "/nodes" + nodeId,
node.getNodeClass().getName(),
node.getAttributes(), true, false);
if (!rc)
{
return rc;
}
}
}
return true;
}
/**
* Perform all steps necessary to safely commit changes into backend.
*
* @param batchId
* ID of the processed subtree.
* @param backupId
* Backup ID.
*
* @return <code>true</code> if operation was successful.
*/
boolean commit(String batchId, String backupId)
{
boolean rc = false;
String lockId = batchId + "/lock";
try
{
// create backup
rc = DirectoryService.copySubtree(backend,
batchId,
backend,
backupId,
true);
if (!rc)
{
return rc;
}
// create lock
rc = backend.addNode(lockId, "lock", null, null);
if (!rc)
{
logWarning(lockId, "Unable to commit batch. Failed to add lock node.");
return rc;
}
rc = backend.addNodeString(lockId, "backupId", backupId);
if (!rc)
{
logWarning(lockId, "Unable to commit batch. Failed to set backupId on " +
"the lock node.");
return rc;
}
rc = backend.addNodeBoolean(lockId, "backed-up", true);
if (!rc)
{
logWarning(lockId, "Unable to commit batch. Failed to set backed-up on " +
"the lock node.");
return rc;
}
// commit backed up tree before applying changes
rc = backend.update();
if (!rc)
{
logWarning(lockId, "Unable to commit batch. Failed to commit external storage.");
return rc;
}
// now we can start applying changes
rc = applyChanges();
if (!rc)
{
logWarning(lockId, "Unable to commit batch. Failed apply changes.");
// applying of changes failed, roll back all changes
rollbackWorker(batchId, backupId);
backend.update();
return rc;
}
rc = backend.deleteNode(lockId);
// note: here we can safely ignore result of next operation
// in worst case we will get orphaned backup
DirectoryService.pruneSubtree(backend, backupId, false);
if (!rc)
{
// This one is really hard to recover from.
// Since node removal is failed, most likely removal of
// backup is failed too. So, all following changes in the
// subtree will be rolled back during next restore.
// From the other hand, failure here does mean that
// backend can't work properly anymore, so, most likely
// all subsequent updates will fail too.
// TODO: perhaps we should introduce special "R/O" mode
// switch for backend in order to prevent further updates
// and saving of (potentially incorrect) content.
logWarning(lockId, "Unable to commit batch. Failed to delete lock node.");
return rc;
}
rc = backend.update();
if (!rc)
{
logWarning(lockId, "Unable to commit batch. Failed to commit external storage.");
// commit failed, roll back all changes
rollbackWorker(batchId, backupId);
return rc;
}
}
finally
{
// ignore errors here
backend.deleteNode(lockId);
DirectoryService.pruneSubtree(backend, backupId, false);
}
return rc;
}
/**
* Roll back the changes made in source directory, using backup subtree as
* an authoritative source of information. If rollback operation is
* successful, then backend is updated.
*
* @param batchId
* ID of the processed subtree.
* @param backupId
* ID of the backup.
*
* @return <code>true</code> if rollback was successful.
*/
boolean rollback(String batchId, String backupId)
{
boolean rc = rollbackWorker(batchId, backupId);
if (!rc)
{
return rc;
}
// note that successful rollback will remove /lock node
// so, cleanup should only remove backup
rc = DirectoryService.pruneSubtree(backend, backupId, false);
if (rc)
{
rc = backend.update();
}
return rc;
}
/**
* Apply changes to main back-end.
*
* @return <code>true</code> if operation was successful.
*/
boolean applyChanges()
{
synchronized (mutex)
{
if (!applyMovedNodes())
{
return false;
}
if (!applyDeletedNodes(getDeletedList()))
{
return false;
}
if (!applyChangedNodes())
{
return false;
}
moved.clear();
changed.clear();
removed.clear();
}
return true;
}
/**
* Rollback worker method.
*
* @param batchId
* Updated subtree.
* @param backupId
* Backup of the subtree.
*
* @return <code>true</code> if operation is successful.
*/
private boolean rollbackWorker(String batchId, String backupId)
{
// check attributes
Attribute[] attr = DirectoryService.getNodeAttributes(backend, backupId);
boolean rc = DirectoryService.setNodeAttributes(backend, batchId, attr);
if (!rc)
{
return rc;
}
// check child nodes
String[] nodes = backend.enumerateNodes(backupId);
if (nodes != null)
{
Set<String> saved = new CaseInsensitiveHashSet<>(nodes.length);
saved.addAll(Arrays.asList(nodes));
Set<String> current = new CaseInsensitiveHashSet<>();
nodes = backend.enumerateNodes(batchId);
if (nodes != null)
{
current.addAll(Arrays.asList(nodes));
}
for(String name : saved)
{
if (current.contains(name))
{
rc = rollbackWorker(batchId + "/" + name, backupId + "/" + name);
current.remove(name);
}
else
{
rc = DirectoryService.copySubtree(backend,
backupId + "/" + name,
backend,
batchId + "/" + name,
true);
}
if (!rc)
{
return rc;
}
}
if (!current.isEmpty())
{
for (String name : current)
{
rc = DirectoryService.pruneSubtree(backend,
batchId + "/" + name,
false);
if (!rc)
{
return rc;
}
}
}
}
return true;
}
/**
* Apply moved nodes to backend.
*
* @return <code>true</code> if operation was successful and
* <code>false</code> otherwise.
*/
private boolean applyMovedNodes()
{
for (String nodeId : getMovedList())
{
List<String> tmp = selectChildNodes(nodeId, getDeletedList());
if (!tmp.isEmpty())
{
// Collection is sorted in ascending order,
// so we must reverse it before use.
Collections.reverse(tmp);
if (!applyDeletedNodes(tmp))
{
return false;
}
removed.keySet().removeAll(tmp);
}
String[] newIds = IdUtils.listPaths(nodeId);
// make sure that all intermediate nodes are present
for (String id : newIds)
{
// do not process last node
if (id.equals(nodeId))
{
break;
}
// Note that destination is already checked and preloaded so
// it must be present in list of changed nodes and we don't need
// to call backend.
if (changed.containsKey(id) && !removed.containsKey(id))
{
if (!addChangedNode(id))
{
return false;
}
// since we performed this operation we don't need to repeat
// it again when we will be applying changed nodes
changed.remove(id);
}
}
// move node at backend only if it really exists, otherwise
// it is present in changed list and will be added later
if (backend.getNodeClassName(moved.get(nodeId)) != null)
{
if (!backend.moveNode(moved.get(nodeId), nodeId))
{
return false;
}
}
}
return true;
}
/**
* Removed specified nodes to backend.
*
* @param forDelete
* Collection of nodes which need to be deleted. Note that
* collection must be in proper order - child nodes must be
* before their parents.
*
* @return <code>true</code> if operation was successful and
* <code>false</code> otherwise.
*/
private boolean applyDeletedNodes(Collection<String> forDelete)
{
for (String nodeId : forDelete)
{
if (backend.getNodeClassName(nodeId) != null)
{
if (!backend.deleteNode(nodeId))
{
logWarning(nodeId, "Unable to delete node.");
return false;
}
}
}
return true;
}
/**
* Apply changed nodes to backend.
*
* @return <code>true</code> if operation was successful and
* <code>false</code> otherwise.
*/
private boolean applyChangedNodes()
{
for (String nodeId : getChangedList())
{
boolean res = addChangedNode(nodeId);
if (!res)
{
return false;
}
}
return true;
}
/**
* Add node with specified ID from list of changed nodes.
*
* @param nodeId
* ID of node to add.
*
* @return <code>true</code> if operation was successful and
* <code>false</code> otherwise.
*/
private boolean addChangedNode(String nodeId)
{
DirNode node = changed.get(nodeId);
return DirectoryService.addNode(backend,
nodeId,
node.getNodeClass().getName(),
node.getAttributes(),
true,
false);
}
/**
* Worker routine for all addNodeXXX() methods.
*
* @param nodeId
* Node ID.
* @param name
* Attribute name.
* @param value
* Attribute value.
*
* @return <code>true</code> if operation was successful.
*/
private boolean addNodeValue(String nodeId, String name, Object value)
{
//Check if node is already in the list
DirNode node = locateNode(nodeId, true);
if (node == null)
{
return false;
}
return node.addAttributeValue(name, value, false);
}
/**
* Return value of specified variable of node attribute.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
*
* @return value of the attribute for the specified attribute name and
* index or <code>null</code>.
*/
@SuppressWarnings("unchecked")
private <T> T getNodeAttributeValue(String nodeId, String name, int index)
{
DirNode node = locateNode(nodeId, false);
if (node == null)
{
return null;
}
Attribute attr = node.getAttribute(name);
if (attr == null)
{
return null;
}
return (T) attr.getValue(index);
}
/**
* Set attribute value for specified node ID, attribute name and index. If
* wrong parameter is passed or attribute read-only then <code>false</code>
* is returned.
*
* @param nodeId
* Node identifier.
* @param name
* Attribute name.
* @param index
* Index of the value to retrieve.
* @param value
* New value for the attribute variable.
*
* @return <code>true</code> is operation is successful and
* <code>false</code> otherwise.
*/
private boolean setNodeValue(String nodeId, String name, int index,
Object value)
{
DirNode node = locateNode(nodeId, true);
if (node == null)
{
return false;
}
return node.setAttributeValue(name, value, index);
}
/**
* Get list of all changed nodes.
*
* @return array of node IDs.
*/
private Collection<String> getChangedList()
{
return new TreeSet<>(changed.keySet());
}
/**
* Get list of all moved nodes.
*
* @return array of node IDs.
*/
private Collection<String> getMovedList()
{
return moved.keySet();
}
/**
* Get list of all deleted nodes.
*
* @return array of node IDs.
*/
private Collection<String> getDeletedList()
{
Set<String> mov = new CaseInsensitiveHashSet<>();
mov.addAll(moved.values());
List<String> res = new ArrayList<>();
for (String key : new TreeSet<>(removed.keySet()))
{
if (!mov.contains(key))
{
// note that nodes added in reverse order
res.add(0, key);
}
}
return res;
}
/**
* Find node in the local list of changed nodes or load it from real
* back-end. If requested, node is stored in the local list of changed
* nodes.
*
* @param nodeId
* Node ID.
* @param preload
* If this parameter is <code>true</code> then loaded node is
* stored in the list of changed nodes.
*
* @return located or loaded node or <code>null</code> if no such node
* exists.
*/
private DirNode locateNode(String nodeId, boolean preload)
{
nodeId = IdUtils.normalize(nodeId);
if (nodeId == null)
{
return null;
}
synchronized (mutex)
{
DirNode node = removed.get(nodeId);
if (node != null) //node was deleted
{
return null;
}
node = changed.get(nodeId);
if (node != null)
{
return node;
}
//retrieve node from the back-end
node = loadNode(nodeId);
if (node != null && preload)
{
changed.put(nodeId, node);
String[] paths = IdUtils.listPaths(nodeId);
for (int i = 0; paths != null && i < (paths.length - 1); i++)
{
if (changed.get(paths[i]) == null)
{
DirNode tmpNode = loadNode(paths[i]);
if (tmpNode == null)
{
throw new RuntimeException("Unexpected failure loading " +
"intermediate node " + paths[i]);
}
changed.put(paths[i], tmpNode);
}
}
}
return node;
}
}
/**
* Return ObjectClass for a given name.
*
* @param nodeClass
* Class name to find.
*
* @return ObjectClass if class with such a name exists and
* <code>null</code> otherwise.
*/
private ObjectClass getObjClass(String nodeClass)
{
return SchemaStorage.getSchema().getObjectClass(nodeClass);
}
/**
* Load node data from real back-end.
*
* @param nodeId
* Node ID.
*
* @return loaded node or <code>null</code> in case of error.
*/
private DirNode loadNode(String nodeId)
{
DirNode node = null;
nodeId = IdUtils.normalize(nodeId);
if (nodeId == null)
{
return null;
}
String nodeClass = backend.getNodeClassName(nodeId);
if (nodeClass == null)
{
return null;
}
String name = "";
if (!nodeId.isEmpty())
{
String[] path = IdUtils.splitId(nodeId);
if (path == null)
{
return null;
}
name = path[1];
}
ObjectClass objClass = getObjClass(nodeClass);
node = new DirNode(objClass, name);
Attribute[] attrs = DirectoryService.getNodeAttributes(backend, nodeId);
for (int i = 0; attrs != null && i < attrs.length; i++)
{
if (!node.setAttribute(attrs[i]))
return null;
}
String[] list = backend.enumerateNodes(nodeId);
for (int i = 0; i < list.length; i++)
{
if (!node.addChild(list[i]))
{
return null;
}
}
return node;
}
/**
* Verify node initialisation data and make sure that such a node can be
* created and it will be valid.
*
* @param nodeName
* Node name.
* @param nodeClass
* Node class.
* @param names
* Array of attribute names.
* @param values
* Array of attribute values.
*
* @return constructed node or <code>null</code> in case of error.
*/
private DirNode validateNode(String nodeName, String nodeClass,
String[] names, Object[] values)
{
ObjectClass objClass = getObjClass(nodeClass);
if (objClass == null)
{
return null;
}
DirNode node = new DirNode(objClass, nodeName);
if (names != null && values != null)
{
if (names.length != values.length)
{
return null;
}
for (int i = 0; i < names.length; i++)
{
boolean rc = node.addAttributeValue(names[i], values[i], true);
if (!rc)
{
return null;
}
}
}
if (!node.isValid())
{
return null;
}
return node;
}
/**
* Convenience method which produces array of strings from any given string
* collection.
*
* @param coll
* Source collection.
*
* @return Array of strings.
*/
private static String[] toArray(Collection<String> coll)
{
return coll.toArray(new String[coll.size()]);
}
/**
* Select nodes which start from a given path.
*
* @param baseId
* Base node ID.
*
* @return collection of selected nodes.
*/
private static List<String> selectChildNodes(String baseId,
Collection<String> source)
{
Set<String> ids = new TreeSet<>(source);
List<String> tmp = new ArrayList<>();
Iterator<String> iter = ids.iterator();
String id = null;
// skip to first id
while(iter.hasNext())
{
id = iter.next();
if (id.startsWith(baseId))
{
break;
}
id = null;
}
// create copy of moved nodes
while (id != null)
{
tmp.add(id);
if (iter.hasNext())
{
id = iter.next();
if (!id.startsWith(baseId))
{
id = null;
}
}
else
{
id = null;
}
}
return tmp;
}
}