package org.exist.xquery.modules.file; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Properties; import javax.xml.transform.OutputKeys; import org.exist.collections.Collection; import org.exist.dom.BinaryDocument; import org.exist.dom.DocumentImpl; import org.exist.dom.QName; import org.exist.memtree.MemTreeBuilder; import org.exist.storage.lock.Lock; import org.exist.storage.serializers.Serializer; import org.exist.util.serializer.SAXSerializer; import org.exist.util.serializer.SerializerPool; import org.exist.xmldb.XmldbURI; import org.exist.xquery.BasicFunction; import org.exist.xquery.Cardinality; import org.exist.xquery.FunctionSignature; import org.exist.xquery.XPathException; import org.exist.xquery.XQueryContext; import org.exist.xquery.value.DateTimeValue; import org.exist.xquery.value.FunctionParameterSequenceType; import org.exist.xquery.value.FunctionReturnSequenceType; import org.exist.xquery.value.Sequence; import org.exist.xquery.value.SequenceType; import org.exist.xquery.value.Type; import org.xml.sax.SAXException; public class Sync extends BasicFunction { public final static FunctionSignature signature = new FunctionSignature( new QName("sync", FileModule.NAMESPACE_URI, FileModule.PREFIX), "Synchronize a collection with a directory hierarchy. Compares last modified time stamps. " + "If $dateTime is given, only resources modified after this time stamp are taken into account. " + "This method is only available to the DBA role.", new SequenceType[]{ new FunctionParameterSequenceType("collection", Type.STRING, Cardinality.EXACTLY_ONE, "The collection to sync."), new FunctionParameterSequenceType("targetPath", Type.ITEM, Cardinality.EXACTLY_ONE, "The full path or URI to the directory"), new FunctionParameterSequenceType("dateTime", Type.DATE_TIME, Cardinality.ZERO_OR_ONE, "Optional: only resources modified after the given date/time will be synchronized.") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, "true if successful, false otherwise") ); private final static Properties DEFAULT_PROPERTIES = new Properties(); static { DEFAULT_PROPERTIES.put(OutputKeys.INDENT, "yes"); DEFAULT_PROPERTIES.put(OutputKeys.OMIT_XML_DECLARATION, "no"); } public Sync(XQueryContext context) { super(context, signature); } @Override public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException { if (!context.getUser().hasDbaRole()) throw new XPathException(this, "Function file:sync is only available to the DBA role"); String collectionPath = args[0].getStringValue(); Date startDate = null; if (args[2].hasOne()) { DateTimeValue dtv = (DateTimeValue) args[2].itemAt(0); startDate = dtv.getDate(); } String target = args[1].getStringValue(); File targetDir = FileModuleHelper.getFile(target); context.pushDocumentContext(); MemTreeBuilder output = context.getDocumentBuilder(); try { if (!targetDir.isAbsolute()) { File home = context.getBroker().getConfiguration().getExistHome(); targetDir = new File(home, target); } output.startDocument(); output.startElement(new QName("sync", FileModule.NAMESPACE_URI), null); output.addAttribute(new QName("collection", FileModule.NAMESPACE_URI), collectionPath); output.addAttribute(new QName("dir", FileModule.NAMESPACE_URI), targetDir.getAbsolutePath()); saveCollection(XmldbURI.create(collectionPath), targetDir, startDate, output); output.endElement(); output.endDocument(); } finally { context.popDocumentContext(); } return output.getDocument(); } private void saveCollection(XmldbURI collectionPath, File targetDir, Date startDate, MemTreeBuilder output) { if (!targetDir.exists() && !targetDir.mkdirs()) { reportError(output, "Failed to create output directory: " + targetDir.getAbsolutePath() + " for collection " + collectionPath); return; } if (!targetDir.canWrite()) { reportError(output, "Failed to write to output directory: " + targetDir.getAbsolutePath()); return; } List<XmldbURI> subcollections = null; Collection collection = null; try { collection = context.getBroker().openCollection(collectionPath, Lock.READ_LOCK); if (collection == null) { reportError(output, "Collection not found: " + collectionPath); return; } for (Iterator<DocumentImpl> i = collection.iterator(context.getBroker()); i.hasNext(); ) { DocumentImpl doc = i.next(); if (startDate == null || doc.getMetadata().getLastModified() > startDate.getTime()) { if (doc.getResourceType() == DocumentImpl.BINARY_FILE) { saveBinary(targetDir, (BinaryDocument) doc, output); } else { saveXML(targetDir, doc, output); } } } subcollections = new ArrayList<XmldbURI>(collection.getChildCollectionCount()); for (Iterator<XmldbURI> i = collection.collectionIterator(); i.hasNext(); ) { subcollections.add(i.next()); } } finally { if (collection != null) collection.getLock().release(Lock.READ_LOCK); } for (XmldbURI childURI : subcollections) { File childDir = new File(targetDir, childURI.lastSegment().toString()); saveCollection(collectionPath.append(childURI), childDir, startDate, output); } } private void reportError(MemTreeBuilder output, String msg) { output.startElement(new QName("error", FileModule.NAMESPACE_URI), null); output.characters(msg); output.endElement(); } private void saveXML(File targetDir, DocumentImpl doc, MemTreeBuilder output) { File targetFile = new File(targetDir, doc.getFileURI().toASCIIString()); if (targetFile.exists() && targetFile.lastModified() >= doc.getMetadata().getLastModified()) { return; } SAXSerializer sax = (SAXSerializer)SerializerPool.getInstance().borrowObject( SAXSerializer.class ); try { output.startElement(new QName("update", FileModule.NAMESPACE_URI), null); output.addAttribute(new QName("file"), targetFile.getAbsolutePath()); output.addAttribute(new QName("name"), doc.getFileURI().toString()); output.addAttribute(new QName("collection"), doc.getCollection().getURI().toString()); output.addAttribute(new QName("type"), "xml"); output.addAttribute(new QName("modified"), new DateTimeValue(new Date(doc.getMetadata().getLastModified())).getStringValue()); OutputStream os = new FileOutputStream(targetFile); Writer writer = new OutputStreamWriter( os, "UTF-8" ); sax.setOutput(writer, DEFAULT_PROPERTIES); Serializer serializer = context.getBroker().getSerializer(); serializer.reset(); serializer.setProperties(DEFAULT_PROPERTIES); serializer.setReceiver( sax ); serializer.toSAX( doc ); writer.close(); } catch (IOException e) { reportError(output, "IO error while saving file: " + targetFile.getAbsolutePath()); } catch (SAXException e) { reportError(output, "SAX exception while saving file " + targetFile.getAbsolutePath() + ": " + e.getMessage()); } catch (XPathException e) { reportError(output, e.getMessage()); } finally { output.endElement(); SerializerPool.getInstance().returnObject( sax ); } } private void saveBinary(File targetDir, BinaryDocument binary, MemTreeBuilder output) { File targetFile = new File(targetDir, binary.getFileURI().toASCIIString()); if (targetFile.exists() && targetFile.lastModified() >= binary.getMetadata().getLastModified()) { return; } try { output.startElement(new QName("update", FileModule.NAMESPACE_URI), null); output.addAttribute(new QName("file"), targetFile.getAbsolutePath()); output.addAttribute(new QName("name"), binary.getFileURI().toString()); output.addAttribute(new QName("collection"), binary.getCollection().getURI().toString()); output.addAttribute(new QName("type"), "binary"); output.addAttribute(new QName("modified"), new DateTimeValue(new Date(binary.getMetadata().getLastModified())).getStringValue()); OutputStream os = new FileOutputStream(targetFile); InputStream is = context.getBroker().getBinaryResource(binary); int c; byte buf[] = new byte[4096]; while ((c = is.read(buf)) > -1) { os.write(buf, 0, c); } os.close(); is.close(); } catch (IOException e) { reportError(output, "IO error while saving file: " + targetFile.getAbsolutePath()); } catch (XPathException e) { reportError(output, e.getMessage()); } finally { output.endElement(); } } }