/* See LICENSE for licensing and NOTICE for copyright. */ package org.ldaptive.io; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; 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 org.ldaptive.LdapAttribute; import org.ldaptive.LdapEntry; import org.ldaptive.SearchResult; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * Writes a {@link SearchResult} as DSML version 1 to a {@link Writer}. * * @author Middleware Services */ public class Dsmlv1Writer implements SearchResultWriter { /** Document builder factory. */ private static final DocumentBuilderFactory DOC_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); /** Transformer factory. */ private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance(); /** * Initialize the document builder factory. */ static { DOC_BUILDER_FACTORY.setNamespaceAware(true); } /** Writer to write to. */ private final Writer dsmlWriter; /** Transformer output properties. See {@link Transformer#setOutputProperty(String, String)}. */ private Map<String, String> outputProperties = new HashMap<>(); /** * Creates a new dsml writer. The following transformer output properties are set by default: * * <ul> * <li>"doctype-public", "yes"</li> * <li>"indent", "yes"</li> * <li>"{http://xml.apache.org/xslt}indent-amount", "2"</li> * </ul> * * @param writer to write DSML to */ public Dsmlv1Writer(final Writer writer) { dsmlWriter = writer; outputProperties.put(OutputKeys.DOCTYPE_PUBLIC, "yes"); outputProperties.put(OutputKeys.INDENT, "yes"); outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2"); } /** * Returns the transformer output properties used by this writer. * * @return transformer output properties */ public Map<String, String> getOutputProperties() { return outputProperties; } /** * Sets the transformer output properties used by this writer. * * @param properties transformer output properties */ public void setOutputProperties(final Map<String, String> properties) { outputProperties = properties; } /** * Writes the supplied search result to the writer. * * @param result search result to write * * @throws IOException if an error occurs using the writer */ @Override public void write(final SearchResult result) throws IOException { try { final Transformer transformer = TRANSFORMER_FACTORY.newTransformer(); for (Map.Entry<String, String> prop : outputProperties.entrySet()) { transformer.setOutputProperty(prop.getKey(), prop.getValue()); } final StreamResult sr = new StreamResult(dsmlWriter); final DOMSource source = new DOMSource(createDsml(result)); transformer.transform(source, sr); dsmlWriter.flush(); } catch (ParserConfigurationException | TransformerException e) { throw new IOException(e); } } /** * Creates DSML that corresponds to the supplied search result. * * @param result search result to parse * * @return DSML * * @throws ParserConfigurationException if a document builder cannot be created */ protected Document createDsml(final SearchResult result) throws ParserConfigurationException { final DocumentBuilder db = DOC_BUILDER_FACTORY.newDocumentBuilder(); final DOMImplementation domImpl = db.getDOMImplementation(); final Document doc = domImpl.createDocument("http://www.dsml.org/DSML", "dsml:dsml", null); doc.setXmlStandalone(true); final Element entriesElement = doc.createElement("dsml:directory-entries"); doc.getDocumentElement().appendChild(entriesElement); // build document object from result if (result != null) { for (LdapEntry le : result.getEntries()) { final Element entryElement = doc.createElement("dsml:entry"); if (le.getDn() != null) { entryElement.setAttribute("dn", le.getDn()); } createDsmlAttributes(doc, le.getAttributes()).forEach(entryElement::appendChild); entriesElement.appendChild(entryElement); } } return doc; } /** * Returns a list of <dsml:attr/> elements for the supplied attributes. * * @param doc to source elements from * @param attrs to iterate over * * @return list of elements contains attributes */ protected List<Element> createDsmlAttributes(final Document doc, final Collection<LdapAttribute> attrs) { final List<Element> attrElements = new ArrayList<>(); for (LdapAttribute attr : attrs) { final String attrName = attr.getName(); Element attrElement; if ("objectclass".equalsIgnoreCase(attrName)) { attrElement = createObjectclassElement(doc, attr); if (attrElement.hasChildNodes()) { attrElements.add(0, attrElement); } } else { attrElement = createAttrElement(doc, attr); if (attrElement.hasChildNodes()) { attrElements.add(attrElement); } } } return attrElements; } /** * Returns a <dsml:attr/> element for the supplied ldap attribute. * * @param doc to source elements from * @param attr ldap attribute to add * * @return element containing the attribute */ protected Element createAttrElement(final Document doc, final LdapAttribute attr) { final Element attrElement = doc.createElement("dsml:attr"); attrElement.setAttribute("name", attr.getName()); for (String s : attr.getStringValues()) { final Element valueElement = doc.createElement("dsml:value"); attrElement.appendChild(valueElement); setAttrValue(doc, valueElement, s, attr.isBinary()); } return attrElement; } /** * Returns a <dsml:objectclass/> element for the supplied ldap attribute. * * @param doc to source elements from * @param attr ldap attribute to add * * @return element containing the attribute values */ protected Element createObjectclassElement(final Document doc, final LdapAttribute attr) { final Element ocElement = doc.createElement("dsml:objectclass"); for (String s : attr.getStringValues()) { final Element ocValueElement = doc.createElement("dsml:oc-value"); ocElement.appendChild(ocValueElement); setAttrValue(doc, ocValueElement, s, attr.isBinary()); } return ocElement; } /** * Adds the supplied string to the value element. * * @param doc to create nodes with * @param valueElement to append value to * @param value to create node for * @param isBase64 whether the value is base64 encoded */ protected void setAttrValue( final Document doc, final Element valueElement, final String value, final boolean isBase64) { if (value != null) { valueElement.appendChild(doc.createTextNode(value)); if (isBase64) { valueElement.setAttribute("encoding", "base64"); } } } }