/*
* (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Bogdan Stefanescu
* Thierry Delprat
* Florent Guillaume
*/
package org.nuxeo.ecm.core.io.impl.plugins;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CloseShieldInputStream;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.nuxeo.common.utils.Path;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.impl.blob.ZipEntryBlob;
import org.nuxeo.ecm.core.io.ExportConstants;
import org.nuxeo.ecm.core.io.ExportedDocument;
import org.nuxeo.ecm.core.io.impl.AbstractDocumentReader;
import org.nuxeo.ecm.core.io.impl.DWord;
import org.nuxeo.ecm.core.io.impl.ExportedDocumentImpl;
/**
* Reads nuxeo archives generated using {@link NuxeoArchiveWriter}.
* <p>
* If you need to read a CoreIO XML Archive that was not directly generated by {@link NuxeoArchiveWriter} or that was
* modified you need to use the NuxeoArchiveReader(File) constructor.
* <p>
* This implementation holds either a {@link ZipInputStream}, in which case it is assumed to have been generated by
* {@link NuxeoArchiveWriter} and has a special format (TODO DOCUMENT), or it holds a {@link ZipFile} that may be used
* in a random-access manner.
*/
public class NuxeoArchiveReader extends AbstractDocumentReader {
private ZipInputStream in;
private boolean inMustBeClosed;
private ZipFile zipFile;
private List<String> zipIndex;
private final Collection<File> filesToDelete = new ArrayList<File>();
/**
* Create a {@link NuxeoArchiveReader} from an {@link InputStream}.
* <p>
* The InputStream must point to an archive that was generated by {@link NuxeoArchiveWriter}.
*
* @param in InputStream pointing an archive that was generated by NuxeoArchiveWriter
* @throws IOException
*/
public NuxeoArchiveReader(InputStream in) throws IOException {
this(new ZipInputStream(in), true);
}
/**
* Protected constructor used by {@link ZipReader}. Must not close the stream when done.
*/
protected NuxeoArchiveReader(ZipInputStream in, boolean checkMarker) throws IOException {
this.in = in;
inMustBeClosed = !checkMarker;
if (checkMarker) {
checkMarker();
}
}
/**
* Create a {@link NuxeoArchiveReader} from a {@link File}.
* <p>
* This constructor is different from others because it allows the input zip file to have been generated by an other
* engine that {@link NuxeoArchiveWriter}.
* <p>
* In particular, you can use this constructor on a Zip Archive that was manually modified.
*
* @param file a Zip archive
*/
public NuxeoArchiveReader(File file) throws IOException {
this.zipFile = new ZipFile(file);
buildOrderedZipIndex();
checkMarker();
}
protected void buildOrderedZipIndex() {
zipIndex = new ArrayList<String>();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
zipIndex.add(entry.getName());
}
Collections.sort(zipIndex, new Comparator<String>() {
@Override
public int compare(String spath1, String spath2) {
return spath1.compareTo(spath2);
}
});
}
@Override
public ExportedDocument read() throws IOException {
if (zipFile != null) {
return readZip();
} else {
return readOrderedStream();
}
}
protected ExportedDocument readZip() throws IOException {
if (zipIndex.size() == 0) {
return null;
}
String idxname = zipIndex.remove(0);
ZipEntry entry = zipFile.getEntry(idxname);
if (entry == null) {
return null;
}
if (!entry.isDirectory()) {
if (entry.getName().equals(ExportConstants.MARKER_FILE)) {
return read();
} else if (entry.getName().equals(ExportConstants.DOCUMENT_FILE)) {
// the repository ROOT! TODO: how to handle root? it doesn't
// have a dir ..
ExportedDocument xdoc = new ExportedDocumentImpl();
xdoc.setPath(new Path("/"));
xdoc.setDocument(loadXML(entry));
return xdoc;
} else {
throw new IOException("Invalid Nuxeo archive on entry " + entry.getName());
}
}
// find the direct children entry that are part of the same document
// since archive is modifiable we can not rely on the Extra bits thing
List<String> childEntries = new ArrayList<String>();
int depth = new Path(idxname).removeTrailingSeparator().segmentCount();
for (String path : zipIndex) {
if (path.startsWith(idxname)) {
int subdepth = new Path(path).removeTrailingSeparator().segmentCount();
if (subdepth != depth + 1 || zipFile.getEntry(path).isDirectory()) {
continue;
}
childEntries.add(path);
} else {
break;
}
}
if (childEntries.size() == 0) {
return read(); // empty dir -> try next directory
}
String name = entry.getName();
ExportedDocument xdoc = new ExportedDocumentImpl();
xdoc.setPath(new Path(name).removeTrailingSeparator());
for (String childEntryName : childEntries) {
int i = zipIndex.indexOf(childEntryName);
idxname = zipIndex.remove(i);
entry = zipFile.getEntry(idxname);
name = entry.getName();
if (name.endsWith(ExportConstants.DOCUMENT_FILE)) {
xdoc.setDocument(loadXML(entry));
} else if (name.endsWith(".xml")) { // external doc file
xdoc.putDocument(FilenameUtils.getBaseName(entry.getName()), loadXML(entry));
} else { // should be a blob
xdoc.putBlob(FilenameUtils.getName(entry.getName()), createBlob(entry));
}
}
return xdoc;
}
protected ExportedDocument readOrderedStream() throws IOException {
ZipEntry entry = in.getNextEntry();
if (entry == null) {
return null;
}
if (!entry.isDirectory()) {
if (entry.getName().equals(ExportConstants.MARKER_FILE)) {
return read();
} else if (entry.getName().equals(ExportConstants.DOCUMENT_FILE)) {
// the repository ROOT! TODO: how to handle root? it doesn't
// have a dir ..
ExportedDocument xdoc = new ExportedDocumentImpl();
xdoc.setPath(new Path("/"));
xdoc.setDocument(loadXML(entry));
return xdoc;
} else {
throw new IOException("Invalid Nuxeo archive");
}
}
int count = getFilesCount(entry);
if (count == 0) {
return read(); // empty dir -> try next directory
}
String name = entry.getName();
ExportedDocument xdoc = new ExportedDocumentImpl();
xdoc.setPath(new Path(name).removeTrailingSeparator());
for (int i = 0; i < count; i++) {
entry = in.getNextEntry();
name = entry.getName();
if (name.endsWith(ExportConstants.DOCUMENT_FILE)) {
xdoc.setDocument(loadXML(entry));
} else if (name.endsWith(".xml")) { // external doc file
xdoc.putDocument(FilenameUtils.getBaseName(entry.getName()), loadXML(entry));
} else { // should be a blob
xdoc.putBlob(FilenameUtils.getName(entry.getName()), createBlob(entry));
}
}
return xdoc;
}
@Override
public void close() {
IOUtils.closeQuietly(zipFile);
if (inMustBeClosed) {
IOUtils.closeQuietly(in);
}
for (File file : filesToDelete) {
file.delete();
}
}
private static int getFilesCount(ZipEntry entry) throws IOException {
byte[] bytes = entry.getExtra();
if (bytes == null) {
return 0;
} else if (bytes.length != 4) {
throw new IOException("Invalid Nuxeo Archive");
} else {
return new DWord(bytes).getInt();
}
}
private Document loadXML(ZipEntry entry) throws IOException {
try {
SAXReader saxReader = new SAXReader();
if (zipFile != null) {
try (InputStream stream = zipFile.getInputStream(entry)) {
return saxReader.read(stream);
}
} else {
// SAXReader.read always closes the stream, but we don't want that
// so wrap it in a CloseShieldInputStream
try (CloseShieldInputStream ncin = new CloseShieldInputStream(in)) {
return saxReader.read(ncin);
}
}
} catch (DocumentException e) {
throw new IOException("Failed to read zip entry: " + entry.getName(), e);
}
}
private Blob createBlob(ZipEntry entry) throws IOException {
if (zipFile != null) {
return new ZipEntryBlob(zipFile, entry);
} else {
// should decompress since this is a generic stream
// FileBlob always closes the stream, but we don't want that
// so wrap it in a CloseShieldInputStream
try (CloseShieldInputStream ncin = new CloseShieldInputStream(in)) {
Blob blob = Blobs.createBlob(ncin);
filesToDelete.add(blob.getFile());
return blob;
}
}
}
private void checkMarker() throws IOException {
if (zipFile == null) {
ZipEntry entry = in.getNextEntry();
if (entry == null) {
throw new IOException("Not a valid Nuxeo Archive - no marker file found (unexpected end of zip)");
}
if (!isMarkerEntry(entry)) {
throw new IOException("Not a valid Nuxeo Archive - no marker file found");
}
} else {
if (!zipIndex.contains(ExportConstants.MARKER_FILE)) {
throw new IOException("Not a valid Nuxeo Archive - no marker file found");
}
}
}
public static boolean isMarkerEntry(ZipEntry entry) {
return entry.getName().equals(ExportConstants.MARKER_FILE);
}
}