/*
* Copyright (C) 2014 Jan Pokorsky
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.cas.lib.proarc.common.object.ndk;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yourmediashelf.fedora.client.FedoraClientException;
import cz.cas.lib.proarc.common.dublincore.DcStreamEditor;
import cz.cas.lib.proarc.common.dublincore.DcStreamEditor.DublinCoreRecord;
import cz.cas.lib.proarc.common.export.mets.ValidationErrorHandler;
import cz.cas.lib.proarc.common.fedora.DigitalObjectException;
import cz.cas.lib.proarc.common.fedora.DigitalObjectValidationException;
import cz.cas.lib.proarc.common.fedora.FedoraObject;
import cz.cas.lib.proarc.common.fedora.FoxmlUtils;
import cz.cas.lib.proarc.common.fedora.PageView.PageViewHandler;
import cz.cas.lib.proarc.common.fedora.PageView.PageViewItem;
import cz.cas.lib.proarc.common.fedora.RemoteStorage;
import cz.cas.lib.proarc.common.fedora.SearchView;
import cz.cas.lib.proarc.common.fedora.SearchView.Item;
import cz.cas.lib.proarc.common.fedora.SearchView.Query;
import cz.cas.lib.proarc.common.fedora.XmlStreamEditor;
import cz.cas.lib.proarc.common.fedora.relation.RelationEditor;
import cz.cas.lib.proarc.common.json.JsonUtils;
import cz.cas.lib.proarc.common.mods.ModsStreamEditor;
import cz.cas.lib.proarc.common.mods.ModsUtils;
import cz.cas.lib.proarc.common.mods.custom.ModsConstants;
import cz.cas.lib.proarc.common.mods.ndk.NdkMapper;
import cz.cas.lib.proarc.common.mods.ndk.NdkMapper.Context;
import cz.cas.lib.proarc.common.mods.ndk.NdkMapperFactory;
import cz.cas.lib.proarc.common.mods.ndk.NdkPageMapper;
import cz.cas.lib.proarc.common.mods.ndk.NdkPageMapper.Page;
import cz.cas.lib.proarc.common.object.DescriptionMetadata;
import cz.cas.lib.proarc.common.object.DigitalObjectCrawler;
import cz.cas.lib.proarc.common.object.DigitalObjectElement;
import cz.cas.lib.proarc.common.object.DigitalObjectHandler;
import cz.cas.lib.proarc.common.object.DigitalObjectManager;
import cz.cas.lib.proarc.common.object.MetadataHandler;
import cz.cas.lib.proarc.common.object.model.MetaModel;
import cz.cas.lib.proarc.common.object.model.MetaModelRepository;
import cz.cas.lib.proarc.mods.DateDefinition;
import cz.cas.lib.proarc.mods.FormDefinition;
import cz.cas.lib.proarc.mods.IdentifierDefinition;
import cz.cas.lib.proarc.mods.LocationDefinition;
import cz.cas.lib.proarc.mods.ModsDefinition;
import cz.cas.lib.proarc.mods.OriginInfoDefinition;
import cz.cas.lib.proarc.mods.PhysicalDescriptionDefinition;
import cz.cas.lib.proarc.mods.PhysicalLocationDefinition;
import cz.cas.lib.proarc.mods.StringPlusLanguage;
import cz.cas.lib.proarc.mods.TitleInfoDefinition;
import cz.cas.lib.proarc.oaidublincore.OaiDcType;
import java.io.IOException;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.xml.bind.DataBindingException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Validator;
import org.xml.sax.SAXException;
/**
* Handles description metadata in the NDK format.
*
* @author Jan Pokorsky
*/
public class NdkMetadataHandler implements MetadataHandler<ModsDefinition>, PageViewHandler {
public static final String ERR_NDK_CHANGE_MODS_WITH_URNNBN = "Err_Ndk_Change_Mods_With_UrnNbn";
public static final String ERR_NDK_CHANGE_MODS_WITH_MEMBERS = "Err_Ndk_Change_Mods_With_Members";
public static final String ERR_NDK_DOI_DUPLICITY = "Err_Ndk_Doi_Duplicity";
public static final String ERR_NDK_REMOVE_URNNBN = "Err_Ndk_Remove_UrnNbn";
/**
* The set of model IDs that should be checked for connected members.
*/
private static final Set<String> HAS_MEMBER_VALIDATION_MODELS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
NdkPlugin.MODEL_MONOGRAPHTITLE, NdkPlugin.MODEL_PERIODICAL, NdkPlugin.MODEL_PERIODICALVOLUME
)));
private static final Logger LOG = Logger.getLogger(NdkMetadataHandler.class.getName());
protected final DigitalObjectHandler handler;
protected final ModsStreamEditor editor;
protected final FedoraObject fobject;
protected DigitalObjectCrawler crawler;
private final NdkMapperFactory mapperFactory;
public NdkMetadataHandler(DigitalObjectHandler handler) {
this(handler, new NdkMapperFactory());
}
public NdkMetadataHandler(DigitalObjectHandler handler, NdkMapperFactory mapperFactory) {
this.handler = handler;
this.fobject = handler.getFedoraObject();
XmlStreamEditor streamEditor = fobject.getEditor(FoxmlUtils.inlineProfile(
DESCRIPTION_DATASTREAM_ID, ModsConstants.NS, DESCRIPTION_DATASTREAM_LABEL));
this.editor = new ModsStreamEditor(streamEditor, fobject);
this.mapperFactory = mapperFactory;
}
@Override
public void setMetadata(DescriptionMetadata<ModsDefinition> data, String message) throws DigitalObjectException {
ModsDefinition mods = data.getData();
String modelId = handler.relations().getModel();
if (mods == null) {
mods = createDefault(modelId);
}
write(modelId, mods, data, message);
}
/**
* Creates a new MODS with required default values according to model ID.
* Override to support custom models.
*/
protected ModsDefinition createDefault(String modelId) throws DigitalObjectException {
ModsDefinition defaultMods = ModsStreamEditor.defaultMods(fobject.getPid());
DigitalObjectHandler parent = handler.getParameterParent();
if (NdkPlugin.MODEL_PERIODICALISSUE.equals(modelId)) {
// issue 124
DigitalObjectHandler title = findEnclosingObject(parent, NdkPlugin.MODEL_PERIODICAL);
if (title != null) {
ModsDefinition titleMods = title.<ModsDefinition>metadata().getMetadata().getData();
inheritTitleInfo(defaultMods, titleMods.getTitleInfo());
defaultMods.getLanguage().addAll(titleMods.getLanguage());
inheritLocation(defaultMods, titleMods.getLocation());
inheritIdentifier(defaultMods, titleMods.getIdentifier(), "ccnb", "issn");
}
} else if (NdkPlugin.MODEL_PERIODICALSUPPLEMENT.equals(modelId)) {
// issue 137
DigitalObjectHandler title = findEnclosingObject(parent, NdkPlugin.MODEL_PERIODICAL);
if (title != null) {
ModsDefinition titleMods = title.<ModsDefinition>metadata().getMetadata().getData();
inheritSupplementTitleInfo(defaultMods, titleMods.getTitleInfo());
defaultMods.getLanguage().addAll(titleMods.getLanguage());
inheritIdentifier(defaultMods, titleMods.getIdentifier(), "ccnb", "issn");
}
} else if (NdkPlugin.MODEL_MONOGRAPHSUPPLEMENT.equals(modelId)) {
// issue 240
DigitalObjectHandler title = findEnclosingObject(parent, NdkPlugin.MODEL_MONOGRAPHVOLUME);
if (title != null) {
ModsDefinition titleMods = title.<ModsDefinition>metadata().getMetadata().getData();
inheritSupplementTitleInfo(defaultMods, titleMods.getTitleInfo());
defaultMods.getLanguage().addAll(titleMods.getLanguage());
inheritIdentifier(defaultMods, titleMods.getIdentifier(), "ccnb", "isbn");
inheritOriginInfoDateIssued(defaultMods, titleMods.getOriginInfo());
inheritPhysicalDescriptionForm(defaultMods, titleMods.getPhysicalDescription());
}
} else if (NdkPlugin.MODEL_CHAPTER.equals(modelId)) {
// issue 241
DigitalObjectHandler title = findEnclosingObject(parent, NdkPlugin.MODEL_MONOGRAPHVOLUME);
if (title != null) {
ModsDefinition titleMods = title.<ModsDefinition>metadata().getMetadata().getData();
defaultMods.getLanguage().addAll(titleMods.getLanguage());
inheritIdentifier(defaultMods, titleMods.getIdentifier(), "ccnb", "isbn");
inheritPhysicalDescriptionForm(defaultMods, titleMods.getPhysicalDescription());
}
}
return defaultMods;
}
protected final void inheritIdentifier(ModsDefinition mods, List<IdentifierDefinition> ids, String... includeIdTypes) {
for (IdentifierDefinition id : ids) {
String type = id.getType();
if (includeIdTypes == null) {
mods.getIdentifier().add(id);
} else {
for (String includeIdType : includeIdTypes) {
if (includeIdType.equals(type)) {
mods.getIdentifier().add(id);
}
}
}
}
}
private void inheritLocation(ModsDefinition mods, List<LocationDefinition> locs) {
for (LocationDefinition loc : locs) {
List<PhysicalLocationDefinition> pls = loc.getPhysicalLocation();
List<StringPlusLanguage> sls = loc.getShelfLocator();
if (!pls.isEmpty() || !sls.isEmpty()) {
loc.getUrl().clear();
mods.getLocation().add(loc);
}
}
}
protected final void inheritOriginInfoDateIssued(ModsDefinition mods, List<OriginInfoDefinition> ois) {
for (OriginInfoDefinition oi : ois) {
OriginInfoDefinition newOi = null;
for (DateDefinition dateIssued : oi.getDateIssued()) {
if (newOi == null) {
newOi = new OriginInfoDefinition();
mods.getOriginInfo().add(newOi);
}
newOi.getDateIssued().add(dateIssued);
}
}
}
protected final void inheritPhysicalDescriptionForm(ModsDefinition mods, List<PhysicalDescriptionDefinition> pds) {
for (PhysicalDescriptionDefinition pd : pds) {
PhysicalDescriptionDefinition newPd = null;
for (FormDefinition form : pd.getForm()) {
if (newPd == null) {
newPd = new PhysicalDescriptionDefinition();
mods.getPhysicalDescription().add(newPd);
}
newPd.getForm().add(form);
}
}
}
private void inheritTitleInfo(ModsDefinition mods, List<TitleInfoDefinition> tis) {
for (TitleInfoDefinition ti : tis) {
if (ti.getType() == null) {
ti.getPartNumber().clear();
ti.getPartName().clear();
ti.getNonSort().clear();
mods.getTitleInfo().add(ti);
}
}
}
protected final void inheritSupplementTitleInfo(ModsDefinition mods, List<TitleInfoDefinition> tis) {
for (TitleInfoDefinition ti : tis) {
if (ti.getType() == null) {
ti.getPartNumber().clear();
ti.getPartName().clear();
ti.getNonSort().clear();
ti.getSubTitle().clear();
mods.getTitleInfo().add(ti);
}
}
}
@Override
public void setMetadataAsJson(DescriptionMetadata<String> jsonData, String message) throws DigitalObjectException {
String json = jsonData.getData();
String editorId = jsonData.getEditor();
String modelId = handler.relations().getModel();
ModsDefinition mods;
if (json == null) {
mods = createDefault(modelId);
} else {
NdkMapper mapper = mapperFactory.get(modelId);
Context context = new Context(handler);
ObjectMapper jsMapper = JsonUtils.defaultObjectMapper();
try {
mods = mapper.fromJsonObject(jsMapper, json, context);
} catch (Exception ex) {
throw new DigitalObjectException(fobject.getPid(), null, ModsStreamEditor.DATASTREAM_ID, null, ex);
}
}
write(modelId, mods, jsonData, message);
}
@Override
public void setMetadataAsXml(DescriptionMetadata<String> xmlData, String message) throws DigitalObjectException {
ModsDefinition mods;
String modelId = handler.relations().getModel();
if (xmlData.getData() != null) {
ValidationErrorHandler errHandler = new ValidationErrorHandler();
try {
Validator validator = ModsUtils.getSchema().newValidator();
validator.setErrorHandler(errHandler);
validator.validate(new StreamSource(new StringReader(xmlData.getData())));
checkValidation(errHandler, xmlData);
mods = ModsUtils.unmarshalModsType(new StreamSource(new StringReader(xmlData.getData())));
} catch (DataBindingException | SAXException | IOException ex) {
checkValidation(errHandler, xmlData);
throw new DigitalObjectValidationException(xmlData.getPid(),
xmlData.getBatchId(), ModsStreamEditor.DATASTREAM_ID, null, ex)
.addValidation("mods", ex.getMessage());
}
} else {
mods = createDefault(modelId);
}
write(modelId, mods, xmlData, message);
}
private void checkValidation(ValidationErrorHandler errHandler, DescriptionMetadata<String> xmlData)
throws DigitalObjectValidationException {
if (!errHandler.getValidationErrors().isEmpty()) {
String msg = errHandler.getValidationErrors().stream().collect(Collectors.joining("\n"));
throw new DigitalObjectValidationException(xmlData.getPid(), xmlData.getBatchId(), ModsStreamEditor.DATASTREAM_ID, msg, null)
.addValidation("mods", msg);
}
}
@Override
public DescriptionMetadata<ModsDefinition> getMetadata() throws DigitalObjectException {
ModsDefinition mods = editor.read();
DescriptionMetadata<ModsDefinition> dm = new DescriptionMetadata<ModsDefinition>();
dm.setPid(fobject.getPid());
dm.setTimestamp(editor.getLastModified());
// dm.setEditor(editorId);
dm.setData(mods);
return dm;
}
@Override
@SuppressWarnings("unchecked")
public <O> DescriptionMetadata<O> getMetadataAsJsonObject(String mappingId) throws DigitalObjectException {
DescriptionMetadata<ModsDefinition> dm = getMetadata();
DescriptionMetadata json = dm;
if (mappingId == null) {
String modelId = handler.relations().getModel();
MetaModel model = modelId == null ? null : MetaModelRepository.getInstance().find(modelId);
if (model == null) {
throw new DigitalObjectException(fobject.getPid(), null, "ds", "Missing mappingId!", null);
}
mappingId = model.getModsCustomEditor();
}
NdkMapper mapper = mapperFactory.get(mappingId);
Context context = new Context(handler);
json.setData(mapper.toJsonObject(dm.getData(), context));
json.setEditor(mappingId);
return json;
}
@Override
public DescriptionMetadata<String> getMetadataAsXml() throws DigitalObjectException {
String xml = editor.readAsString();
DescriptionMetadata<String> dm = new DescriptionMetadata<String>();
dm.setPid(fobject.getPid());
dm.setTimestamp(editor.getLastModified());
// dm.setEditor(editorId);
dm.setData(xml);
return dm;
}
@Override
public PageViewItem createPageViewItem(Locale locale) throws DigitalObjectException {
String modelId = handler.relations().getModel();
if (modelId.equals(NdkPlugin.MODEL_PAGE)) {
ModsDefinition mods = editor.read();
NdkPageMapper mapper = new NdkPageMapper();
Page page = mapper.toJsonObject(mods, new Context(handler));
PageViewItem item = new PageViewItem();
item.setPageIndex(page.getIndex());
item.setPageNumber(page.getNumber());
item.setPageType(page.getType());
item.setPageTypeLabel(NdkPageMapper.getPageTypeLabel(item.getPageType(), locale));
return item;
} else {
throw new DigitalObjectException(fobject.getPid(), "Unexpected model for NDK page: " + modelId);
}
}
@Override
public void setPage(PageViewItem page, String message) throws DigitalObjectException {
String modelId = handler.relations().getModel();
if (modelId.equals(NdkPlugin.MODEL_PAGE)) {
DescriptionMetadata<ModsDefinition> metadata = new DescriptionMetadata<ModsDefinition>();
metadata.setTimestamp(editor.getLastModified());
NdkPageMapper mapper = new NdkPageMapper();
ModsDefinition mods = mapper.createPage(
page.getPageIndex(), page.getPageNumber(), page.getPageType(), new Context(handler));
metadata.setIgnoreValidation(true);
write(modelId, mods, metadata, message);
} else {
throw new DigitalObjectException(fobject.getPid(), "Unexpected model for NDK page: " + modelId);
}
}
private void checkBeforeWrite(ModsDefinition mods, ModsDefinition oldMods, boolean ignoreValidations) throws DigitalObjectException {
if (ignoreValidations) {
checkIdentifiers(mods, oldMods, null);
return ;
}
DigitalObjectValidationException ex = new DigitalObjectValidationException(fobject.getPid(), null,
DESCRIPTION_DATASTREAM_ID, "MODS validation", null);
checkIdentifiers(mods, oldMods, ex);
RelationEditor relations = handler.relations();
List<String> members = relations.getMembers();
if (HAS_MEMBER_VALIDATION_MODELS.contains(relations.getModel()) && !members.isEmpty()) {
ex.addValidation("mods", ERR_NDK_CHANGE_MODS_WITH_MEMBERS);
}
if (!ex.getValidations().isEmpty()) {
throw ex;
}
}
private void checkIdentifiers(ModsDefinition mods, ModsDefinition oldMods, DigitalObjectValidationException ex) throws DigitalObjectException {
ModsStreamEditor.addPid(mods, fobject.getPid());
List<IdentifierDefinition> oldIds = oldMods != null ? oldMods.getIdentifier()
: Collections.<IdentifierDefinition>emptyList();
// check URN:NBN
for (IdentifierDefinition oldId : oldIds) {
if ("urnnbn".equals(oldId.getType()) && oldId.getValue() != null && !oldId.getValue().trim().isEmpty()) {
boolean missingId = true;
for (IdentifierDefinition id : mods.getIdentifier()) {
if (oldId.getType().equals(id.getType()) && oldId.getValue().equals(id.getValue())) {
missingId = false;
break;
}
}
if (missingId) {
if (ex != null) {
ex.addValidation("mods.identifier", ERR_NDK_REMOVE_URNNBN, oldId.getValue());
} else {
mods.getIdentifier().add(oldId);
}
} else if (ex != null) {
ex.addValidation("mods.identifier", ERR_NDK_CHANGE_MODS_WITH_URNNBN, oldId.getValue());
}
}
}
checkDoiDuplicity(mods, ex);
}
/** issue 443. */
private void checkDoiDuplicity(ModsDefinition mods, DigitalObjectValidationException ex) throws DigitalObjectException {
if (ex == null) {
return ;
}
SearchView search = RemoteStorage.getInstance().getSearch();
for (IdentifierDefinition idDef : mods.getIdentifier()) {
if ("doi".equals(idDef.getType()) && idDef.getValue() != null) {
String doi = idDef.getValue();
if (doi != null && !doi.isEmpty()) {
try {
List<Item> results = search.findQuery(new Query().setIdentifier(doi));
if (!results.isEmpty()) {
if (results.size() == 1 && results.get(0).getPid().equals(fobject.getPid())) {
// ignore the self-reference
continue;
}
ex.addValidation("mods.identifier", ERR_NDK_DOI_DUPLICITY, doi);
}
} catch (FedoraClientException ex1) {
throw new DigitalObjectException(fobject.getPid(), ex1);
} catch (IOException ex1) {
throw new DigitalObjectException(fobject.getPid(), ex1);
}
}
}
}
}
protected void write(String modelId, ModsDefinition mods,
DescriptionMetadata<?> options, String message) throws DigitalObjectException {
ModsDefinition oldMods = null;
long timestamp = options.getTimestamp();
if (timestamp < 0) {
// rewrite with brand new MODS
timestamp = editor.getLastModified();
}
if (timestamp > 0) {
oldMods = editor.read();
}
checkBeforeWrite(mods, oldMods, options.isIgnoreValidation());
NdkMapper mapper = mapperFactory.get(modelId);
Context context = new Context(handler);
mapper.createMods(mods, context);
if (LOG.isLoggable(Level.FINE)) {
String toXml = ModsUtils.toXml(mods, true);
LOG.fine(toXml);
}
editor.write(mods, timestamp, message);
// DC
OaiDcType dc = mapper.toDc(mods, context);
DcStreamEditor dcEditor = handler.objectMetadata();
DublinCoreRecord dcr = dcEditor.read();
dcr.setDc(dc);
dcEditor.write(handler, dcr, message);
// Label
String label = mapper.toLabel(mods);
fobject.setLabel(label);
}
protected final DigitalObjectHandler findEnclosingObject(
DigitalObjectHandler obj, String searcModelId) throws DigitalObjectException {
if (obj != null) {
if (searcModelId.equals(obj.relations().getModel())) {
return obj;
} else {
DigitalObjectElement parent = getCrawler().getParent(obj.getFedoraObject().getPid());
return findEnclosingObject(parent, searcModelId);
}
}
return null;
}
private DigitalObjectHandler findEnclosingObject(
DigitalObjectElement obj, String searcModelId) throws DigitalObjectException {
if (obj == DigitalObjectElement.NULL) {
return null;
} else if (searcModelId.equals(obj.getModelId())) {
return obj.getHandler();
} else {
DigitalObjectElement parent = getCrawler().getParent(obj.getPid());
return findEnclosingObject(parent, searcModelId);
}
}
public DigitalObjectCrawler getCrawler() {
if (crawler == null) {
crawler = new DigitalObjectCrawler(DigitalObjectManager.getDefault(),
RemoteStorage.getInstance().getSearch());
}
return crawler;
}
/**
* Wraps MODS for JSON serialization. Subclasses can add own properties.
*/
public static class ModsWrapper {
private ModsDefinition mods;
public ModsWrapper() {
}
public ModsWrapper(ModsDefinition mods) {
this.mods = mods;
}
public ModsDefinition getMods() {
return mods;
}
public void setMods(ModsDefinition mods) {
this.mods = mods;
}
}
}