/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE file at the root of the source
* tree and available online at
*
* https://github.com/keeps/roda
*/
package org.roda.core.plugins.misc;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
import org.apache.commons.io.FilenameUtils;
import org.roda.core.storage.fs.FSUtils;
import org.roda_project.commons_ip.model.AIP;
import org.roda_project.commons_ip.model.IPContentType;
import org.roda_project.commons_ip.model.IPDescriptiveMetadata;
import org.roda_project.commons_ip.model.IPFile;
import org.roda_project.commons_ip.model.IPMetadata;
import org.roda_project.commons_ip.model.IPRepresentation;
import org.roda_project.commons_ip.model.MetadataType;
import org.roda_project.commons_ip.model.ParseException;
import org.roda_project.commons_ip.model.RepresentationContentType;
import org.roda_project.commons_ip.model.RepresentationStatus;
import org.roda_project.commons_ip.model.impl.AIPWrap;
import org.roda_project.commons_ip.model.impl.BasicAIP;
import org.roda_project.commons_ip.utils.IPException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link AIP} implementation that can read a RODA AIP from a folder.
*
* @author Rui Castro (rui.castro@gmail.com)
*/
final class RodaFolderAIP extends AIPWrap {
/**
* Logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(RodaFolderAIP.class);
/**
* Constant string "id".
*/
private static final String ID = "id";
/**
* Constant string "type".
*/
private static final String TYPE = "type";
/**
* Constant string "version".
*/
private static final String VERSION = "version";
/**
* Constant string "representations".
*/
private static final String REPRESENTATIONS = "representations";
/**
* Constant string "metadata".
*/
private static final String METADATA = "metadata";
/**
* Constant string "descriptive".
*/
private static final String DESCRIPTIVE = "descriptive";
/**
* Constant string "descriptiveMetadata".
*/
private static final String DESCRIPTIVE_METADATA = "descriptiveMetadata";
/**
* Constant string "preservation".
*/
private static final String PRESERVATION = "preservation";
/**
* Constant string "other".
*/
private static final String OTHER = "other";
/**
* Constant string "schemas".
*/
private static final String SCHEMAS = "schemas";
/**
* Constant string "documentation".
*/
private static final String DOCUMENTATION = "documentation";
/**
* Constant string "submission".
*/
private static final String SUBMISSION = "submission";
/**
* Constructor.
*
* @param aip
* the {@link AIP} to wrap.
* @param path
* AIP path.
*/
private RodaFolderAIP(final AIP aip, final Path path) {
super(aip);
setBasePath(path);
}
/**
* Reads a {@link RodaFolderAIP} from the given folder.
*
* @param source
* the source folder.
* @return a {@link RodaFolderAIP}.
* @throws ParseException
* if some error occurs.
*/
static AIP parse(final Path source) throws ParseException {
return new RodaFolderAIP(new BasicAIP(), source).read();
}
/**
* Read the AIP.
*
* @return the {@link AIP}.
* @throws ParseException
* if some error occurred.
*/
private AIP read() throws ParseException {
try {
final ObjectMapper mapper = new ObjectMapper();
final JsonNode json = mapper.readTree(getBasePath().resolve("aip.json").toFile());
this.setId(getBasePath().getFileName().toString());
if (json.has("parentId")) {
this.setAncestors(Collections.singletonList(json.get("parentId").asText()));
}
this.setContentType(new IPContentType(json.get(TYPE).asText()));
this.setState(json.get("state").asText());
final Path mdPath = getBasePath().resolve(METADATA);
readJsonDescriptiveMDs(this, json);
findAndAddPreservationMDs(this::addPreservationMetadata);
findAndAddOtherMDs(mdPath.resolve(OTHER), this::addOtherMetadata);
findIPFiles(getBasePath().resolve(SCHEMAS)).forEach(this::addSchema);
findIPFiles(getBasePath().resolve(DOCUMENTATION)).forEach(this::addDocumentation);
findIPFiles(getBasePath().resolve(SUBMISSION)).forEach(this::addSubmission);
readRepresentations(this, json);
return this;
} catch (final IOException | IPException e) {
LOGGER.debug("Error reading aip.json", e);
throw new ParseException("Error reading aip.json", e);
}
}
/**
* Read {@link IPRepresentation} and update the {@link AIP}.
*
* @param aip
* the {@link AIP}.
* @param json
* the JSON node.
* @throws IPException
* if some error occurs.
* @throws IOException
* if some I/O error occurs.
*/
private void readRepresentations(final AIP aip, final JsonNode json) throws IPException, IOException {
if (json.has(REPRESENTATIONS)) {
final Iterator<JsonNode> iterator = json.get(REPRESENTATIONS).elements();
while (iterator.hasNext()) {
aip.addRepresentation(readRepresentation(iterator.next()));
}
}
}
/**
* Read {@link IPRepresentation}.
*
* @param json
* the JSON node.
* @return the {@link IPRepresentation}.
* @throws IOException
* if some I/O error occurs.
* @throws IPException
* if some error occured.
*/
private IPRepresentation readRepresentation(final JsonNode json) throws IOException, IPException {
final IPRepresentation rep = new IPRepresentation(json.get(ID).asText());
rep.setStatus(
json.get("original").asBoolean() ? RepresentationStatus.getORIGINAL() : RepresentationStatus.getOTHER());
rep.setContentType(new RepresentationContentType(json.get(TYPE).asText()));
final Path repPath = getBasePath().resolve(REPRESENTATIONS).resolve(rep.getRepresentationID());
final Path mdPath = repPath.resolve(METADATA);
findIPFiles(repPath.resolve("data")).forEach(rep::addFile);
findDescriptiveMDs(mdPath.resolve(DESCRIPTIVE)).forEach(rep::addDescriptiveMetadata);
findPreservationMDs(mdPath.resolve(PRESERVATION)).forEach(rep::addPreservationMetadata);
findAndAddOtherMDs(mdPath.resolve(OTHER), rep::addOtherMetadata);
// TODO: ask for example of representation agents
findIPFiles(repPath.resolve(SCHEMAS)).forEach(rep::addDocumentation);
findIPFiles(repPath.resolve(DOCUMENTATION)).forEach(rep::addDocumentation);
return rep;
}
/**
* Read {@link IPDescriptiveMetadata} records from <code>aip.json</code>
* "descriptiveMetadata" field and update the {@link AIP}.
*
* @param aip
* the {@link AIP}.
* @param json
* the JSON node.
* @throws IPException
* if some error occurs.
*/
private void readJsonDescriptiveMDs(final AIP aip, final JsonNode json) throws IPException {
if (json.has(DESCRIPTIVE_METADATA)) {
final Iterator<JsonNode> iterator = json.get(DESCRIPTIVE_METADATA).elements();
while (iterator.hasNext()) {
aip.addDescriptiveMetadata(readJsonDescriptionMD(iterator.next()));
}
}
}
/**
* Read {@link IPDescriptiveMetadata} from a {@link JsonNode}.
*
* @param json
* the JSON node.
* @return the {@link IPDescriptiveMetadata}.
*/
private IPDescriptiveMetadata readJsonDescriptionMD(final JsonNode json) {
final String id = json.get(ID).asText();
final String version = json.has(VERSION) ? json.get(VERSION).asText() : null;
return new IPDescriptiveMetadata(id, new IPFile(getBasePath().resolve(METADATA).resolve(DESCRIPTIVE).resolve(id)),
new MetadataType(json.get(TYPE).asText()), version);
}
/**
* Find preservation metadata files and adds them to {@link AIP}.
*
* @param mdSetter
* the {@link IPMetadataSetter}.
* @throws IOException
* if some I/O error occurs.
* @throws IPException
* if some other error occurs.
*/
private void findAndAddPreservationMDs(final IPMetadataSetter mdSetter) throws IOException, IPException {
for (IPMetadata md : findPreservationMDs(getBasePath().resolve(METADATA).resolve(PRESERVATION))) {
mdSetter.addMetadata(md);
}
}
/**
* Find other metadata files and for each one calls
* {@link IPMetadataSetter#addMetadata(IPMetadata)}.
*
* @param omdPath
* the {@link Path} to other metadata files.
* @param mdSetter
* the {@link IPMetadataSetter}.
* @throws IOException
* if some I/O error occurs.
* @throws IPException
* if some other error occurs.
*/
private void findAndAddOtherMDs(final Path omdPath, final IPMetadataSetter mdSetter) throws IOException, IPException {
if (FSUtils.isDirectory(omdPath)) {
final Iterator<Path> paths = Files.list(omdPath).iterator();
while (paths.hasNext()) {
final Path mdPath = paths.next();
if (FSUtils.isDirectory(mdPath)) {
for (IPMetadata md : findMDs(mdPath, MetadataType.OTHER().setOtherType(mdPath.getFileName().toString()))) {
mdSetter.addMetadata(md);
}
}
if (FSUtils.isFile(mdPath)) {
for (IPMetadata md : findMDs(mdPath, MetadataType.OTHER())) {
mdSetter.addMetadata(md);
}
}
}
}
}
/**
* Find {@link IPDescriptiveMetadata}s in the given {@link Path}.
*
* @param dmdPath
* the {@link Path}.
* @return the {@link List <IPDescriptiveMetadata>}.
* @throws IOException
* if some I/O error occurs.
*/
private List<IPDescriptiveMetadata> findDescriptiveMDs(final Path dmdPath) throws IOException {
final List<IPDescriptiveMetadata> list = new ArrayList<>();
if (FSUtils.isDirectory(dmdPath)) {
try (Stream<Path> paths = Files.walk(dmdPath)) {
paths.forEach(filePath -> {
if (FSUtils.isFile(filePath)) {
list.add(new IPDescriptiveMetadata(FilenameUtils.removeExtension(filePath.getFileName().toString()),
new IPFile(filePath), MetadataType.OTHER(), null));
}
});
}
}
return list;
}
/**
* Find {@link IPMetadata}s in the given {@link Path}.
*
* @param mdPath
* the {@link Path}.
* @return the {@link List<IPMetadata>}.
* @throws IOException
* if some I/O error occurs.
*/
private List<IPMetadata> findPreservationMDs(final Path mdPath) throws IOException {
return findMDs(mdPath, new MetadataType(MetadataType.MetadataTypeEnum.PREMIS));
}
/**
* Find {@link IPMetadata}s in the given {@link Path}.
*
* @param mdPath
* the {@link Path}.
* @param mdType
* the {@link MetadataType}.
* @return the {@link List<IPMetadata>}.
* @throws IOException
* if some I/O error occurs.
*/
private List<IPMetadata> findMDs(final Path mdPath, final MetadataType mdType) throws IOException {
final List<IPMetadata> list = new ArrayList<>();
if (FSUtils.isDirectory(mdPath)) {
try (Stream<Path> paths = Files.walk(mdPath)) {
paths.forEach(filePath -> {
if (FSUtils.isFile(filePath)) {
list.add(new IPMetadata(FilenameUtils.removeExtension(filePath.getFileName().toString()),
new IPFile(filePath), mdType));
}
});
}
}
return list;
}
/**
* Find {@link IPFile}s in the given {@link Path}.
*
* @param filesPath
* the {@link Path}.
* @return the {@link List<IPFile>}.
* @throws IOException
* if some I/O error occurs.
*/
private List<IPFile> findIPFiles(final Path filesPath) throws IOException {
final List<IPFile> list = new ArrayList<>();
if (FSUtils.isDirectory(filesPath)) {
try (Stream<Path> paths = Files.walk(filesPath)) {
paths.forEach(filePath -> {
if (FSUtils.isFile(filePath)) {
list.add(new IPFile(filePath));
}
});
}
}
return list;
}
/**
* {@link IPMetadata} setter interface. Implementations of this interface
* should add the {@link IPMetadata} records to themselves.
*/
@FunctionalInterface
interface IPMetadataSetter {
/**
* Add the specified metadata.
*
* @param md
* the {@link IPMetadata}.
* @throws IPException
* if some error occurred.
*/
void addMetadata(IPMetadata md) throws IPException;
}
}