package ring.deployer;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Resource;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XMLResource;
import ring.persistence.ExistDB;
import ring.persistence.ExistDBStore;
import ring.persistence.Loadpoint;
import ring.persistence.ResourceList;
import ring.persistence.XQuery;
import ring.util.UserUtilities;
/**
* Encapsulates the logic of deploying a given XML document.
* @author projectmoon
*
*/
public class XMLDeployer {
private ExistDB db;
private DeployableFileEntry entry;
private String documentName;
private String xmlInDocument; //for the imported doc
private static boolean xmlUpdates = false;
public XMLDeployer(ExistDB db, DeployableFileEntry entry) {
this.db = db;
this.entry = entry;
documentName = entry.getStrippedEntryName();
}
public boolean xmlUpdates() {
return xmlUpdates;
}
public void deploy() throws XMLDBException, SAXException, IOException {
Collection col = db.getCollection(ExistDBStore.STATIC_COLLECTION);
col.setProperty(OutputKeys.INDENT, "no");
//Determine if this document needs to be updated:
//Retrieve the document content of the same name from the database.
//Retrieve the document content of the incoming document.
//if shaHash(incomingDocument) == shaHash(documentInDB):
// reutrn. the document does not need to be updated.
xmlInDocument = stripWhitespaceAndHeader(entry.getInputStream());
XMLResource resource = (XMLResource)col.getResource(documentName);
if (resource != null) {
String xmlInDB = (String)resource.getContent(); //This is already stripped of its header/whitespace
String hash1 = UserUtilities.sha1Hash(xmlInDB);
String hash2 = UserUtilities.sha1Hash(xmlInDocument);
if (hash1.equals(hash2)) {
col.close();
//System.out.println("This document does not need to be updated.");
return;
}
else {
System.out.println("Updating XML: " + documentName);
}
}
else {
System.out.println("Adding new XML: " + documentName);
}
col.close();
//Now, we have determined that the document needs to be updated, or imported.
xmlUpdates = true;
//Next, we handle ID clashes.
//COMMENTED OUT because clashing is handled by removing of existing documents, and removal of deleted documents.
//handleClashes();
//If a document with this name already exists in the DB:
// delete it.
deleteExistingDocument();
//Finally, import this document.
importDocument();
}
/**
* Handles clashses. Currently not necessary.
* @throws SAXException
* @throws IOException
* @throws XMLDBException
*/
@SuppressWarnings("unused")
private void handleClashes() throws SAXException, IOException, XMLDBException {
//First, all clashes need to be handled.
//For each ID found in this XML document:
// Atempt to retrieve an object from the database with this ID.
// If object returned is not null:
// boolean yes = idHasOwnDocument(id)
// if yes to ID has its own document:
// Delete that document.
// else:
// Replace definition for ID with a referential definition.
//Find all IDs in the document.
IDFinder finder = new IDFinder();
XMLReader parser = XMLReaderFactory.createXMLReader();
parser.setContentHandler(finder);
InputStream input = entry.getInputStream();
InputSource src = new InputSource(new BufferedInputStream(input));
parser.parse(src);
//For each found ID, attempt to retrieve an object from the database.
//If an object is found, that means we have a clash.
//We need to handle each clash individually.
XQuery xq = new XQuery();
xq.setLoadpoint(Loadpoint.STATIC);
for (String id : finder.getIDs()) {
String query = "for $doc in //ring//*[@id=\"" + id + "\"] return $doc";
xq.setQuery(query);
ResourceList set = xq.execute();
//If size > 0, we have a clash.
if (set.size() > 0) {
//There should be one resource.
//All IDs are unique per collection. This is enforced by compiler.
assert (set.size() == 1);
XMLResource res = (XMLResource)set.nextResource();
handleClashForResource(id, res);
}
set.close();
}
}
/**
* Handles a clash for a given resource found in the DB. This clash is handled in one of two ways:
* if the resource is determined to be its own document as per the idHasOwnDocument method, then it
* is outright deleted. If the resource is determined to be embedded in a larger document, it
* is instead replaced with a referential definition. This ensures that if an object definition is
* refactored out of an xml file it is embedded into, that existing data will be updated.
* @param id
* @param res
* @throws XMLDBException
*/
private void handleClashForResource(String id, XMLResource res) throws XMLDBException {
System.out.println("handling clash for: " + id);
//Determine if this resource is its own document.
//If yes, delete that document.
//Else, replace with a referential definition.
if (idHasOwnDocument(id, res)) {
System.out.println("has own document");
//eXist cannot figure out how to find the parent collection of these fragments properly.
//So, we have to do it slightly differently.
Collection parent = db.getCollection(ExistDBStore.STATIC_COLLECTION);
Resource removeIt = parent.getResource(res.getDocumentId());
if (removeIt != null) {
parent.removeResource(removeIt);
}
parent.close();
}
else {
String delete = "for $doc in //ring//*[@id=\"" + id + "\"] return (update delete $doc/*)";
String update = "for $doc in //ring//*[@id=\"" + id + "\"] return update value $doc/@ref with \"true\"";
XQuery xq = new XQuery();
xq.setQuery(delete);
xq.executeUpdate();
xq.setQuery(update);
xq.executeUpdate();
}
}
private boolean idHasOwnDocument(String id, XMLResource res) throws XMLDBException {
//In order for an ID to be considered to have its own document, the following must be true:
//1. The document has only one element within the ring element. This element may have any # of subelements.
//2. The element must have an id that matches the id passed to this method.
Collection parent = db.getCollection(ExistDBStore.STATIC_COLLECTION);
XMLResource actualDocument = (XMLResource)parent.getResource(res.getDocumentId());
boolean hasOwnDocument = true;
Element ringRoot = (Element)actualDocument.getContentAsDOM().getChildNodes().item(0);
NodeList children = ringRoot.getChildNodes();
int elements = 0;
int elementIndex = 0; //This is used if if we meet requirement #1.
for (int c = 0; c < children.getLength(); c++) {
if (children.item(c).getNodeType() == Node.ELEMENT_NODE) {
elements++;
elementIndex = c;
}
}
//Fulfills requirement #1.
if (elements != 1) {
hasOwnDocument = false;
}
else {
//Fulfills requirement #1.
Element e = (Element)children.item(elementIndex);
if (e.getAttribute("id").equalsIgnoreCase(id) == false) {
System.out.println("Doesn't meet requirement #2");
hasOwnDocument = false;
}
}
parent.close();
//If one of these conditions is not met, it is considered embedded in another document.
return hasOwnDocument;
}
private void deleteExistingDocument() throws XMLDBException {
Collection col = db.getCollection(ExistDBStore.STATIC_COLLECTION);
XMLResource res = (XMLResource)col.getResource(documentName);
if (res != null) {
col.removeResource(res);
}
col.close();
}
private void importDocument() throws XMLDBException {
Collection col = db.getCollection(ExistDBStore.STATIC_COLLECTION);
XMLResource res = (XMLResource)col.createResource(documentName, XMLResource.RESOURCE_TYPE);
res.setContent(xmlInDocument);
col.storeResource(res);
col.close();
}
private String stripWhitespaceAndHeader(InputStream xml) {
try {
TransformerFactory tf = TransformerFactory.newInstance();
InputStream input = this.getClass().getClassLoader().getResourceAsStream("ring/deployer/strip-space.xsl");
Transformer transformer = tf.newTransformer(new StreamSource(input));
StreamSource src = new StreamSource(xml);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
StreamResult result = new StreamResult(stream);
transformer.transform(src, result);
return new String(stream.toByteArray());
}
catch (TransformerConfigurationException e) {
e.printStackTrace();
return null;
}
catch (TransformerException e) {
e.printStackTrace();
return null;
}
}
}