/*
*
* Copyright (c) 2010-2012 ForgeRock Inc. All Rights Reserved
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://www.opensource.org/licenses/cddl1.php or
* OpenIDM/legal/CDDLv1.0.txt
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at OpenIDM/legal/CDDLv1.0.txt.
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted 2010 [name of copyright owner]"
*
* $Id$
*/
package org.forgerock.openicf.connectors.xml;
import org.forgerock.openicf.connectors.xml.util.AttributeTypeUtil;
import org.forgerock.openicf.connectors.xml.util.ElementIdentifierFieldType;
import org.forgerock.openicf.connectors.xml.util.GuardedStringAccessor;
import org.forgerock.openicf.connectors.xml.util.NamespaceLookupUtil;
import org.forgerock.openicf.connectors.xml.util.XmlHandlerUtil;
import org.forgerock.openicf.connectors.xml.query.abstracts.Query;
import org.forgerock.openicf.connectors.xml.query.QueryBuilder;
import org.forgerock.openicf.connectors.xml.query.XQueryHandler;
import com.sun.xml.xsom.XSSchema;
import com.sun.xml.xsom.XSSchemaSet;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xquery.XQException;
import javax.xml.xquery.XQResultSequence;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.AlreadyExistsException;
import org.identityconnectors.framework.common.exceptions.ConfigurationException;
import org.identityconnectors.framework.common.exceptions.UnknownUidException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeInfo;
import org.identityconnectors.framework.common.objects.AttributeUtil;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.common.objects.Name;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.ObjectClassInfo;
import org.identityconnectors.framework.common.objects.Schema;
import org.identityconnectors.common.Assertions;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.AttributeInfoUtil;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
public class XMLHandlerImpl implements XMLHandler {
/**
* Setup logging for the {@link XMLHandlerImpl}.
*/
private static final Log log = Log.getLog(XMLHandlerImpl.class);
private XMLConfiguration config;
private volatile Document document;
private Schema connSchema;
private XSSchema icfSchema;
private XSSchema riSchema;
private long lastModified = 0l;
private volatile long version = 0l;
public static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance";
public static final String ICF_NAMESPACE_PREFIX = "icf";
public static final String RI_NAMESPACE_PREFIX = "ri";
public static final String XSI_NAMESPACE_PREFIX = "xsi";
public static final String ICF_CONTAINER_TAG = "OpenICFContainer";
public XMLHandlerImpl(XMLConfiguration config, Schema connSchema, XSSchemaSet xsdSchemas) {
Assertions.nullCheck(config.getXmlFilePath(), "filePath");
this.config = config;
this.connSchema = connSchema;
this.riSchema = xsdSchemas.getSchema(1);
this.icfSchema = xsdSchemas.getSchema(2);
NamespaceLookupUtil.INSTANCE.initialize(icfSchema, riSchema);
}
public XMLHandler init() {
buildDocument();
return this;
}
public Uid create(final ObjectClass objClass, final Set<Attribute> attributes) {
final String method = "create";
log.info("Entry {0}", method);
// Validate object type
XmlHandlerUtil.checkObjectType(objClass, riSchema);
ObjectClassInfo objInfo = connSchema.findObjectClassInfo(objClass.getObjectClassValue());
Set<AttributeInfo> objAttributes = null;
Map<String, AttributeInfo> supportedAttributeInfoMap = null;
Map<String, Attribute> providedAttributesMap = null;
String uidValue = null;
if (attributes != null) {
objAttributes = objInfo.getAttributeInfo();
supportedAttributeInfoMap = new HashMap<String, AttributeInfo>(AttributeInfoUtil.toMap(objAttributes));
providedAttributesMap = new HashMap<String, Attribute>(AttributeUtil.toMap(attributes));
}
// Check if __NAME__ is defined
if (providedAttributesMap == null || !providedAttributesMap.containsKey(Name.NAME) || providedAttributesMap.get(Name.NAME).getValue().isEmpty()) {
throw new IllegalArgumentException(Name.NAME + " must be defined.");
}
Name name = AttributeUtil.getNameFromAttributes(attributes);
// Check if entry already exists
if (entryExists(objClass, new Uid(name.getNameValue()), ElementIdentifierFieldType.BY_NAME)) {
throw new AlreadyExistsException("Could not create entry. An entry with the " + Uid.NAME + " of "
+ name.getNameValue() + " already exists.");
}
// Create or get UID
if (supportedAttributeInfoMap.containsKey(Uid.NAME)) {
uidValue = UUID.randomUUID().toString();
} else {
uidValue = name.getNameValue();
}
// Create object type element
Element objElement = getDocument().createElementNS(riSchema.getTargetNamespace(), objClass.getObjectClassValue());
objElement.setPrefix(RI_NAMESPACE_PREFIX);
// Add child elements
for (AttributeInfo attributeInfo : objAttributes) {
String attributeName = attributeInfo.getName();
List<String> values = AttributeTypeUtil.findAttributeValue(providedAttributesMap.get(attributeName), attributeInfo);
// Check if required attributes contain values
if (attributeInfo.isRequired()) {
if (providedAttributesMap.containsKey(attributeName) && !values.isEmpty()) {
for (String value : values) {
Assertions.blankCheck(value, attributeName);
}
} else {
throw new IllegalArgumentException("Missing required field: " + attributeName);
}
}
if (!attributeInfo.isMultiValued() && values.size() > 1) {
throw new IllegalArgumentException("Attribute field: " + attributeName + " is not multivalued and can not contain more than one value");
}
if (!supportedAttributeInfoMap.containsKey(attributeName)) {
continue;
}
if (!attributeInfo.isCreateable() && providedAttributesMap.containsKey(attributeName)) {
throw new IllegalArgumentException(attributeName + " is not a creatable field.");
}
Element childElement = null;
if (attributeName.equals(Uid.NAME)) {
childElement = createDomElement(attributeName, uidValue);
objElement.appendChild(childElement);
} else if (providedAttributesMap.containsKey(attributeName)) {
// Check if provided value is instance of the class defined in schema
Class expectedClass = attributeInfo.getType();
if (!valuesAreExpectedClass(expectedClass, providedAttributesMap.get(attributeName).getValue())) {
throw new IllegalArgumentException(attributeName + " contains values of illegal type");
}
// Create elements
for (String value : values) {
childElement = createDomElement(attributeName, value);
objElement.appendChild(childElement);
}
} // Create empty element if not provided
else {
childElement = createDomElement(attributeName, "");
objElement.appendChild(childElement);
}
log.info("Creating new entry: {0}", attributes.toString());
}
getDocument().getDocumentElement().appendChild(objElement);
log.info("Exit {0}", method);
return new Uid(uidValue);
}
public Uid update(ObjectClass objClass, Uid uid, Set<Attribute> replaceAttributes) {
final String method = "update";
log.info("Entry {0}", method);
XmlHandlerUtil.checkObjectType(objClass, riSchema);
ObjectClassInfo objInfo = connSchema.findObjectClassInfo(objClass.getObjectClassValue());
Map<String, AttributeInfo> objAttributes = AttributeInfoUtil.toMap(objInfo.getAttributeInfo());
if (entryExists(objClass, uid, ElementIdentifierFieldType.AUTO)) {
Element entry = getEntry(objClass, uid, ElementIdentifierFieldType.AUTO);
for (Attribute attribute : replaceAttributes) {
if (!objAttributes.containsKey(attribute.getName())) {
throw new IllegalArgumentException("Data field: " + attribute.getName() + " is not supported.");
}
AttributeInfo attributeInfo = objAttributes.get(attribute.getName());
String attributeName = attribute.getName();
if (!attributeInfo.isUpdateable()) {
throw new IllegalArgumentException(attributeName + " is not updatable.");
}
if (attributeInfo.isRequired()) {
List<String> values = AttributeTypeUtil.findAttributeValue(attribute, attributeInfo);
if (values.isEmpty()) {
throw new IllegalArgumentException("No values provided for required attribute: " + attributeName);
}
for (String value : values) {
Assertions.blankCheck(value, attributeName);
Assertions.nullCheck(value, attributeName);
}
}
// Check if the provided value is the same as the class defined in schema
Class expectedClass = attributeInfo.getType();
if (attribute.getValue() != null) {
if (!valuesAreExpectedClass(expectedClass, attribute.getValue())) {
throw new IllegalArgumentException(attributeName + " contains values of illegal type");
}
}
// Remove existing nodes from entry
removeChildrenFromElement(entry, prefixAttributeName(attributeName));
// Add updated nodes to entry
List<String> values = AttributeTypeUtil.findAttributeValue(attribute, attributeInfo);
if (!attributeInfo.isMultiValued() && values.size() > 1) {
throw new IllegalArgumentException("Data field: " + attributeName + " is not multivalued can not have more than one value");
}
// Append empty element if no values is provided
if (values.isEmpty()) {
Element updatedElement = createDomElement(attributeName, "");
entry.appendChild(updatedElement);
} else {
for (String value : values) {
Element updatedElement = createDomElement(attributeName, value);
entry.appendChild(updatedElement);
}
}
}
} else {
throw new UnknownUidException("Could not update entry. No entry of type " + objClass.getObjectClassValue() + " with the id " + uid.getUidValue() + " found.");
}
log.info("Exit {0}", method);
return uid;
}
public void delete(final ObjectClass objClass, final Uid uid) {
final String method = "delete";
log.info("Entry {0}", method);
XmlHandlerUtil.checkObjectType(objClass, riSchema);
if (entryExists(objClass, uid, ElementIdentifierFieldType.AUTO)) {
Element elementToRemove = getEntry(objClass, uid, ElementIdentifierFieldType.AUTO);
getDocument().getDocumentElement().removeChild(elementToRemove);
log.info("Deleting entry: " + elementToRemove.toString());
} else {
throw new UnknownUidException("Deleting entry failed. Could not find an entry of type " + objClass.getObjectClassValue() + " with the uid " + uid.getUidValue());
}
log.info("Exit {0}", method);
}
public Collection<ConnectorObject> search(String query, ObjectClass objClass) {
final String method = "search";
log.info("Entry {0}", method);
List<ConnectorObject> results = new ArrayList<ConnectorObject>();
if (query != null && !query.isEmpty() && objClass != null) {
ObjectClassInfo objInfo = connSchema.findObjectClassInfo(objClass.getObjectClassValue());
Set<AttributeInfo> objAttributes = objInfo.getAttributeInfo();
// Map with the attribute-names and what class they are
HashMap<String, String> attributeClassMap = new HashMap<String, String>();
for (AttributeInfo info : objAttributes) {
attributeClassMap.put(info.getName(), info.getType().getSimpleName());
}
// Map with the AttributeInfo for each attribute
HashMap<String, AttributeInfo> attributeInfoMap =
new HashMap<String, AttributeInfo>(AttributeInfoUtil.toMap(objInfo.getAttributeInfo()));
XQueryHandler xqHandler = null;
try {
xqHandler = new XQueryHandler(query, getDocument());
XQResultSequence queryResult = xqHandler.getResultSequence();
ConnectorObjectCreator conObjCreator =
new ConnectorObjectCreator(attributeClassMap, attributeInfoMap, objClass);
while (queryResult.next()) {
Node resultNode = queryResult.getItem().getNode();
NodeList nodes = resultNode.getChildNodes();
ConnectorObject conObj = conObjCreator.createConnectorObject(nodes);
results.add(conObj);
}
} catch (XQException ex) {
log.error("Error while searching: {0}", ex);
throw new ConnectorException(ex);
} finally {
if (null != xqHandler) {
xqHandler.close();
}
}
}
log.info("Exit {0}", method);
return results;
}
private boolean isExternallyModified() {
boolean modified = false;
if (config.getXmlFilePath().exists()) {
modified = lastModified != config.getXmlFilePath().lastModified();
}
return modified;
}
public void dispose() {
final String method = "serialize";
log.info("Entry {0}", method);
if (version != lastModified && isExternallyModified()) {
log.error("UPDATE COLLUSION: File has been modified after read into memory and the data in memory has not been synced before.");
}
try {
try {
XPathFactory xpathFactory = new net.sf.saxon.xpath.XPathFactoryImpl();
// XPath to find empty text nodes.
XPathExpression xpathExp = xpathFactory.newXPath().compile("//text()[normalize-space(.) = '']");
NodeList emptyTextNodes = null;
emptyTextNodes = (NodeList) xpathExp.evaluate(document, XPathConstants.NODESET);
// Remove each empty text node from document.
for (int i = 0; i < emptyTextNodes.getLength(); i++) {
Node emptyTextNode = emptyTextNodes.item(i);
emptyTextNode.getParentNode().removeChild(emptyTextNode);
}
} catch (XPathExpressionException e) {
//We don't care. It's just formatting.
}
TransformerFactory transformerFactory = new net.sf.saxon.TransformerFactoryImpl();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
DOMSource source = new DOMSource(document);
/* Running this code in java 5 we had to change
StreamResult result = new StreamResult(config.getXmlFilePath());
into
StreamResult result = new StreamResult(config.getXmlFilePath().getPath());
Otherwise you get the following error:
javax.xml.transform.TransformerException: java.io.FileNotFoundException:
*/
/*
* If the safePath is not escaped then it throws
* net.sf.saxon.trans.XPathException: java.net.URISyntaxException:
* Illegal character in safePath at index 9: /temp/XML Connector/test.xml
* String safePath = config.getXmlFilePath().getPath().replaceAll(" ", "%20");
*/
FileOutputStream fos = new FileOutputStream(config.getXmlFilePath());
StreamResult result = new StreamResult(fos);
transformer.transform(source, result);
log.info("Saving changes to xml file");
} catch (TransformerException ex) {
log.error("Failed saving changes to xml file: {0}", ex);
throw ConnectorException.wrap(ex);
} catch (FileNotFoundException ex) {
log.error("Failed saving changes to xml file: {0}", ex);
throw ConnectorException.wrap(ex);
}
log.info("Entry {0}", method);
}
public Uid authenticate(String username, GuardedString password) {
final String method = "authenticate";
log.info("Entry {0}", method);
Uid uid = null;
Element entry = getEntry(ObjectClass.ACCOUNT, new Uid(username), ElementIdentifierFieldType.BY_NAME);
if (entry != null) {
NodeList passwordElements = entry.getElementsByTagName(ICF_NAMESPACE_PREFIX + ":__PASSWORD__");
String xmlPassword = passwordElements.item(0).getTextContent();
GuardedStringAccessor accessor = new GuardedStringAccessor();
password.access(accessor);
StringBuilder sb = new StringBuilder();
sb.append(accessor.getArray());
String userPassword = sb.toString();
if (xmlPassword.equals(userPassword)) {
NodeList uidElements = entry.getElementsByTagName(ICF_NAMESPACE_PREFIX + ":" + Uid.NAME);
if (uidElements.getLength() >= 1) {
uid = new Uid(uidElements.item(0).getTextContent());
} else {
NodeList nameElements = entry.getElementsByTagName(ICF_NAMESPACE_PREFIX + ":" + Name.NAME);
uid = new Uid(nameElements.item(0).getTextContent());
}
}
}
log.info("Exit {0}", method);
return uid;
}
public boolean isSupportUid(ObjectClass objectClass) {
ObjectClassInfo objInfo = connSchema.findObjectClassInfo(objectClass.getObjectClassValue());
if (null != objInfo) {
for (AttributeInfo info : objInfo.getAttributeInfo()) {
if (info.is(Uid.NAME)) {
return true;
}
}
}
return false;
}
private void createDocument() {
final String method = "createDocument";
log.info("Entry {0}", method);
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
DocumentBuilder builder = null;
try {
builder = builderFactory.newDocumentBuilder();
log.info("Creating new xml storage file: {0}", config.getXmlFilePath());
} catch (ParserConfigurationException ex) {
log.error("Filed creating XML document: {0}", ex);
throw ConnectorException.wrap(ex);
}
DOMImplementation implementation = builder.getDOMImplementation();
document = implementation.createDocument(icfSchema.getTargetNamespace(), ICF_CONTAINER_TAG, null);
Element root = document.getDocumentElement();
root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:" + XSI_NAMESPACE_PREFIX, XSI_NAMESPACE);
root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:" + RI_NAMESPACE_PREFIX, riSchema.getTargetNamespace());
root.setPrefix(ICF_NAMESPACE_PREFIX);
if (config.getXsdIcfFilePath() == null) {
root.setAttribute(XSI_NAMESPACE_PREFIX + ":schemaLocation",
riSchema.getTargetNamespace() + " " + config.getXsdFilePath());
} else {
root.setAttribute(XSI_NAMESPACE_PREFIX + ":schemaLocation",
riSchema.getTargetNamespace() + " " + config.getXsdFilePath() + " "
+ icfSchema.getTargetNamespace() + " " + config.getXsdIcfFilePath());
}
log.info("Exit {0}", method);
}
private void loadDocument(File xmlFile) {
final String method = "loadDocument";
log.info("Entry {0}", method);
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setNamespaceAware(true);
DocumentBuilder docBuilder;
try {
docBuilder = docBuilderFactory.newDocumentBuilder();
document = docBuilder.parse(xmlFile);
lastModified = xmlFile.lastModified();
version = lastModified;
log.info("Loading XML document from: {0}", xmlFile.getPath());
} catch (ParserConfigurationException ex) {
throw ConnectorException.wrap(ex);
} catch (SAXException ex) {
throw ConnectorException.wrap(ex);
} catch (IOException ex) {
throw ConnectorException.wrap(ex);
}
log.info("Exit {0}", method);
}
private void buildDocument() {
final String method = "buildDocument";
log.info("Entry {0}", method);
File xmlFile = config.getXmlFilePath();
if (!xmlFile.exists()) {
if (config.isCreateFileIfNotExists()) {
createDocument();
} else {
throw new ConfigurationException("Data file does not exists: " + config.getXmlFilePath().toString());
}
} else {
loadDocument(xmlFile);
}
log.info("Exit {0}", method);
}
private Element createDomElement(String elementName, String value) {
Element element = null;
if (icfSchema.getElementDecls().containsKey(elementName)) {
element = getDocument().createElementNS(icfSchema.getTargetNamespace(), elementName);
element.setPrefix(ICF_NAMESPACE_PREFIX);
} else {
element = getDocument().createElementNS(riSchema.getTargetNamespace(), elementName);
element.setPrefix(RI_NAMESPACE_PREFIX);
}
element.setTextContent(value);
return element;
}
private Element getEntry(ObjectClass objClass, Uid uid, ElementIdentifierFieldType identifierField) {
final String method = "getEntry";
log.info("Entry {0}", method);
Element result = null;
// Build search query
XMLFilterTranslator translator = new XMLFilterTranslator(isSupportUid(objClass));
String idField = getElementIdentifierField(objClass, identifierField);
AttributeBuilder builder = new AttributeBuilder();
builder.setName(idField);
builder.addValue(uid.getUidValue());
EqualsFilter equals = new EqualsFilter(builder.build());
Query query = translator.createEqualsExpression(equals, false);
QueryBuilder queryBuilder = new QueryBuilder(query, objClass);
// Execute query
XQueryHandler xqHandler = null;
try {
xqHandler = new XQueryHandler(queryBuilder.toString(), document);
XQResultSequence results = xqHandler.getResultSequence();
if (results.next()) {
result = (Element) results.getItem().getNode();
log.info("Entry found: ", result.toString());
}
} catch (XQException ex) {
throw ConnectorException.wrap(ex);
} finally {
if (null != xqHandler) {
xqHandler.close();
}
}
log.info("Exit {0}", method);
return result;
}
private boolean entryExists(ObjectClass objClass, Uid uid, ElementIdentifierFieldType identifierField) {
if (getEntry(objClass, uid, identifierField) != null) {
return true;
}
return false;
}
private String getElementIdentifierField(ObjectClass objClass, ElementIdentifierFieldType identifierField) {
String elementField = "";
if (identifierField == ElementIdentifierFieldType.BY_NAME) {
elementField = Name.NAME;
} else if (identifierField == ElementIdentifierFieldType.BY_UID) {
elementField = Uid.NAME;
} else {
ObjectClassInfo objInfo = connSchema.findObjectClassInfo(objClass.getObjectClassValue());
Set<AttributeInfo> objAttrSet = objInfo.getAttributeInfo();
Map<String, AttributeInfo> attrInfoMap = AttributeInfoUtil.toMap(objAttrSet);
if (attrInfoMap.containsKey(Uid.NAME)) {
elementField = Uid.NAME;
} else {
elementField = Name.NAME;
}
}
return elementField;
}
private boolean valuesAreExpectedClass(Class expectedClass, List<Object> values) {
if (null != values) {
if (expectedClass.isPrimitive()) {
expectedClass = AttributeTypeUtil.convertPrimitiveToWrapper(expectedClass.getName());
}
for (Object obj : values) {
if (expectedClass != obj.getClass()) {
return false;
}
}
}
return true;
}
private void removeChildrenFromElement(Element element, String childName) {
NodeList oldNodes = element.getElementsByTagName(childName);
List<Element> elementsToRemove = new ArrayList<Element>();
for (int i = 0; i < oldNodes.getLength(); i++) {
elementsToRemove.add((Element) oldNodes.item(i));
}
for (Element e : elementsToRemove) {
element.removeChild(e);
}
}
private String prefixAttributeName(String attrName) {
String prefix = "";
if (icfSchema.getElementDecls().containsKey(attrName)) {
prefix = ICF_NAMESPACE_PREFIX + ":" + attrName;
} else {
prefix = RI_NAMESPACE_PREFIX + ":" + attrName;
}
return prefix;
}
private Document getDocument() {
if (null == document) {
throw new ConnectorException("Data file does not exists: " + config.getXmlFilePath().toString());
}
return document;
}
}