diff --git a/pom.xml b/pom.xml index a212ebd7ab..45ac572540 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ ipt - 3.0.7-SNAPSHOT + 3.1.0-SNAPSHOT war IPT @@ -43,7 +43,7 @@ 1.16.13 0.60 2.21 - 2.2 + 2.3 0.67 1.4 diff --git a/src/main/java/org/gbif/ipt/action/BaseAction.java b/src/main/java/org/gbif/ipt/action/BaseAction.java index bf3362cec0..cf93ed4468 100644 --- a/src/main/java/org/gbif/ipt/action/BaseAction.java +++ b/src/main/java/org/gbif/ipt/action/BaseAction.java @@ -192,7 +192,7 @@ public String getRequestURL() { .replaceQueryParam("request_locale") .build().toString(); } catch (RuntimeException e) { - LOG.warn("Failed to reconstruct requestURL from " + req.getRequestURL(), e); + LOG.warn("Failed to reconstruct requestURL from {}. Error: {}", req.getRequestURL(), e.getMessage()); } return getBaseURL(); } diff --git a/src/main/java/org/gbif/ipt/action/manage/MetadataAction.java b/src/main/java/org/gbif/ipt/action/manage/MetadataAction.java index 74110b7c9b..e3b74871fd 100644 --- a/src/main/java/org/gbif/ipt/action/manage/MetadataAction.java +++ b/src/main/java/org/gbif/ipt/action/manage/MetadataAction.java @@ -362,14 +362,25 @@ public void prepare() { // update frequencies list, derived from XML vocabulary, and displayed in drop-down on basic metadata page frequencies = new LinkedHashMap<>(); - frequencies.putAll(vocabManager.getI18nVocab(Constants.VOCAB_URI_UPDATE_FREQUENCIES, getLocaleLanguage(), false)); - + // temporary fix: remove "unkown" from vocabulary + vocabManager.getI18nVocab(Constants.VOCAB_URI_UPDATE_FREQUENCIES, getLocaleLanguage(), false) + .entrySet() + .stream() + .filter(p -> !"unkown".equals(p.getKey())) + .forEach(p -> frequencies.put(p.getKey(), p.getValue())); // sanitize intellectualRights - pre-v2.2 text was manually entered and may have characters that break js if (getEml().getIntellectualRights() != null) { getEml().setIntellectualRights(removeNewlineCharacters(getEml().getIntellectualRights())); } + // Public, published occurrence resources have a distribution download URL + if (CoreRowType.OCCURRENCE.toString().equalsIgnoreCase(resource.getCoreType()) + && resource.isPublished() + && resource.isPubliclyAvailable()) { + resource.getEml().setDistributionDownloadUrl(cfg.getBaseUrl() + "/archive.do?r=" + resource.getShortname()); + } + // populate agent vocabularies loadAgentVocabularies(); @@ -397,10 +408,6 @@ public void prepare() { } if (isHttpPost()) { - resource.getEml().getDescription().clear(); - resource.getEml().getContacts().clear(); - resource.getEml().getCreators().clear(); - resource.getEml().getMetadataProviders().clear(); resource.getEml().setIntellectualRights(null); // publishing organisation, if provided must match organisation @@ -415,6 +422,17 @@ public void prepare() { } break; + case CONTACTS_SECTION: + // populate agent vocabularies + loadAgentVocabularies(); + if (isHttpPost()) { + resource.getEml().getContacts().clear(); + resource.getEml().getCreators().clear(); + resource.getEml().getMetadataProviders().clear(); + resource.getEml().getAssociatedParties().clear(); + } + break; + case GEOGRAPHIC_COVERAGE_SECTION: if (isHttpPost()) { resource.getEml().getGeospatialCoverages().clear(); @@ -443,19 +461,13 @@ public void prepare() { } break; - case PARTIES_SECTION: - // populate agent vocabularies - loadAgentVocabularies(); - if (isHttpPost()) { - resource.getEml().getAssociatedParties().clear(); - } - break; - case PROJECT_SECTION: // populate agent vocabularies loadAgentVocabularies(); if (isHttpPost()) { resource.getEml().getProject().getPersonnel().clear(); + resource.getEml().getProject().getAwards().clear(); + resource.getEml().getProject().getRelatedProjects().clear(); } break; @@ -506,9 +518,9 @@ public void prepare() { @Override public String save() throws Exception { - // before saving, the minimum amount of mandatory metadata must have been provided, and ALL metadata sections must - // be valid, otherwise an error is displayed - if (emlValidator.areAllSectionsValid(this, resource)) { + // before saving, the minimum amount of mandatory metadata must have been provided, and the current metadata section + // must be valid, otherwise an error is displayed + if (emlValidator.isSectionValid(this, resource, section)) { // Save metadata information (eml.xml) resourceManager.saveEml(resource); // save date metadata was last modified @@ -520,6 +532,12 @@ public String save() throws Exception { // progress to next section, since save succeeded switch (section) { case BASIC_SECTION: + next = MetadataSection.CONTACTS_SECTION; + break; + case CONTACTS_SECTION: + next = MetadataSection.ACKNOWLEDGEMENTS_SECTION; + break; + case ACKNOWLEDGEMENTS_SECTION: next = MetadataSection.GEOGRAPHIC_COVERAGE_SECTION; break; case GEOGRAPHIC_COVERAGE_SECTION: @@ -529,12 +547,12 @@ public String save() throws Exception { next = MetadataSection.TEMPORAL_COVERAGE_SECTION; break; case TEMPORAL_COVERAGE_SECTION: + next = MetadataSection.ADDITIONAL_DESCRIPTION_SECTION; + break; + case ADDITIONAL_DESCRIPTION_SECTION: next = MetadataSection.KEYWORDS_SECTION; break; case KEYWORDS_SECTION: - next = MetadataSection.PARTIES_SECTION; - break; - case PARTIES_SECTION: next = MetadataSection.PROJECT_SECTION; break; case PROJECT_SECTION: @@ -689,7 +707,7 @@ public String getResourceHasCore() { /** * On the basic metadata page, this map populates the update frequencies dropdown. The map is derived from the - * vocabulary {@link -linkoffline http://rs.gbif.org/vocabulary/eml/update_frequency.xml}. + * vocabulary http://rs.gbif.org/vocabulary/eml/update_frequency.xml. * * @return update frequencies map */ @@ -859,7 +877,7 @@ private void loadAgentVocabularies() { Agent current = new Agent(); current.setFirstName(getCurrentUser().getFirstname()); current.setLastName(getCurrentUser().getLastname()); - current.setEmail(getCurrentUser().getEmail()); + current.setEmail(Collections.singletonList(getCurrentUser().getEmail())); // contacts list Agent firstContact = null; diff --git a/src/main/java/org/gbif/ipt/action/manage/OverviewAction.java b/src/main/java/org/gbif/ipt/action/manage/OverviewAction.java index 9a6441934c..3de843eb95 100644 --- a/src/main/java/org/gbif/ipt/action/manage/OverviewAction.java +++ b/src/main/java/org/gbif/ipt/action/manage/OverviewAction.java @@ -96,10 +96,10 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.stream.Stream; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; +import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -1252,6 +1252,14 @@ public void prepare() { usableSpace = cfg.getDataDir().getDataDirUsableSpace(); freeDiscSpaceReadable = org.apache.commons.io.FileUtils.byteCountToDisplaySize(usableSpace); + + if (resource.isPublished() + && resource.getEml() != null + && resource.getEml().getProject() != null + && resource.getEml().getProject().getTitle() != null + && resource.getEml().getProject().getPersonnel().isEmpty()) { + addActionWarning(getText("validation.personnel.now.required")); + } } } @@ -1652,7 +1660,7 @@ public String replaceEml() { LOG.error("Failed to replace EML", e); addActionError(getText("manage.overview.failed.replace.eml")); return ERROR; - } catch (SAXException e) { + } catch (SAXException | ParserConfigurationException e) { LOG.error("Failed to create EML validator", e); addActionError(getText("manage.overview.failed.replace.eml.validator")); return ERROR; diff --git a/src/main/java/org/gbif/ipt/action/portal/ResourceAction.java b/src/main/java/org/gbif/ipt/action/portal/ResourceAction.java index 91952cf160..fb8aedeed8 100644 --- a/src/main/java/org/gbif/ipt/action/portal/ResourceAction.java +++ b/src/main/java/org/gbif/ipt/action/portal/ResourceAction.java @@ -899,7 +899,7 @@ private boolean isValidAgent(Agent agent) { */ private boolean agentsMatch(Agent agent1, Agent agent2) { boolean namesMatch = false; - boolean emailsMatch; + boolean emailsMatch = false; boolean positionsMatch; if (agent1.getFullName() != null && agent2.getFullName() != null) { @@ -907,7 +907,19 @@ private boolean agentsMatch(Agent agent1, Agent agent2) { } if (agent1.getEmail() != null && agent2.getEmail() != null) { - emailsMatch = agent1.getEmail().equalsIgnoreCase(agent2.getEmail()); + emailsMatch = true; + if (agent1.getEmail().size() != agent2.getEmail().size()) { + emailsMatch = false; + } else { + for (int i = 0; i < agent1.getEmail().size(); i++) { + String email1 = agent1.getEmail().get(i); + String email2 = agent2.getEmail().get(i); + if (!email1.equalsIgnoreCase(email2)) { + emailsMatch = false; + break; + } + } + } } else { emailsMatch = true; } diff --git a/src/main/java/org/gbif/ipt/config/Constants.java b/src/main/java/org/gbif/ipt/config/Constants.java index f28da7f3cb..144af4ece1 100644 --- a/src/main/java/org/gbif/ipt/config/Constants.java +++ b/src/main/java/org/gbif/ipt/config/Constants.java @@ -127,6 +127,9 @@ public final class Constants { public static final Set GBIF_SUPPORTED_LICENSES; public static final Set GBIF_SUPPORTED_LICENSES_CODES; + public static final String EML_2_1_1_SCHEMA = "eml://ecoinformatics.org/eml-2.1.1"; + public static final String EML_2_2_0_SCHEMA = "https://eml.ecoinformatics.org/eml-2.2.0"; + static { Set licencesInternal = new HashSet<>(); licencesInternal.add("http://creativecommons.org/publicdomain/zero/1.0/legalcode"); diff --git a/src/main/java/org/gbif/ipt/model/Resource.java b/src/main/java/org/gbif/ipt/model/Resource.java index 6a07ebe93b..04d0eda09b 100644 --- a/src/main/java/org/gbif/ipt/model/Resource.java +++ b/src/main/java/org/gbif/ipt/model/Resource.java @@ -1464,7 +1464,12 @@ public String generateResourceCitation(@NotNull BigDecimal version, @NotNull URI // make list of verified agents (having first and last name) Set verifiedAuthorList = new LinkedHashSet<>(); - Stream.of(getEml().getCreators(), getEml().getMetadataProviders()) + + List suitableAssociatedParties = getEml().getAssociatedParties().stream() + .filter(p -> "originator".equalsIgnoreCase(p.getRole()) || "metadataProvider".equalsIgnoreCase(p.getRole())) + .collect(Collectors.toList()); + + Stream.of(getEml().getCreators(), getEml().getMetadataProviders(), suitableAssociatedParties) .flatMap(Collection::stream) .map(this::getCitationAgentName) .filter(StringUtils::isNotEmpty) diff --git a/src/main/java/org/gbif/ipt/model/voc/MetadataSection.java b/src/main/java/org/gbif/ipt/model/voc/MetadataSection.java index 540e281816..8b66a9736c 100644 --- a/src/main/java/org/gbif/ipt/model/voc/MetadataSection.java +++ b/src/main/java/org/gbif/ipt/model/voc/MetadataSection.java @@ -21,9 +21,12 @@ */ public enum MetadataSection { BASIC_SECTION("basic"), + CONTACTS_SECTION("contacts"), + ACKNOWLEDGEMENTS_SECTION("acknowledgements"), GEOGRAPHIC_COVERAGE_SECTION ("geocoverage"), TAXANOMIC_COVERAGE_SECTION ("taxcoverage"), TEMPORAL_COVERAGE_SECTION ("tempcoverage"), + ADDITIONAL_DESCRIPTION_SECTION("additionalDescription"), PROJECT_SECTION ("project"), METHODS_SECTION ("methods"), CITATIONS_SECTION ("citations"), diff --git a/src/main/java/org/gbif/ipt/service/manage/ResourceManager.java b/src/main/java/org/gbif/ipt/service/manage/ResourceManager.java index 8421cb3229..7283d5d215 100644 --- a/src/main/java/org/gbif/ipt/service/manage/ResourceManager.java +++ b/src/main/java/org/gbif/ipt/service/manage/ResourceManager.java @@ -43,6 +43,7 @@ import java.util.concurrent.ThreadPoolExecutor; import javax.annotation.Nullable; +import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.collections4.ListValuedMap; import org.xml.sax.SAXException; @@ -421,7 +422,7 @@ Resource create(String shortname, String type, File dwca, User creator, BaseActi * @param emlFile * @param validate */ - void replaceEml(Resource resource, File emlFile, boolean validate) throws SAXException, IOException, InvalidEmlException, ImportException; + void replaceEml(Resource resource, File emlFile, boolean validate) throws SAXException, ParserConfigurationException, IOException, InvalidEmlException, ImportException; /** * Replace the datapackage metadata file in a resource by the provided file diff --git a/src/main/java/org/gbif/ipt/service/manage/impl/ResourceManagerImpl.java b/src/main/java/org/gbif/ipt/service/manage/impl/ResourceManagerImpl.java index e7b21046e5..15b38e21c0 100644 --- a/src/main/java/org/gbif/ipt/service/manage/impl/ResourceManagerImpl.java +++ b/src/main/java/org/gbif/ipt/service/manage/impl/ResourceManagerImpl.java @@ -54,7 +54,6 @@ import org.gbif.ipt.model.InferredEmlMetadata; import org.gbif.ipt.model.InferredEmlTaxonomicCoverage; import org.gbif.ipt.model.InferredEmlTemporalCoverage; -import org.gbif.ipt.model.InferredMetadata; import org.gbif.ipt.model.Ipt; import org.gbif.ipt.model.Organisation; import org.gbif.ipt.model.PropertyMapping; @@ -176,6 +175,9 @@ import javax.annotation.Nullable; import javax.validation.constraints.NotNull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListValuedMap; @@ -197,9 +199,12 @@ import static org.gbif.ipt.config.Constants.CAMTRAP_DP; import static org.gbif.ipt.config.Constants.CAMTRAP_DP_OBSERVATIONS; import static org.gbif.ipt.config.Constants.COL_DP; +import static org.gbif.ipt.config.Constants.EML_2_1_1_SCHEMA; +import static org.gbif.ipt.config.Constants.EML_2_2_0_SCHEMA; import static org.gbif.ipt.config.DataDir.COL_DP_METADATA_FILENAME; import static org.gbif.ipt.config.DataDir.EML_XML_FILENAME; import static org.gbif.ipt.config.DataDir.FRICTIONLESS_METADATA_FILENAME; +import static org.gbif.ipt.model.Resource.CoreRowType.METADATA; import static org.gbif.ipt.utils.FileUtils.getFileExtension; import static org.gbif.ipt.utils.MetadataUtils.metadataClassForType; @@ -425,14 +430,7 @@ private Eml convertMetadataToEml(Dataset metadata) { eml.setTitle(metadata.getTitle()); if (metadata.getDescription() != null) { - // split description into paragraphs - List paragraphs = Arrays.stream(metadata.getDescription().split("\r?\n")) - .map(org.gbif.utils.text.StringUtils::trim) - .filter(StringUtils::isNotEmpty) - .collect(Collectors.toList()); - for (String para : paragraphs) { - eml.addDescriptionPara(para); - } + eml.setDescription(metadata.getDescription()); } if (metadata.getHomepage() != null) { @@ -459,10 +457,36 @@ private Eml convertMetadataToEml(Dataset metadata) { * @throws IOException if failed to read EML file * @throws InvalidEmlException if EML is invalid */ - private void validateEmlFile(File emlFile) throws SAXException, IOException, InvalidEmlException { - EmlValidator emlValidator = EmlValidator.newValidator(EMLProfileVersion.GBIF_1_2); - String emlString = FileUtils.readFileToString(emlFile, StandardCharsets.UTF_8); - emlValidator.validate(emlString); + private void validateEmlFile(File emlFile) + throws SAXException, ParserConfigurationException, IOException, InvalidEmlException { + EMLProfileVersion emlProfileVersion = getEmlProfileVersion(emlFile); + EmlValidator emlValidator = EmlValidator.newValidator(emlProfileVersion); + String emlString = FileUtils.readFileToString(emlFile, StandardCharsets.UTF_8); + emlValidator.validate(emlString); + } + + private EMLProfileVersion getEmlProfileVersion(File emlFile) + throws SAXException, ParserConfigurationException, IOException, InvalidEmlException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + org.w3c.dom.Document document = builder.parse(emlFile); + + String emlNamespace = document.getDocumentElement().getNamespaceURI(); + + EMLProfileVersion emlProfileVersion; + if (EML_2_1_1_SCHEMA.equals(emlNamespace)) { + LOG.debug("Use GBIF metadata profile 1.2 for validation"); + emlProfileVersion = EMLProfileVersion.GBIF_1_2; + } else if (EML_2_2_0_SCHEMA.equals(emlNamespace)) { + LOG.debug("Use GBIF metadata profile 1.3 for validation"); + emlProfileVersion = EMLProfileVersion.GBIF_1_3; + } else { + LOG.error("Unsupported EML version or unrecognized namespace."); + throw new InvalidEmlException("Unsupported EML version or unrecognized namespace."); + } + + return emlProfileVersion; } private void validateDatapackageMetadataFile(BaseAction action, File metadataFile, Class metadataClass) throws IOException, org.gbif.ipt.service.InvalidMetadataException { @@ -987,7 +1011,7 @@ private Resource createFromDwcArchive(String shortname, File dwca, User creator, * Validation is optional. */ @Override - public void replaceEml(Resource resource, File emlFile, boolean validate) throws SAXException, IOException, InvalidEmlException, ImportException { + public void replaceEml(Resource resource, File emlFile, boolean validate) throws SAXException, ParserConfigurationException, IOException, InvalidEmlException, ImportException { if (validate) { validateEmlFile(emlFile); } @@ -3060,7 +3084,24 @@ private void publishEml(Resource resource, BigDecimal version) throws Publicatio // create versioned eml file File trunkFile = dataDir.resourceEmlFile(resource.getShortname()); + + // validate EML (only for metadata-only resources, otherwise it will be validated afterward) + if (METADATA.toString().equalsIgnoreCase(resource.getCoreType())) { + try { + EmlValidator emlValidator = org.gbif.metadata.eml.EmlValidator.newValidator(EMLProfileVersion.GBIF_1_3); + String emlString = FileUtils.readFileToString(trunkFile, StandardCharsets.UTF_8); + emlValidator.validate(emlString); + } catch (IOException | SAXException e) { + throw new PublicationException(PublicationException.TYPE.EML, + "Can't publish eml file for resource " + resource.getShortname() + ". Failed to validate EML", e); + } catch (InvalidEmlException e) { + throw new PublicationException(PublicationException.TYPE.EML, + "Can't publish eml file for resource " + resource.getShortname() + ". Invalid EML", e); + } + } + File versionedFile = dataDir.resourceEmlFile(resource.getShortname(), version); + try { FileUtils.copyFile(trunkFile, versionedFile); } catch (IOException e) { diff --git a/src/main/java/org/gbif/ipt/service/manage/impl/ResourceMetadataInferringServiceImpl.java b/src/main/java/org/gbif/ipt/service/manage/impl/ResourceMetadataInferringServiceImpl.java index b96e3e291f..3161a5daac 100644 --- a/src/main/java/org/gbif/ipt/service/manage/impl/ResourceMetadataInferringServiceImpl.java +++ b/src/main/java/org/gbif/ipt/service/manage/impl/ResourceMetadataInferringServiceImpl.java @@ -192,8 +192,8 @@ private void finalizeInferredMetadata(InferredEmlMetadata metadata, InferredEmlT if (!errorOccurredWhileProcessingGeographicMetadata) { TemporalCoverage tempCoverage = new TemporalCoverage(); try { - tempCoverage.setStart(params.startDateStr); - tempCoverage.setEnd(params.endDateStr); + tempCoverage.setStart(params.startDateTA.toString()); + tempCoverage.setEnd(params.endDateTA.toString()); inferredTemporalMetadata.setInferred(true); inferredTemporalMetadata.setData(tempCoverage); } catch (ParseException e) { @@ -1219,8 +1219,8 @@ private void finalizeInferredMetadata(InferredCamtrapMetadata metadata, Inferred if (!errorOccurredWhileProcessingTemporalMetadata) { try { - inferredTemporalScope.setStartDate(DateUtils.calendarDate(params.startDateStr)); - inferredTemporalScope.setEndDate(DateUtils.calendarDate(params.endDateStr)); + inferredTemporalScope.setStartDate(DateUtils.calendarDate(params.startDateTA.toString())); + inferredTemporalScope.setEndDate(DateUtils.calendarDate(params.endDateTA.toString())); inferredTemporalScope.setInferred(true); } catch (ParseException e) { LOG.error("Failed to parse date for temporal coverage", e); diff --git a/src/main/java/org/gbif/ipt/service/registry/impl/RegistryManagerImpl.java b/src/main/java/org/gbif/ipt/service/registry/impl/RegistryManagerImpl.java index c41605ebce..fb7ff902c3 100644 --- a/src/main/java/org/gbif/ipt/service/registry/impl/RegistryManagerImpl.java +++ b/src/main/java/org/gbif/ipt/service/registry/impl/RegistryManagerImpl.java @@ -59,6 +59,7 @@ import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; @@ -164,7 +165,7 @@ private List buildRegistryParameters(Resource resource) { // if primaryContact is null, use resource creator as primary contact. if (primaryContact == null) { primaryContact = new Agent(); - primaryContact.setEmail(resource.getCreator().getEmail()); + primaryContact.setEmail(Collections.singletonList(resource.getCreator().getEmail())); primaryContact.setFirstName(resource.getCreator().getFirstname()); primaryContact.setLastName(resource.getCreator().getLastname()); primaryContact.setRole(null); @@ -175,12 +176,12 @@ private List buildRegistryParameters(Resource resource) { primaryContact.setRole(null); data.add(new BasicNameValuePair("primaryContactType", primaryContactType)); - data.add(new BasicNameValuePair("primaryContactEmail", StringUtils.trimToEmpty(primaryContact.getEmail()))); + data.add(new BasicNameValuePair("primaryContactEmail", !primaryContact.getEmail().isEmpty() ? StringUtils.trimToEmpty(primaryContact.getEmail().get(0)) : "")); data.add(new BasicNameValuePair("primaryContactName", StringUtils.trimToNull(StringUtils.trimToEmpty(primaryContact.getFullName())))); data.add(new BasicNameValuePair("primaryContactAddress", StringUtils.trimToEmpty(primaryContact.getAddress().toFormattedString()))); - data.add(new BasicNameValuePair("primaryContactPhone", StringUtils.trimToEmpty(primaryContact.getPhone()))); + data.add(new BasicNameValuePair("primaryContactPhone", !primaryContact.getPhone().isEmpty() ? StringUtils.trimToEmpty(primaryContact.getPhone().get(0)) : "")); // see if we have a published dwca or if its only metadata RegistryServices services = buildServiceTypeParams(resource); diff --git a/src/main/java/org/gbif/ipt/task/Eml2Rtf.java b/src/main/java/org/gbif/ipt/task/Eml2Rtf.java index 26371e71df..6d6c726ee7 100644 --- a/src/main/java/org/gbif/ipt/task/Eml2Rtf.java +++ b/src/main/java/org/gbif/ipt/task/Eml2Rtf.java @@ -103,12 +103,8 @@ private void addAbstract(Document doc, Eml eml) throws DocumentException { p.add(new Phrase(getText("rtf.abstract"), fontTitle)); p.add(Chunk.NEWLINE); p.add(Chunk.NEWLINE); - for (String para : eml.getDescription()) { - if (StringUtils.isNotBlank(para)) { - p.add(para.replace("\r\n", "\n")); - p.add(Chunk.NEWLINE); - } - } + p.add(eml.getDescription()); + p.add(Chunk.NEWLINE); doc.add(p); p.clear(); } @@ -290,8 +286,8 @@ private void addAuthors(Document doc, Eml eml) throws DocumentException { p.add(creator.getFirstName() + " "); } p.add(creator.getLastName()); - if (StringUtils.isNotBlank(creator.getEmail())) { - p.add(" (" + creator.getEmail() + ")"); + if (!creator.getEmail().isEmpty()) { + p.add(" (" + creator.getEmail().get(0) + ")"); } isFirst = false; } @@ -312,8 +308,8 @@ private void addAuthors(Document doc, Eml eml) throws DocumentException { p.add(metadataProvider.getFirstName() + " "); } p.add(metadataProvider.getLastName()); - if (StringUtils.isNotBlank(metadataProvider.getEmail())) { - p.add(" (" + metadataProvider.getEmail() + ")"); + if (!metadataProvider.getEmail().isEmpty()) { + p.add(" (" + metadataProvider.getEmail().get(0) + ")"); } isFirst = false; } diff --git a/src/main/java/org/gbif/ipt/task/GenerateDCAT.java b/src/main/java/org/gbif/ipt/task/GenerateDCAT.java index c4a98fe6b8..f5f9acacc8 100644 --- a/src/main/java/org/gbif/ipt/task/GenerateDCAT.java +++ b/src/main/java/org/gbif/ipt/task/GenerateDCAT.java @@ -533,21 +533,9 @@ protected String createDCATDatasetInformation(Resource resource) { } //dct:description - if (!eml.getDescription().isEmpty()) { + if (eml.getDescription() != null) { addPredicateToBuilder(datasetBuilder, "dct:description"); - StringBuilder description = new StringBuilder(); - Iterator iter = eml.getDescription().iterator(); - while (iter.hasNext()) { - String des = StringUtils.trimToNull(iter.next()); - if (des != null) { - description.append(des); - } - // turtle format requires line breaks to be escaped - if (iter.hasNext()) { - description.append("\\n"); - } - } - addObjectToBuilder(datasetBuilder, description.toString(), ObjectTypes.LITERAL); + addObjectToBuilder(datasetBuilder, eml.getDescription(), ObjectTypes.LITERAL); } //dcat:keyword @@ -576,8 +564,8 @@ protected String createDCATDatasetInformation(Resource resource) { for (Agent contact : eml.getContacts()) { addPredicateToBuilder(datasetBuilder, "dcat:contactPoint"); String agent = " a vcard:Individual ; vcard:fn \"" + contact.getFullName() + "\""; - if (contact.getEmail() != null) { - agent += "; vcard:hasEmail "; + if (contact.getEmail() != null && !contact.getEmail().isEmpty()) { + agent += "; vcard:hasEmail "; } addObjectToBuilder(datasetBuilder, agent, ObjectTypes.OBJECT); } diff --git a/src/main/java/org/gbif/ipt/task/GenerateDwca.java b/src/main/java/org/gbif/ipt/task/GenerateDwca.java index d9ddc1fe9a..54fe7fa14e 100644 --- a/src/main/java/org/gbif/ipt/task/GenerateDwca.java +++ b/src/main/java/org/gbif/ipt/task/GenerateDwca.java @@ -34,6 +34,9 @@ import org.gbif.ipt.service.admin.VocabulariesManager; import org.gbif.ipt.service.manage.SourceManager; import org.gbif.ipt.utils.MapUtils; +import org.gbif.metadata.eml.EMLProfileVersion; +import org.gbif.metadata.eml.EmlValidator; +import org.gbif.metadata.eml.InvalidEmlException; import org.gbif.utils.file.ClosableReportingIterator; import org.gbif.utils.file.CompressionUtil; import org.gbif.utils.file.csv.CSVReader; @@ -43,6 +46,7 @@ import java.io.File; import java.io.FileFilter; import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; @@ -75,6 +79,7 @@ import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; +import org.xml.sax.SAXException; public class GenerateDwca extends ReportingTask implements Callable> { @@ -297,6 +302,30 @@ private void writeHeaderLine(List propertyList, int totalColu private void addEmlFile() throws GeneratorException, InterruptedException { checkForInterruption(); setState(STATE.METADATA); + + // validate EML + try { + addMessage(Level.INFO, "? Validating EML file"); + EmlValidator emlValidator = org.gbif.metadata.eml.EmlValidator.newValidator(EMLProfileVersion.GBIF_1_3); + + try (InputStream is = FileUtils.openInputStream(dataDir.resourceEmlFile(resource.getShortname()))) { + emlValidator.validate(is); + addMessage(Level.INFO, "✓ Validated EML file"); + } + } catch (IOException | SAXException e) { + // some error validating this file, report + log.error("Exception caught while validating EML file", e); + addMessage(Level.ERROR, "Failed to validate EML file"); + setState(e); + throw new GeneratorException("Problem occurred while validating DwC-A (EML)", e); + } catch (InvalidEmlException e) { + // InvalidEmlException + log.error("Invalid EML", e); + addMessage(Level.ERROR, "Invalid EML file: " + e.getMessage()); + setState(e); + throw new GeneratorException("Invalid EML", e); + } + try { FileUtils.copyFile(dataDir.resourceEmlFile(resource.getShortname()), new File(dwcaFolder, DataDir.EML_XML_FILENAME)); @@ -386,6 +415,7 @@ private void validate() throws GeneratorException, InterruptedException { try { // retrieve newly generated archive - decompressed Archive arch = DwcFiles.fromLocation(dwcaFolder.toPath()); + // populate basisOfRecord lookup HashMap loadBasisOfRecordMapFromVocabulary(); // perform validation on core file (includes core ID and basisOfRecord validation) @@ -402,7 +432,7 @@ private void validate() throws GeneratorException, InterruptedException { throw new GeneratorException("Problem occurred while validating DwC-A", e); } // final reporting - addMessage(Level.INFO, "Archive validated"); + addMessage(Level.INFO, "✓ Archive validated"); } /** diff --git a/src/main/java/org/gbif/ipt/utils/DataCiteMetadataBuilder.java b/src/main/java/org/gbif/ipt/utils/DataCiteMetadataBuilder.java index d9570eaf4b..97bbfaf891 100644 --- a/src/main/java/org/gbif/ipt/utils/DataCiteMetadataBuilder.java +++ b/src/main/java/org/gbif/ipt/utils/DataCiteMetadataBuilder.java @@ -215,17 +215,14 @@ protected static DataCiteMetadata.Sizes getSizes(Resource resource) { */ protected static DataCiteMetadata.Descriptions getDescriptions(Eml eml) { DataCiteMetadata.Descriptions descriptions = FACTORY.createDataCiteMetadataDescriptions(); - if (!eml.getDescription().isEmpty()) { - for (String para : eml.getDescription()) { - if (StringUtils.isNotBlank(para)) { - DataCiteMetadata.Descriptions.Description description = FACTORY.createDataCiteMetadataDescriptionsDescription(); - description.setDescriptionType(DescriptionType.ABSTRACT); - description.setLang(eml.getMetadataLanguage()); - description.getContent().add(para); - descriptions.getDescription().add(description); - } - } + if (eml.getDescription() != null) { + DataCiteMetadata.Descriptions.Description description = FACTORY.createDataCiteMetadataDescriptionsDescription(); + description.setDescriptionType(DescriptionType.ABSTRACT); + description.setLang(eml.getMetadataLanguage()); + description.getContent().add(eml.getDescription()); + descriptions.getDescription().add(description); } + return descriptions; } @@ -532,9 +529,9 @@ else if (StringUtils.isNotBlank(agent.getOrganisation())) { contributor.setContributorName(contributorName); } // 3. try position name - else if (StringUtils.isNotBlank(agent.getPosition())) { + else if (!agent.getPosition().isEmpty() && StringUtils.isNotBlank(agent.getPosition().get(0))) { final DataCiteMetadata.Contributors.Contributor.ContributorName contributorName = FACTORY.createDataCiteMetadataContributorsContributorContributorName(); - contributorName.setValue(agent.getPosition()); + contributorName.setValue(agent.getPosition().get(0)); contributor.setContributorName(contributorName); // affiliation is optional if (StringUtils.isNotBlank(agent.getOrganisation())) { diff --git a/src/main/java/org/gbif/ipt/validation/AgentValidator.java b/src/main/java/org/gbif/ipt/validation/AgentValidator.java index 1cf70fc3a3..c2abd44435 100644 --- a/src/main/java/org/gbif/ipt/validation/AgentValidator.java +++ b/src/main/java/org/gbif/ipt/validation/AgentValidator.java @@ -26,8 +26,8 @@ public class AgentValidator extends BaseValidator { * @return true if name (at least lastname) and email exist. Otherwise, return false. */ public static boolean hasCompleteContactInfo(Agent agent) { - return agent != null && agent.getFullName() != null && !(agent.getFullName().length() == 0) - && agent.getEmail() != null && !(agent.getEmail().length() == 0); + return agent != null && agent.getFullName() != null && !(agent.getFullName().isEmpty()) + && agent.getEmail() != null && !agent.getEmail().isEmpty() && !agent.getEmail().get(0).isEmpty(); } } diff --git a/src/main/java/org/gbif/ipt/validation/EmlValidator.java b/src/main/java/org/gbif/ipt/validation/EmlValidator.java index 6cd95c937c..866a61f07b 100644 --- a/src/main/java/org/gbif/ipt/validation/EmlValidator.java +++ b/src/main/java/org/gbif/ipt/validation/EmlValidator.java @@ -20,6 +20,9 @@ import org.gbif.ipt.model.voc.MetadataSection; import org.gbif.ipt.service.admin.RegistrationManager; import org.gbif.ipt.struts2.SimpleTextProvider; +import org.gbif.metadata.eml.EMLProfileVersion; +import org.gbif.metadata.eml.InvalidEmlException; +import org.gbif.metadata.eml.ipt.IptEmlWriter; import org.gbif.metadata.eml.ipt.model.Address; import org.gbif.metadata.eml.ipt.model.Agent; import org.gbif.metadata.eml.ipt.model.BBox; @@ -34,6 +37,7 @@ import org.gbif.metadata.eml.ipt.model.PhysicalData; import org.gbif.metadata.eml.ipt.model.Point; import org.gbif.metadata.eml.ipt.model.Project; +import org.gbif.metadata.eml.ipt.model.ProjectAward; import org.gbif.metadata.eml.ipt.model.StudyAreaDescription; import org.gbif.metadata.eml.ipt.model.TaxonKeyword; import org.gbif.metadata.eml.ipt.model.TaxonomicCoverage; @@ -44,34 +48,68 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.regex.Pattern; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import com.google.inject.Inject; import static org.gbif.ipt.validation.EmailValidationMessageTranslator.EMAIL_ERROR_TRANSLATIONS; +import static org.gbif.metadata.eml.EmlValidator.newValidator; public class EmlValidator extends BaseValidator { + private static final Logger LOG = LogManager.getLogger(EmlValidator.class); + + // Regular expression to match the pattern: error name, line number, column number, and optionally any technical codes like 'cvc-complex-type.2.3'. + private static final String EML_VALIDATION_ERROR_PATTERN = "^[^;]*;\\s*lineNumber:\\s*\\d+;\\s*columnNumber:\\s*\\d+;\\s*(?:[^:]*:\\s*)?"; protected static Pattern phonePattern = Pattern.compile("[\\w ()/+-\\.]+"); private AppConfig cfg; private RegistrationManager regManager; private SimpleTextProvider simpleTextProvider; + private org.gbif.metadata.eml.EmlValidator emlProfileValidator; + + private static final Map TAG_REPLACEMENTS; + + static { + TAG_REPLACEMENTS = new HashMap<>(); + TAG_REPLACEMENTS.put("title", "h"); + TAG_REPLACEMENTS.put("section", "div"); + TAG_REPLACEMENTS.put("para", "p"); + TAG_REPLACEMENTS.put("value", "value"); // no change + TAG_REPLACEMENTS.put("itemizedlist", "ul"); + TAG_REPLACEMENTS.put("orderedlist", "ol"); + TAG_REPLACEMENTS.put("emphasis", "b"); + TAG_REPLACEMENTS.put("subscript", "sub"); + TAG_REPLACEMENTS.put("superscript", "sup"); + TAG_REPLACEMENTS.put("literalLayout", "pre"); + TAG_REPLACEMENTS.put("ulink", "a"); + } @Inject public EmlValidator(AppConfig cfg, RegistrationManager registrationManager, SimpleTextProvider simpleTextProvider) { this.cfg = cfg; this.regManager = registrationManager; this.simpleTextProvider = simpleTextProvider; + try { + this.emlProfileValidator = newValidator(EMLProfileVersion.GBIF_1_3); + } catch (Exception e) { + LOG.error("Failed to initialize EML Profile Validator", e); + } } /** * Returns a formatted URL string, prefixing it with a default scheme component if its not an absolute URL. - * + * * @return the URL always having a scheme component, or null if incoming URL string was null or empty */ public static String formatURL(String url) { @@ -94,7 +132,7 @@ public static String formatURL(String url) { /** * Checks if the incoming string representing a URL, is in fact a well-formed URI. - * + * * @return true if the string is a well-formed URI */ public static boolean isWellFormedURI(String url) { @@ -132,7 +170,7 @@ public boolean isValid(Resource resource, @Nullable MetadataSection section) { /** * Validate if all metadata sections are valid. For the first section encountered that doesn't validate, an * error message will appear for that section only. - * + * * @param action Action * @param resource resource * @return whether all sections validated or not @@ -150,11 +188,22 @@ public boolean areAllSectionsValid(BaseAction action, Resource resource) { return !problemsEncountered; } + public boolean isSectionValid(BaseAction action, Resource resource, MetadataSection section) { + validate(action, resource, section); + + if (action.hasActionErrors() || action.hasFieldErrors()) { + action.addActionError(action.getText("manage.failed", new String[] {action.getText("submenu." + section.getName())})); + return false; + } + + return true; + } + /** * Validate an EML document, optionally only a part of it. *
* For each section, validation only proceeds if at least one field in the section's form has been entered. - * + * * @param action BaseAction * @param resource resource * @param section EML document section name @@ -184,19 +233,32 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec action.addActionWarning(action.getText("eml.title.shortname.match")); } - // description - mandatory and greater than 5 chars - if (eml.getDescription().isEmpty()) { - action - .addActionError(action.getText("validation.required", new String[] {action.getText("eml.description")})); + String strippedDescription = Optional.ofNullable(eml.getDescription()) + .map(d -> d.replaceAll("<[^>]*>", "")) // get rid of tags + .map(String::trim) + .orElse(""); + + // description - mandatory + if (StringUtils.isEmpty(strippedDescription)) { + action.addActionError( + action.getText("validation.required", new String[] {action.getText("eml.description")})); + } else if (!exists(strippedDescription, 5)) { + // ensure description is longer than min length + action.addActionError( + action.getText("validation.short", new String[] {action.getText("eml.description"), "5"})); + } else if (emlProfileValidator == null) { + action.addActionError(action.getText("validation.cannnot.be.performed")); } else { - // ensure each description is longer than min length - int index = 0; - for (String d : eml.getDescription()) { - if (!exists(d, 5)) { - action.addFieldError("eml.description[" + index + "]", - action.getText("validation.short", new String[] {action.getText("eml.description"), "5"})); - } - index++; + try { + Eml stubValidationEml = getStubEml(); + stubValidationEml.setDescription(eml.getDescription()); + String emlString = IptEmlWriter.writeEmlAsString(stubValidationEml); + emlProfileValidator.validate(emlString); + } catch (InvalidEmlException e) { + action.addActionError(action.getText("validation.invalid.ext", new String[] {action.getText("eml.description"), simplifyDocBookValidationErrorMessage(e.getMessage())})); + } catch (Exception e) { + action.addActionError(action.getText("validation.failed.see.logs", new String[] {action.getText("eml.description")})); + LOG.error("Failed to validate description", e); } } @@ -237,16 +299,19 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec } // update frequency - mandatory (defaults to Unknown) - if (eml.getUpdateFrequency()==null) { + if (eml.getUpdateFrequency() == null) { if (resource.getUpdateFrequency() != null) { eml.setUpdateFrequency(resource.getUpdateFrequency().getIdentifier()); action.addActionWarning(action.getText("eml.updateFrequency.default.interval", new String[] {resource.getUpdateFrequency().getIdentifier()})); } else { action.addActionWarning(action.getText("eml.updateFrequency.default")); - eml.setUpdateFrequency(MaintenanceUpdateFrequency.UNKOWN.getIdentifier()); + eml.setUpdateFrequency(MaintenanceUpdateFrequency.UNKNOWN.getIdentifier()); } } + break; + + case CONTACTS_SECTION: // Contacts list: at least one field has to have had data entered into it to qualify for validation if (isAgentsListEmpty(eml.getContacts())) { action.addActionError(action.getText("eml.contact.required")); @@ -257,60 +322,71 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec // firstName - optional. But if firstName exists, lastName have to exist if (exists(c.getFirstName()) && !exists(c.getLastName())) { action.addFieldError("eml.contacts[" + index + "].lastName", - action.getText("validation.firstname.lastname")); + action.getText("validation.firstname.lastname")); } // directory and personnel id both required (if either is supplied) if (!c.getUserIds().isEmpty()) { if (exists(c.getUserIds().get(0).getDirectory()) && !exists(c.getUserIds().get(0).getIdentifier())) { action.addFieldError("eml.contacts[" + index + "].userIds[0].identifier", - action.getText("validation.personnel")); + action.getText("validation.personnel")); } else if (!exists(c.getUserIds().get(0).getDirectory()) && exists(c.getUserIds().get(0).getIdentifier())) { action.addFieldError("eml.contacts[" + index + "].userIds[0].directory", - action.getText("validation.directory")); + action.getText("validation.directory")); } } // At least one of organisation, position, or a lastName have to exist - if (!exists(c.getOrganisation()) && !exists(c.getLastName()) && !exists(c.getPosition())) { + if (!exists(c.getOrganisation()) && !exists(c.getLastName()) && (c.getPosition().isEmpty() || !exists(c.getPosition().get(0)))) { action.addActionError(action.getText("validation.lastname.organisation.position")); action.addFieldError("eml.contacts[" + index + "].organisation", action - .getText("validation.required", new String[] {action.getText("eml.contact.organisation")})); + .getText("validation.required", new String[] {action.getText("eml.contact.organisation")})); action.addFieldError("eml.contacts[" + index + "].lastName", - action.getText("validation.required", new String[] {action.getText("eml.contact.lastName")})); + action.getText("validation.required", new String[] {action.getText("eml.contact.lastName")})); action.addFieldError("eml.contacts[" + index + "].position", - action.getText("validation.required", new String[] {action.getText("eml.contact.position")})); + action.getText("validation.required", new String[] {action.getText("eml.contact.position")})); } - /* email is optional. But if it exists, should be a valid email address */ - ValidationResult emailValidationResult = checkEmailValid(c.getEmail()); - if (exists(c.getEmail()) && !emailValidationResult.isValid()) { - action.addFieldError( - "eml.contacts[" + index + "].email", + /* email(s) are optional. But if they exist, they should be valid email addresses */ + ValidationResult emailValidationResult; + if (!c.getEmail().isEmpty()) { + for (int emailIndex = 0; emailIndex < c.getEmail().size(); emailIndex++) { + emailValidationResult = checkEmailValid(c.getEmail().get(emailIndex)); + if (!emailValidationResult.isValid()) { + action.addFieldError( + "eml.contacts[" + index + "].email[" + emailIndex + "]", action.getText(EMAIL_ERROR_TRANSLATIONS.getOrDefault(emailValidationResult.getMessage(), "validation.email.invalid")) - ); + ); + } + } } - /* phone is optional. But if it exists, should match the pattern */ - if (exists(c.getPhone()) && !isValidPhoneNumber(c.getPhone())) { - action.addFieldError("eml.contacts[" + index + "].phone", - action.getText("validation.invalid", new String[] {action.getText("eml.contact.phone")})); + /* phone(s) are optional. But if they exist, should match the pattern */ + if (!c.getPhone().isEmpty()) { + for (int phoneIndex = 0; phoneIndex < c.getPhone().size(); phoneIndex++) { + if (!isValidPhoneNumber(c.getPhone().get(phoneIndex))) { + action.addFieldError("eml.contacts[" + index + "].phone[" + phoneIndex + "]", + action.getText("validation.invalid", new String[] {action.getText("eml.contact.phone")})); + } + } } - /* Validate the homepage URL from each contact */ - if (c.getHomepage() != null) { - if (formatURL(c.getHomepage()) == null) { - action.addFieldError("eml.contacts[" + index + "].homepage", - action.getText("validation.invalid", - new String[] {action.getText("eml.contact.homepage")})); - } else { - c.setHomepage(formatURL(c.getHomepage())); + /* Validate the homepage URL from each contact */ + if (!c.getHomepage().isEmpty()) { + for (int homepageIndex = 0; homepageIndex < c.getHomepage().size(); homepageIndex++) { + if (formatURL(c.getHomepage().get(homepageIndex)) == null) { + action.addFieldError("eml.contacts[" + index + "].homepage[" + homepageIndex + "]", + action.getText("validation.invalid", + new String[] {action.getText("eml.contact.homepage")})); + } else { + c.getHomepage().set(homepageIndex, formatURL(c.getHomepage().get(homepageIndex))); + } } } } } - // Creators list: at least one contact is required, and + // Creators' list: at least one contact is required, and // at least one field has to have had data entered into it to qualify for validation if (isAgentsListEmpty(eml.getCreators())) { action.addActionError(action.getText("eml.resourceCreator.required")); @@ -321,54 +397,65 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec // firstName - optional. But if firstName exists, lastName have to exist if (exists(c.getFirstName()) && !exists(c.getLastName())) { action.addFieldError("eml.creators[" + index + "].lastName", - action.getText("validation.firstname.lastname")); + action.getText("validation.firstname.lastname")); } // directory and personnel id both required (if either is supplied) if (!c.getUserIds().isEmpty()) { if (exists(c.getUserIds().get(0).getDirectory()) && !exists(c.getUserIds().get(0).getIdentifier())) { action.addFieldError("eml.creators[" + index + "].userIds[0].identifier", - action.getText("validation.personnel")); + action.getText("validation.personnel")); } else if (!exists(c.getUserIds().get(0).getDirectory()) && exists(c.getUserIds().get(0).getIdentifier())) { action.addFieldError("eml.creators[" + index + "].userIds[0].directory", - action.getText("validation.directory")); + action.getText("validation.directory")); } } // At least one of organisation, position, or a lastName have to exist - if (!exists(c.getOrganisation()) && !exists(c.getLastName()) && !exists(c.getPosition())) { + if (!exists(c.getOrganisation()) && !exists(c.getLastName()) && (c.getPosition().isEmpty() || !exists(c.getPosition().get(0)))) { action.addActionError(action.getText("validation.lastname.organisation.position")); action.addFieldError("eml.creators[" + index + "].organisation", action - .getText("validation.required", new String[] {action.getText("eml.resourceCreator.organisation")})); + .getText("validation.required", new String[] {action.getText("eml.resourceCreator.organisation")})); action.addFieldError("eml.creators[" + index + "].lastName", - action.getText("validation.required", new String[] {action.getText("eml.resourceCreator.lastName")})); + action.getText("validation.required", new String[] {action.getText("eml.resourceCreator.lastName")})); action.addFieldError("eml.creators[" + index + "].position", - action.getText("validation.required", new String[] {action.getText("eml.resourceCreator.position")})); + action.getText("validation.required", new String[] {action.getText("eml.resourceCreator.position")})); } - /* email is optional. But if it exists, should be a valid email address */ - ValidationResult emailValidationResult = checkEmailValid(c.getEmail()); - if (exists(c.getEmail()) && !emailValidationResult.isValid()) { - action.addFieldError( - "eml.creators[" + index + "].email", + /* email(s) are optional. But if they exist, they should be valid email addresses */ + ValidationResult emailValidationResult; + if (!c.getEmail().isEmpty()) { + for (int emailIndex = 0; emailIndex < c.getEmail().size(); emailIndex++) { + emailValidationResult = checkEmailValid(c.getEmail().get(emailIndex)); + if (!emailValidationResult.isValid()) { + action.addFieldError( + "eml.creators[" + index + "].email[" + emailIndex + "]", action.getText(EMAIL_ERROR_TRANSLATIONS.getOrDefault(emailValidationResult.getMessage(), "validation.email.invalid")) - ); + ); + } + } } - /* phone is optional. But if it exists, should match the pattern */ - if (exists(c.getPhone()) && !isValidPhoneNumber(c.getPhone())) { - action.addFieldError("eml.creators[" + index + "].phone", - action.getText("validation.invalid", new String[] {action.getText("eml.resourceCreator.phone")})); + /* phone(s) are optional. But if they exist, should match the pattern */ + if (!c.getPhone().isEmpty()) { + for (int phoneIndex = 0; phoneIndex < c.getPhone().size(); phoneIndex++) { + if (!isValidPhoneNumber(c.getPhone().get(phoneIndex))) { + action.addFieldError("eml.creators[" + index + "].phone[" + phoneIndex + "]", + action.getText("validation.invalid", new String[] {action.getText("eml.resourceCreator.phone")})); + } + } } - /* Validate the homepage URL from each contact */ - if (c.getHomepage() != null) { - if (formatURL(c.getHomepage()) == null) { - action.addFieldError("eml.creators[" + index + "].homepage", - action.getText("validation.invalid", - new String[] {action.getText("eml.resourceCreator.homepage")})); - } else { - c.setHomepage(formatURL(c.getHomepage())); + /* Validate the homepage URL from each creator */ + if (!c.getHomepage().isEmpty()) { + for (int homepageIndex = 0; homepageIndex < c.getHomepage().size(); homepageIndex++) { + if (formatURL(c.getHomepage().get(homepageIndex)) == null) { + action.addFieldError("eml.creators[" + index + "].homepage[" + homepageIndex + "]", + action.getText("validation.invalid", + new String[] {action.getText("eml.resourceCreator.homepage")})); + } else { + c.getHomepage().set(homepageIndex, formatURL(c.getHomepage().get(homepageIndex))); + } } } } @@ -382,58 +469,89 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec // firstName - optional. But if firstName exists, lastName have to exist if (exists(c.getFirstName()) && !exists(c.getLastName())) { action.addFieldError("eml.metadataProviders[" + index + "].lastName", - action.getText("validation.firstname.lastname")); + action.getText("validation.firstname.lastname")); } // directory and personnel id both required (if either is supplied) if (!c.getUserIds().isEmpty()) { if (exists(c.getUserIds().get(0).getDirectory()) && !exists(c.getUserIds().get(0).getIdentifier())) { action.addFieldError("eml.metadataProviders[" + index + "].userIds[0].identifier", - action.getText("validation.personnel")); + action.getText("validation.personnel")); } else if (!exists(c.getUserIds().get(0).getDirectory()) && exists(c.getUserIds().get(0).getIdentifier())) { action.addFieldError("eml.metadataProviders[" + index + "].userIds[0].directory", - action.getText("validation.directory")); + action.getText("validation.directory")); } } // At least one of organisation, position, or a lastName have to exist - if (!exists(c.getOrganisation()) && !exists(c.getLastName()) && !exists(c.getPosition())) { + if (!exists(c.getOrganisation()) && !exists(c.getLastName()) && (c.getPosition().isEmpty() || !exists(c.getPosition().get(0)))) { action.addActionError(action.getText("validation.lastname.organisation.position")); action.addFieldError("eml.metadataProviders[" + index + "].organisation", action - .getText("validation.required", new String[] {action.getText("eml.metadataProvider.organisation")})); + .getText("validation.required", new String[] {action.getText("eml.metadataProvider.organisation")})); action.addFieldError("eml.metadataProviders[" + index + "].lastName", - action.getText("validation.required", new String[] {action.getText("eml.metadataProvider.lastName")})); + action.getText("validation.required", new String[] {action.getText("eml.metadataProvider.lastName")})); action.addFieldError("eml.metadataProviders[" + index + "].position", - action.getText("validation.required", new String[] {action.getText("eml.metadataProvider.position")})); + action.getText("validation.required", new String[] {action.getText("eml.metadataProvider.position")})); } - /* email is optional. But if it exists, should be a valid email address */ - ValidationResult emailValidationResult = checkEmailValid(c.getEmail()); - if (exists(c.getEmail()) && !emailValidationResult.isValid()) { - action.addFieldError( - "eml.metadataProviders[" + index + "].email", + /* email(s) are optional. But if they exist, they should be valid email addresses */ + ValidationResult emailValidationResult; + if (!c.getEmail().isEmpty()) { + for (int emailIndex = 0; emailIndex < c.getEmail().size(); emailIndex++) { + emailValidationResult = checkEmailValid(c.getEmail().get(emailIndex)); + if (!emailValidationResult.isValid()) { + action.addFieldError( + "eml.metadataProviders[" + index + "].email[" + emailIndex + "]", action.getText(EMAIL_ERROR_TRANSLATIONS.getOrDefault(emailValidationResult.getMessage(), "validation.email.invalid")) - ); + ); + } + } } - /* phone is optional. But if it exists, should match the pattern */ - if (exists(c.getPhone()) && !isValidPhoneNumber(c.getPhone())) { - action.addFieldError("eml.metadataProviders[" + index + "].phone", - action.getText("validation.invalid", new String[] {action.getText("eml.metadataProvider.phone")})); + /* phone(s) are optional. But if they exist, should match the pattern */ + if (!c.getPhone().isEmpty()) { + for (int phoneIndex = 0; phoneIndex < c.getPhone().size(); phoneIndex++) { + if (!isValidPhoneNumber(c.getPhone().get(phoneIndex))) { + action.addFieldError("eml.metadataProviders[" + index + "].phone[" + phoneIndex + "]", + action.getText("validation.invalid", new String[] {action.getText("eml.metadataProvider.phone")})); + } + } } - /* Validate the homepage URL from each contact */ - if (c.getHomepage() != null) { - if (formatURL(c.getHomepage()) == null) { - action.addFieldError("eml.metadataProviders[" + index + "].homepage", - action.getText("validation.invalid", - new String[] {action.getText("eml.metadataProvider.homepage")})); - } else { - c.setHomepage(formatURL(c.getHomepage())); + /* Validate the homepage URL from each contact */ + if (!c.getHomepage().isEmpty()) { + for (int homepageIndex = 0; homepageIndex < c.getHomepage().size(); homepageIndex++) { + if (formatURL(c.getHomepage().get(homepageIndex)) == null) { + action.addFieldError("eml.metadataProviders[" + index + "].homepage[" + homepageIndex + "]", + action.getText("validation.invalid", + new String[] {action.getText("eml.metadataProvider.homepage")})); + } else { + c.getHomepage().set(homepageIndex, formatURL(c.getHomepage().get(homepageIndex))); + } } } } } + + break; + + case ACKNOWLEDGEMENTS_SECTION: + if (emlProfileValidator == null) { + action.addActionError(action.getText("validation.cannnot.be.performed")); + } else { + try { + Eml stubValidationEml = getStubEml(); + stubValidationEml.setAcknowledgements(eml.getAcknowledgements()); + String emlString = IptEmlWriter.writeEmlAsString(stubValidationEml); + emlProfileValidator.validate(emlString); + } catch (InvalidEmlException e) { + action.addActionError(action.getText("validation.invalid.ext", new String[] {action.getText("manage.metadata.acknowledgements"), simplifyDocBookValidationErrorMessage(e.getMessage())})); + } catch (Exception e) { + action.addActionError(action.getText("validation.failed.see.logs", new String[] {action.getText("manage.metadata.acknowledgements")})); + LOG.error("Failed to validate acknowledgements", e); + } + } + break; case GEOGRAPHIC_COVERAGE_SECTION: @@ -589,6 +707,48 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec break; + case ADDITIONAL_DESCRIPTION_SECTION: + if (emlProfileValidator == null) { + action.addActionError(action.getText("validation.cannnot.be.performed")); + } else { + try { + Eml stubValidationEml = getStubEml(); + stubValidationEml.setGettingStarted(eml.getPurpose()); + String emlString = IptEmlWriter.writeEmlAsString(stubValidationEml); + emlProfileValidator.validate(emlString); + } catch (InvalidEmlException e) { + action.addActionError(action.getText("validation.invalid.ext", new String[] {action.getText("eml.purpose"), simplifyDocBookValidationErrorMessage(e.getMessage())})); + } catch (Exception e) { + action.addActionError(action.getText("validation.failed.see.logs", new String[] {action.getText("eml.purpose")})); + LOG.error("Failed to validate purpose", e); + } + + try { + Eml stubValidationEml = getStubEml(); + stubValidationEml.setGettingStarted(eml.getGettingStarted()); + String emlString = IptEmlWriter.writeEmlAsString(stubValidationEml); + emlProfileValidator.validate(emlString); + } catch (InvalidEmlException e) { + action.addActionError(action.getText("validation.invalid.ext", new String[] {action.getText("manage.metadata.gettingStarted"), simplifyDocBookValidationErrorMessage(e.getMessage())})); + } catch (Exception e) { + action.addActionError(action.getText("validation.failed.see.logs", new String[] {action.getText("manage.metadata.gettingStarted")})); + LOG.error("Failed to validate getting started", e); + } + + try { + Eml stubValidationEml = getStubEml(); + stubValidationEml.setIntroduction(eml.getIntroduction()); + String emlString = IptEmlWriter.writeEmlAsString(stubValidationEml); + emlProfileValidator.validate(emlString); + } catch (InvalidEmlException e) { + action.addActionError(action.getText("validation.invalid.ext", new String[] {action.getText("manage.metadata.introduction"), simplifyDocBookValidationErrorMessage(e.getMessage())})); + } catch (Exception e) { + action.addActionError(action.getText("validation.failed.see.logs", new String[] {action.getText("manage.metadata.introduction")})); + LOG.error("Failed to validate introduction", e); + } + } + break; + case KEYWORDS_SECTION: // at least one field has to have had data entered into it to qualify for validation if (!isKeywordsPageEmpty(eml)) { @@ -632,7 +792,7 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec } // At least one of organisation, position, or a lastName have to exist - if (!exists(ap.getOrganisation()) && !exists(ap.getLastName()) && !exists(ap.getPosition())) { + if (!exists(ap.getOrganisation()) && !exists(ap.getLastName()) && (ap.getPosition().isEmpty() || !exists(ap.getPosition().get(0)))) { action.addActionError(action.getText("validation.lastname.organisation.position")); action.addFieldError("eml.associatedParties[" + index + "].organisation", action .getText("validation.required", new String[] {action.getText("eml.associatedParties.organisation")})); @@ -642,29 +802,40 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec action.getText("validation.required", new String[] {action.getText("eml.associatedParties.position")})); } - /* email is optional. But if it exists, should be a valid email address */ - ValidationResult emailValidationResult = checkEmailValid(ap.getEmail()); - if (exists(ap.getEmail()) && !emailValidationResult.isValid()) { - action.addFieldError( - "eml.associatedParties[" + index + "].email", + /* email(s) are optional. But if they exist, they should be valid email addresses */ + ValidationResult emailValidationResult; + if (!ap.getEmail().isEmpty()) { + for (int emailIndex = 0; emailIndex < ap.getEmail().size(); emailIndex++) { + emailValidationResult = checkEmailValid(ap.getEmail().get(emailIndex)); + if (!emailValidationResult.isValid()) { + action.addFieldError( + "eml.associatedParties[" + index + "].email[" + emailIndex + "]", action.getText(EMAIL_ERROR_TRANSLATIONS.getOrDefault(emailValidationResult.getMessage(), "validation.email.invalid")) - ); + ); + } + } } - /* phone is optional. But if it exists, should match the pattern */ - if (exists(ap.getPhone()) && !isValidPhoneNumber(ap.getPhone())) { - action.addFieldError("eml.associatedParties[" + index + "].phone", - action.getText("validation.invalid", new String[] {action.getText("eml.associatedParties.phone")})); + /* phone(s) are optional. But if they exist, should match the pattern */ + if (!ap.getPhone().isEmpty()) { + for (int phoneIndex = 0; phoneIndex < ap.getPhone().size(); phoneIndex++) { + if (!isValidPhoneNumber(ap.getPhone().get(phoneIndex))) { + action.addFieldError("eml.associatedParties[" + index + "].phone[" + phoneIndex + "]", + action.getText("validation.invalid", new String[] {action.getText("eml.associatedParties.phone")})); + } + } } - /* Validate the homepage URL from each associated parties */ - if (ap.getHomepage() != null) { - if (formatURL(ap.getHomepage()) == null) { - action.addFieldError("eml.associatedParties[" + index + "].homepage", - action.getText("validation.invalid", - new String[] {action.getText("eml.associatedParties.homepage")})); - } else { - ap.setHomepage(formatURL(ap.getHomepage())); + /* Validate the homepage URL from each contact */ + if (!ap.getHomepage().isEmpty()) { + for (int homepageIndex = 0; homepageIndex < ap.getHomepage().size(); homepageIndex++) { + if (formatURL(ap.getHomepage().get(homepageIndex)) == null) { + action.addFieldError("eml.associatedParties[" + index + "].homepage[" + homepageIndex + "]", + action.getText("validation.invalid", + new String[] {action.getText("eml.associatedParties.homepage")})); + } else { + ap.getHomepage().set(homepageIndex, formatURL(ap.getHomepage().get(homepageIndex))); + } } } } @@ -677,34 +848,97 @@ public void validate(BaseAction action, Resource resource, @Nullable MetadataSec if (!isProjectPageEmpty(eml)) { // title is required - if (!exists(eml.getProject().getTitle()) || eml.getProject().getTitle().trim().length() == 0) { + if (!exists(eml.getProject().getTitle()) || eml.getProject().getTitle().trim().isEmpty()) { action.addFieldError("eml.project.title", action.getText("validation.required", new String[] {action.getText("eml.project.title")})); } - // Personnel list - for (int index = 0; index < eml.getProject().getPersonnel().size(); index++) { - Agent p = eml.getProject().getPersonnel().get(index); + // Project awards list: title and funder name required + for (int index = 0; index < eml.getProject().getAwards().size(); index++) { + ProjectAward pa = eml.getProject().getAwards().get(index); - // firstName - optional. But if firstName exists, lastName have to exist - if (exists(p.getFirstName()) && !exists(p.getLastName())) { - action.addFieldError("eml.project.personnel[" + index + "].lastName", - action.getText("validation.firstname.lastname")); + // title + if (!exists(pa.getTitle())) { + action.addFieldError("eml.project.awards[" + index + "].title", + action.getText("validation.required", new String[] {action.getText("eml.project.award.title")})); } - // At least a lastName has to exist - else if (!exists(p.getLastName())) { - action.addFieldError("eml.project.personnel[" + index + "].lastName", - action.getText("validation.required", new String[] {action.getText("eml.project.personnel.lastName")})); + + // funder name + if (!exists(pa.getFunderName())) { + action.addFieldError("eml.project.awards[" + index + "].funderName", + action.getText("validation.required", new String[] {action.getText("eml.project.award.funderName")})); } + } - // directory and personnel id both required (if either is supplied) - if (!p.getUserIds().isEmpty()) { - if (exists(p.getUserIds().get(0).getDirectory()) && !exists(p.getUserIds().get(0).getIdentifier())) { - action.addFieldError("eml.project.personnel[" + index + "].userIds[0].identifier", - action.getText("validation.personnel")); - } else if (!exists(p.getUserIds().get(0).getDirectory()) && exists(p.getUserIds().get(0).getIdentifier())) { - action.addFieldError("eml.project.personnel[" + index + "].userIds[0].directory", - action.getText("validation.directory")); + // Related projects list: title and personnel required + for (int index = 0; index < eml.getProject().getRelatedProjects().size(); index++) { + Project rp = eml.getProject().getRelatedProjects().get(index); + + // related project title + if (!exists(rp.getTitle())) { + action.addFieldError("eml.project.relatedProjects[" + index + "].title", + action.getText("validation.required", new String[] {action.getText("eml.project.relatedProject.title")})); + } + + // related project personnel + if (isAgentsListEmpty(rp.getPersonnel())) { + action.addActionError(action.getText("eml.project.relatedProject.personnel.required")); + } else { + for (int personnelIndex = 0; personnelIndex < eml.getProject().getRelatedProjects().get(index).getPersonnel().size(); personnelIndex++) { + Agent p = eml.getProject().getRelatedProjects().get(index).getPersonnel().get(personnelIndex); + + // firstName - optional. But if firstName exists, lastName have to exist + if (exists(p.getFirstName()) && !exists(p.getLastName())) { + action.addFieldError("eml.project.relatedProjects[" + index + "].personnel[" + personnelIndex + "].lastName", + action.getText("validation.firstname.lastname")); + } + // At least a lastName has to exist + else if (!exists(p.getLastName())) { + action.addFieldError("eml.project.relatedProjects[" + index + "].personnel[" + personnelIndex + "].lastName", + action.getText("validation.required", new String[] {action.getText("eml.project.personnel.lastName")})); + } + + // directory and personnel id both required (if either is supplied) + if (!p.getUserIds().isEmpty()) { + if (exists(p.getUserIds().get(0).getDirectory()) && !exists(p.getUserIds().get(0).getIdentifier())) { + action.addFieldError("eml.project.relatedProjects[" + index + "].personnel[" + personnelIndex + "].userIds[0].identifier", + action.getText("validation.personnel")); + } else if (!exists(p.getUserIds().get(0).getDirectory()) && exists(p.getUserIds().get(0).getIdentifier())) { + action.addFieldError("eml.project.relatedProjects[" + index + "].personnel[" + personnelIndex + "].userIds[0].directory", + action.getText("validation.directory")); + } + } + } + } + } + + // Personnel list + if (isAgentsListEmpty(eml.getProject().getPersonnel())) { + action.addActionError(action.getText("eml.project.personnel.required")); + } else { + for (int index = 0; index < eml.getProject().getPersonnel().size(); index++) { + Agent p = eml.getProject().getPersonnel().get(index); + + // firstName - optional. But if firstName exists, lastName have to exist + if (exists(p.getFirstName()) && !exists(p.getLastName())) { + action.addFieldError("eml.project.personnel[" + index + "].lastName", + action.getText("validation.firstname.lastname")); + } + // At least a lastName has to exist + else if (!exists(p.getLastName())) { + action.addFieldError("eml.project.personnel[" + index + "].lastName", + action.getText("validation.required", new String[] {action.getText("eml.project.personnel.lastName")})); + } + + // directory and personnel id both required (if either is supplied) + if (!p.getUserIds().isEmpty()) { + if (exists(p.getUserIds().get(0).getDirectory()) && !exists(p.getUserIds().get(0).getIdentifier())) { + action.addFieldError("eml.project.personnel[" + index + "].userIds[0].identifier", + action.getText("validation.personnel")); + } else if (!exists(p.getUserIds().get(0).getDirectory()) && exists(p.getUserIds().get(0).getIdentifier())) { + action.addFieldError("eml.project.personnel[" + index + "].userIds[0].directory", + action.getText("validation.directory")); + } } } } @@ -726,7 +960,7 @@ else if (!exists(p.getLastName())) { // method step required int index = 0; for (String method : eml.getMethodSteps()) { - if (method.trim().length() == 0) { + if (method.trim().isEmpty()) { if (emptyFields && index == 0) { eml.getMethodSteps().clear(); break; @@ -868,7 +1102,7 @@ else if (!exists(p.getLastName())) { if (StringUtils.isNotBlank(eml.getDistributionUrl())) { // retrieve a formatted homepage URL including scheme component String formattedUrl = formatURL(eml.getDistributionUrl()); - if (formattedUrl == null || !isWellFormedURI(formattedUrl)) { + if (!isWellFormedURI(formattedUrl)) { action.addFieldError("eml.distributionUrl", action.getText("validation.invalid", new String[] {action.getText("eml.distributionUrl")})); } else { @@ -904,7 +1138,7 @@ else if (!exists(p.getLastName())) { /* Validate distribution URL form each Physical data */ String formattedDistributionUrl = formatURL(pd.getDistributionUrl()); - if (formattedDistributionUrl == null || !isWellFormedURI(formattedDistributionUrl)) { + if (!isWellFormedURI(formattedDistributionUrl)) { action.addFieldError("eml.physicalData[" + index + "].distributionUrl", action .getText("validation.invalid", new String[] {action.getText("eml.physicalData.distributionUrl")})); } else { @@ -939,7 +1173,7 @@ else if (!exists(p.getLastName())) { /** * Determine if the Project page is empty. In other words, the user hasn't entered any information for a single field * yet. There is a total of 7 fields on this page. - * + * * @param eml EML * @return whether the Project page is empty or not. */ @@ -967,7 +1201,7 @@ private boolean isProjectPageEmpty(Eml eml) { /** * Determine if the Methods page is empty. In other words, the user hasn't entered any information for a single field * yet. There is a total of 4 fields on this page. The step description can be multiple. - * + * * @param eml EML * @return whether the Methods page is empty or not. */ @@ -992,7 +1226,7 @@ private boolean isMethodsPageEmpty(Eml eml) { /** * Determine if the Citations page is empty. In other words, the user hasn't entered any information for a single * field yet. There is a total of 4 fields on this page. The bibliographic citation can be multiple. - * + * * @param eml EML * @return whether the Citations page is empty or not. */ @@ -1016,7 +1250,7 @@ private boolean isCitationsPageEmpty(Eml eml) { /** * Determine if a Citation is empty. In other words, the user hasn't entered any information for a single * field yet. There is a total of 2 fields. - * + * * @param citation citation * @return whether the Citation is empty or not. */ @@ -1034,7 +1268,7 @@ private boolean isCitationEmpty(Citation citation) { /** * Determine if the Collections page is empty. In other words, the user hasn't entered any information for a single * field yet. The curatorial section and collection section can be multiple. - * + * * @param eml EML * @return whether the Collections page is empty or not. */ @@ -1068,7 +1302,7 @@ private boolean isCollectionsPageEmpty(Eml eml) { /** * Determine if a JGTICuratorialUnit is empty. In other words, the user hasn't entered any information for a * single field yet. - * + * * @param unit JGTICuratorialUnit * @return whether the JGTICuratorialUnit page is empty or not. */ @@ -1111,7 +1345,7 @@ private boolean isCollectionEmpty(Collection collection) { /** * Determine if the Physical page is empty. In other words, the user hasn't entered any information for a single * field yet. There is a total of 6 fields on this page. The link section can be multiple. - * + * * @param eml EML * @return whether the Physical page is empty or not. */ @@ -1133,7 +1367,7 @@ private boolean isPhysicalPageEmpty(Eml eml) { /** * Determine if a PhysicalData is empty. In other words, the user hasn't entered any information for a single * field yet. - * + * * @param data PhysicalData * @return whether the PhysicalData is empty or not. */ @@ -1156,7 +1390,7 @@ private boolean isExternalLinkEmpty(PhysicalData data) { /** * Determine if the Keywords page is empty. In other words, the user hasn't entered any information for a single * field yet. There is a total of 2 fields on this page. The 2 fields together can be multiple. - * + * * @param eml EML * @return whether the Keywords page is empty or not. */ @@ -1172,7 +1406,7 @@ private boolean isKeywordsPageEmpty(Eml eml) { /** * Determine if the Additional page is empty. In other words, the user hasn't entered any information for a single * field yet. The alternate identifier can be multiple. - * + * * @param eml EML * @return whether the Additional page is empty or not. */ @@ -1200,7 +1434,7 @@ private boolean isAdditionalPageEmpty(Eml eml) { /** * Determine if the Temporal page is empty. In other words, the user hasn't entered any information for a single * field yet. The temporal coverages can be multiple. - * + * * @param eml EML * @return whether the Temporal page is empty or not. */ @@ -1222,7 +1456,7 @@ private boolean isTemporalPageEmpty(Eml eml) { /** * Determine if a single TemporalCoverage is empty. In other words, the user hasn't entered any information for a * single field yet. - * + * * @param cov TemporalCoverage * @return whether the TemporalCoverage is empty or not. */ @@ -1244,7 +1478,7 @@ private boolean isTemporalCoverageEmpty(TemporalCoverage cov) { /** * Determine if the Taxonomic page is empty. In other words, the user hasn't entered any information for a single * field yet. The taxonomic coverages can be multiple. - * + * * @param eml EML * @return whether the Taxonomic page is empty or not. */ @@ -1262,7 +1496,7 @@ private boolean isTaxonomicPageEmpty(Eml eml) { /** * Determine if a TaxonomicCoverage is empty. In other words, the user hasn't entered any information for a single * field yet. - * + * * @param cov TaxonomicCoverage * @return whether the TaxonomicCoverage is empty or not. */ @@ -1285,7 +1519,7 @@ private boolean isTaxonomicCoverageEmpty(TaxonomicCoverage cov) { /** * Determine if a TaxonKeyword is empty. In other words, the user hasn't entered any information for a single * field yet. - * + * * @param word TaxonKeyword * @return whether the TaxonKeyword is empty or not. */ @@ -1302,7 +1536,7 @@ private boolean isTaxonKeywordEmpty(TaxonKeyword word) { /** * Determine if the Geo page is empty. In other words, the user hasn't entered any information for a single * field yet. The geo coverages can be multiple. - * + * * @param eml EML * @return whether the Geo page is empty or not. */ @@ -1340,14 +1574,14 @@ private boolean isAgentEmpty(Agent agent) { if (agent != null) { String first = agent.getFirstName(); String last = agent.getLastName(); - String email = agent.getEmail(); - String home = agent.getHomepage(); + List email = agent.getEmail(); + List home = agent.getHomepage(); String org = agent.getOrganisation(); - String phone = agent.getPhone(); - String position = agent.getPosition(); + List phone = agent.getPhone(); + List position = agent.getPosition(); String city = null; - String street = null; + List street = null; String country = null; String code = null; String province = null; @@ -1371,17 +1605,17 @@ private boolean isAgentEmpty(Agent agent) { } return (StringUtils.isBlank(city) && - StringUtils.isBlank(street) && + CollectionUtils.isEmpty(street) && StringUtils.isBlank(country) && StringUtils.isBlank(code) && StringUtils.isBlank(province) && StringUtils.isBlank(first) && StringUtils.isBlank(last) && - StringUtils.isBlank(email) && - StringUtils.isBlank(home) && + CollectionUtils.isEmpty(email) && + CollectionUtils.isEmpty(home) && StringUtils.isBlank(org) && - StringUtils.isBlank(phone) && - StringUtils.isBlank(position) && + CollectionUtils.isEmpty(phone) && + CollectionUtils.isEmpty(position) && StringUtils.isBlank(directory) && StringUtils.isBlank(identifier)); } @@ -1405,4 +1639,45 @@ private boolean isAgentsListEmpty(List agents) { } return true; } + + // Minimal Stub EML to perform validation of specific fields using XML Schema + private static Eml getStubEml() { + Eml eml = new Eml(); + eml.setTitle("title"); + eml.setLanguage("EN"); + eml.setMetadataLanguage("EN"); + eml.setIntellectualRights("This work is licensed under a Creative Commons Attribution Non Commercial (CC-BY-NC 4.0) License."); + eml.setDescription("description"); + eml.setUpdateFrequency("unknown"); + + Agent contact = new Agent(); + contact.setLastName("Contact 1"); + eml.addContact(contact); + eml.addCreator(contact); + + return eml; + } + + /** + * 1) Removes technical info from the error message (exception message, line and column number etc.) + * 2) Convert DocBook element names to HTML element names + * + * @param technicalMessage error message + * @return simplified and converted message + */ + public static String simplifyDocBookValidationErrorMessage(String technicalMessage) { + // Remove the matched part of the message using the regex. + String simplifiedMessage = technicalMessage.replaceFirst(EML_VALIDATION_ERROR_PATTERN, ""); + + // Replace tags in the message + for (Map.Entry entry : TAG_REPLACEMENTS.entrySet()) { + String originalTag = "'" + entry.getKey() + "'"; + String replacementTag = "'" + entry.getValue() + "'"; + simplifiedMessage = simplifiedMessage.replace(originalTag, replacementTag); + simplifiedMessage = simplifiedMessage.replace(entry.getKey(), entry.getValue()); + } + + // Return the simplified message + return simplifiedMessage.trim(); + } } diff --git a/src/main/resources/ApplicationResources_en.properties b/src/main/resources/ApplicationResources_en.properties index 979f544138..58f63a0831 100644 --- a/src/main/resources/ApplicationResources_en.properties +++ b/src/main/resources/ApplicationResources_en.properties @@ -129,10 +129,13 @@ menu.loggedin=Logged in as {0} # submenus submenu.basic=Basic Metadata +submenu.contacts=Contacts +submenu.acknowledgements=Acknowledgements submenu.parties=Associated Parties submenu.geocoverage=Geographic Coverage submenu.taxcoverage=Taxonomic Coverage submenu.tempcoverage=Temporal Coverage +submenu.additionalDescription=Additional Description submenu.project=Project Data submenu.methods=Sampling Methods submenu.citations=Citations @@ -278,6 +281,7 @@ validation.required={0} is required. validation.input.required=Field is required. validation.input.notNull=Can't be null. validation.invalid={0} is invalid. +validation.invalid.ext={0} is invalid. {1} validation.input.invalid=Field is invalid. validation.short={0} with at least {1} letters is required. validation.contains=The text may not contain the "{0}" symbol. @@ -294,6 +298,8 @@ validation.pattern.violated=Pattern violated validation.maximum.violated=Maximum constraint violated validation.minimum.violated=Minimum constraint violated validation.required.violated=Required constraint violated +validation.cannnot.be.performed=Validation cannot be performed. Please check logs for more details. +validation.failed.see.logs=Failed to validate {}. Please see logs for more details. # user/config validation.email.required=Email is required. @@ -362,6 +368,7 @@ validation.mapping.coreid.uuid.extensions.exist=When using generated UUIDs as id validation.lastname.organisation.position=A last name, an organization, or a position name is required. validation.firstname.lastname=If the first name is supplied, the last name must be also supplied. validation.personnel=If the personnel directory is supplied, the personnel identifier must also be supplied. +validation.personnel.now.required=At least one project contact is now mandatory. Please add a contact person to the project tab. validation.directory=If the personnel identifier is supplied, the personnel directory must also be supplied. validation.longitude.value=Value must be in the range -180 and 180 validation.latitude.value=Value must be in the range -90 and 90 @@ -1416,10 +1423,32 @@ manage.metadata.addnew=Add new manage.metadata.basic.title=Basic Metadata manage.metadata.basic.required.message=Please enter all the mandatory properties on the Basic Metadata page, and then continue entering metadata in the other pages that are applicable to your resource. The more metadata you provide, the greater the chance that your resource will be found, reused by other researchers, and cited. +manage.metadata.gettingStarted=Getting Started +manage.metadata.gettingStarted.description=One or more paragraphs describing the dataset''s overall interpretation, content and structure. For example, the number and names of data files, the types of measurements that they contain, how those data files fit together in an overall design, and how they relate to the data collection methods, experimental design, and sampling design described in other EML sections. One might describe any specialized software that is available and/or may be necessary for analyzing or interpreting the data, and possibly include a high-level description of data formats if they are unusual. +manage.metadata.gettingStarted.help=You can input HTML by clicking </>. The HTML will be converted to DocBook in the EML, so some HTML combinations may not work.

Allowed HTML tags:
  • <div>
  • <p>
  • <ul>
  • <ol>
  • <li>
  • <h1>
  • <pre>
  • <a>
  • <b>
  • <sup>
  • <sub>
+manage.metadata.gettingStarted.unsupportedHtmlInput1=The getting started field contains an unsupported HTML tag: +manage.metadata.gettingStarted.unsupportedHtmlInput2=Please correct the input. Click the info icon next to the field for details on allowed formats. +manage.metadata.introduction=Introduction +manage.metadata.introduction.description=One to many paragraphs that provide background and context for the dataset with appropriate figures and references. This is similar to the introduction for a journal article, and would include, for example, project objectives, hypotheses being addressed, what is known about the pattern or process under study, how the data have been used to date (including references), and how they could be used in the future. +manage.metadata.introduction.help=You can input HTML by clicking </>. The HTML will be converted to DocBook in the EML, so some HTML combinations may not work.

Allowed HTML tags:
  • <div>
  • <p>
  • <ul>
  • <ol>
  • <li>
  • <h1>
  • <pre>
  • <a>
  • <b>
  • <sup>
  • <sub>
+manage.metadata.introduction.unsupportedHtmlInput1=The introduction field contains an unsupported HTML tag: +manage.metadata.introduction.unsupportedHtmlInput2=Please correct the input. Click the info icon next to the field for details on allowed formats. + +manage.metadata.contacts.title=Contacts + +manage.metadata.additionalDescription.title=Additional Description + +manage.metadata.acknowledgements.title=Acknowledgements +manage.metadata.acknowledgements.intro=Please enter metadata that acknowledges funders and other key contributors. +manage.metadata.acknowledgements.description=One or more sentences that acknowledge funders and other key contributors to the study (excluding the dataset authors listed in the creator field). Note that funding awards are also listed by award number in the award section, which provides a structured list of funders, award numbers, and award URIs for the dataset. +manage.metadata.acknowledgements=Acknowledgements +manage.metadata.acknowledgements.info=You can input HTML by clicking </>. The HTML will be converted to DocBook in the EML, so some HTML combinations may not work.

Allowed HTML tags:
  • <div>
  • <p>
  • <ul>
  • <ol>
  • <li>
  • <h1>
  • <pre>
  • <a>
  • <b>
  • <sup>
  • <sub>
+manage.metadata.acknowledgements.unsupportedHtmlInput1=The acknowledgements field contains an unsupported HTML tag: +manage.metadata.acknowledgements.unsupportedHtmlInput2=Please correct the input. Click the info icon next to the field for details on allowed formats. manage.metadata.parties.title=Associated Parties manage.metadata.parties.title.numbered=Associated Party {0} -manage.metadata.parties.intro=Parties associated with this resource (e.g., the hosting institution). +manage.metadata.parties.title.help=Parties associated with this resource (e.g., the hosting institution). manage.metadata.parties.item=associated party manage.metadata.geocoverage.title=Geographic Coverage @@ -1551,6 +1580,7 @@ datapackagemetadata.coordinatePrecision.help=Least precise coordinate precision datapackagemetadata.noData=No data datapackagemetadata.infer.automatically=Infer metadata automatically datapackagemetadata.infer.automatically.help=If selected, IPT will infer metadata automatically. Otherwise, users can provide their own metadata by uploading a file. +datapackagemetadata.infer.automatically.short.help=If selected, IPT will infer metadata automatically. datapackagemetadata.geographic.type=Type datapackagemetadata.geographic.type.help=The type of GeoJSON object. datapackagemetadata.geographic.boundingCoordinates=Bounding Coordinates @@ -1647,8 +1677,12 @@ datapackagemetadata.citation.generate.turn.off=Auto-generation is ON – Turn of eml.title=Title eml.title.help=A descriptive title for the resource, used in page headings and citations. This is for people, not machines, so avoid using a filename, database name or abbreviations known only within your organization. eml.title.shortname.match=Title cannot match the shortname of the resource +eml.shortName=Short Name +eml.shortName.help=The shortName field provides a concise name that describes the resource that is being documented. It is the appropriate place to store a filename associated with other storage systems. eml.description=Description -eml.description.help=A brief overview of the resource that is being documented broken into paragraphs. +eml.description.help=A brief overview of the resource being documented, broken into paragraphs.

You can input HTML by clicking </>. The HTML will be converted to DocBook in the EML, so some HTML combinations may not work.

Allowed HTML tags:
  • <div>
  • <p>
  • <ul>
  • <ol>
  • <li>
  • <h1>
  • <pre>
  • <a>
  • <b>
  • <sup>
  • <sub>
+eml.description.unsupportedHtmlInput1=The description field contains an unsupported HTML tag: +eml.description.unsupportedHtmlInput2=Please correct the input. Click the info icon next to the field for details on allowed formats. eml.description.item=Paragraph eml.publishingOrganisation=Publishing Organization eml.publishingOrganisation.help=Please select the organization responsible for publishing (producing, releasing, holding) this resource. It will be used as the resource''s publishing organization when registering this resource with GBIF and when submitting metadata during DOI registrations. It will also be used to auto-generate the citation for the resource (if auto-generation is turned on), so consider the prominence of the role. Please be aware your selection cannot be changed after the resource has been either registered with GBIF or assigned a DOI. @@ -1690,6 +1724,7 @@ eml.resourceCreator.lastName=Last Name eml.resourceCreator.email=Email eml.resourceCreator.phone=Phone eml.resourceCreator.position=Position +eml.resourceCreator.salutation=Salutation eml.resourceCreator.organisation=Organization eml.resourceCreator.address.address=Address eml.resourceCreator.address.city=City @@ -1705,6 +1740,7 @@ eml.metadataProvider.plural=Metadata Providers eml.metadataProvider.plural.help=The list of metadata providers represents the people and organizations responsible for producing the resource metadata. eml.metadataProvider.firstName=First Name eml.metadataProvider.lastName=Last Name +eml.metadataProvider.salutation=Salutation eml.metadataProvider.email=Email eml.metadataProvider.phone=Phone eml.metadataProvider.position=Position @@ -1728,6 +1764,7 @@ eml.language.available=in {0} eml.contact=Resource Contact eml.contact.plural=Resource Contacts eml.contact.plural.help=The list of contacts represents the people and organizations that should be contacted to get more information about the resource, that curate the resource or to whom putative problems with the resource or its data should be addressed. +eml.contact.salutation=Salutation eml.contact.firstName=First Name eml.contact.lastName=Last Name eml.contact.email=Email @@ -1747,6 +1784,7 @@ eml.contact.directory=Personnel Directory eml.contact.directory.help=The URL of the personnel directory system to which the personnel identifier belongs. eml.contact.identifier=Personnel Identifier eml.contact.identifier.help=A 16-digit ORCID ID (e.g. 0000-0002-1825-0097) or another identifier that links this person to the personnel directory specified. +eml.contact.citation=This contact will appear in the citation eml.country.selection=Select a country, territory, or island eml.rank.selection=Select a rank @@ -1757,6 +1795,7 @@ eml.keywords.help=Any number of keywords delimited by a comma. eml.associatedParties.firstName=First Name eml.associatedParties.lastName=Last Name +eml.associatedParties.salutation=Salutation eml.associatedParties.phone=Phone eml.associatedParties.role=Role eml.associatedParties.role.help=
  • Author: an agent associated with authoring a publication that used the data set, or of a data paper
  • Content Provider: an agent who contributed content to a data set (data set being described may be a composite)
  • Custodian Steward: an agent who is responsible for/takes care of the data set
  • Distributor: an agent involved in the publishing/distribution chain of a data set
  • Editor: an agent associated with editing a publication that used the data set, or of a data paper
  • Metadata Provider: an agent responsible for providing the metadata (the same as metadata provider from basic metadata page)
  • Originator: an agent who originally gathered/prepared the data set (the same as creator from basic metadata page)
  • Owner: an agent who owns the data set (may or may not be the custodian)
  • Point Of Contact: an agent to contact for further information about the data set
  • Principal Investigator: a primary scientific contact associated with the data set
  • Processor: an agent responsible for any post-collection processing of the data set
  • Publisher: the agent associated with publishing a publication that used the data set, or of a data paper
  • User: an agent that makes use of the dataset
  • Programmer: an agent providing informatics/programming support related to the data set
  • Curator: an agent that maintains and documents the specimens in a collection. Some of their duties include preparing and labelling specimens so they are ready for identification, and protecting the specimens
  • Reviewer: person assigned to review the dataset and verify its data and/or metadata quality. This role is analogous to the role played by peer reviewers in the scholarly publication process.
@@ -1770,6 +1809,12 @@ eml.associatedParties.address.country.help=Countries, territories and islands ar eml.associatedParties.address.postalCode=Postal Code eml.associatedParties.email=Email eml.associatedParties.homepage=Home Page +eml.associatedParties.noDirectory=Select a directory +eml.associatedParties.directory=Personnel Directory +eml.associatedParties.directory.help=The URL of the personnel directory system to which the personnel identifier belongs. +eml.associatedParties.identifier=Personnel Identifier +eml.associatedParties.identifier.help=A 16-digit ORCID ID (e.g. 0000-0002-1825-0097) or another identifier that links this person to the personnel directory specified. + eml.geospatialCoverages.description=Description eml.geospatialCoverages.globalCoverage=Set global coverage @@ -1809,11 +1854,31 @@ eml.project.identifier=Identifier eml.project.identifier.help=A unique identifier for the research project. This can be used to link multiple dataset/EML document instances that are associated in some way with the same project, e.g. a monitoring series. The nature of the association can be described in the project description. eml.project.description=Description eml.project.description.help=Abstract about the research project. +eml.project.award=Project Award +eml.project.award.help=The award field is used to provide specific information about the funding awards for a project in a structured format. Sub-fields are provided for the name of the funding agency, the Open Funder Registry identifiers for the agency and program that made the award, the award number assigned, the title of the award, and the URL to the award page describing the award. In general, the funding agency should be listed with a cross-reference to the appropriate identifier from the Open Funder Registry (included in the EML distribution but updated periodically from the Open Funder Registry). +eml.project.award.funderName=Funder name +eml.project.award.funderName.help=The name of the funding institution, with fully expanded acronyms to show the full, official name of the funding agency. +eml.project.award.funderIdentifier=Funder identifier +eml.project.award.funderIdentifier.help=The funder identifier is used to provide one or more canonical identifiers that reference the funder. +eml.project.award.awardNumber=Award number +eml.project.award.awardNumber.help=The awardNumber field provides the unique identifier used by the funder to uniquely identify an award. +eml.project.award.title=Title +eml.project.award.title.help=The title field is used for the title of the award or grant being described. +eml.project.award.awardUrl=Award URL +eml.project.award.awardUrl.help=Typically, the awardUrl is used to find and locate the award, and generally addresses the internet location to find out more information about the award. This should point to a funder site for the award, rather than a project site. +eml.project.relatedProjects=Related Projects +eml.project.relatedProjects.help=This field is a recursive link to another project. This allows projects to be nested under one another in the case where one project spawns another. +eml.project.relatedProject.title=Title +eml.project.relatedProject.identifier=Identifier +eml.project.relatedProject.description=Description +eml.project.relatedProject.personnel=Related Project Personnel +eml.project.relatedProject.personnel.required=At least one related project personnel is required eml.project.personnel=Project Personnel eml.project.personnel.help=The list of personnel represents the people involved in the project. eml.project.personnel.required=At least one personnel is required eml.project.personnel.firstName=Personnel First Name eml.project.personnel.lastName=Personnel Last Name +eml.project.personnel.salutation=Salutation eml.project.personnel.intro=The personnel involved in the project eml.project.funding=Funding eml.project.funding.help=Information about funding sources for the project (e.g., grant and contract numbers, names and addresses of funding sources). Other funding-related information may also be included. @@ -1883,7 +1948,10 @@ eml.distributionUrl.short=Home eml.logoUrl=Resource logo URL eml.logoUrl.help=The URL of the logo associated with a resource. If you don''t have a URL for the resource logo, you may upload an image file selected from your disk. eml.purpose=Purpose -eml.purpose.help=Summary of the intentions for which the data set was developed. Includes objectives for creating the data set and what the data set is to support. +eml.purpose.description=A synopsis of the purpose of this dataset. It may include one or more paragraphs, including a summary of key findings if appropriate. +eml.purpose.help=You can input HTML by clicking </>. The HTML will be converted to DocBook in the EML, so some HTML combinations may not work.

Allowed HTML tags:
  • <div>
  • <p>
  • <ul>
  • <ol>
  • <li>
  • <h1>
  • <pre>
  • <a>
  • <b>
  • <sup>
  • <sub>
+eml.purpose.unsupportedHtmlInput1=The purpose field contains an unsupported HTML tag: +eml.purpose.unsupportedHtmlInput2=Please correct the input. Click the info icon next to the field for details on allowed formats. eml.additionalInfo=Additional Information eml.additionalInfo.help=Any information that is not characterized by the other resource metadata fields, e.g. history of the project, publications that have used the current data, information on related data published elsewhere, etc. @@ -1904,6 +1972,8 @@ eml.intellectualRights.license.help=The licence that you apply to a dataset prov eml.intellectualRights.nolicenses=No licence selected eml.intellectualRights.license.disclaimer=You should only apply a licence to your own work unless you have the necessary rights to apply it to another person''s. +eml.maintenance=Maintenance +eml.maintenance.help=A description of the maintenance of this data resource. eml.updateFrequency=Update Frequency eml.updateFrequency.help=The frequency with which changes are made to the resource after the initial resource has been published. For convenience, its value will default to the auto-publishing interval (if auto-publishing has been turned on), however, it can always be overridden later. Please note a description of the maintenance frequency of the resource can also be entered on the Additional Metadata page. eml.updateFrequency.default=The update frequency defaulted to unknown. @@ -1939,6 +2009,8 @@ rtf.tempcoverage=Temporal coverage rtf.project.details=Project details rtf.project.title=Project title rtf.project.personnel=Personnel +rtf.project.relatedProject=Related Project +rtf.project.award=Award rtf.project.funding=Funding rtf.project.area=Study area descriptions/descriptor rtf.project.design=Design description diff --git a/src/main/webapp/WEB-INF/pages/macros/forms.ftl b/src/main/webapp/WEB-INF/pages/macros/forms.ftl index a5937731f4..d4d14be5b9 100644 --- a/src/main/webapp/WEB-INF/pages/macros/forms.ftl +++ b/src/main/webapp/WEB-INF/pages/macros/forms.ftl @@ -1,6 +1,6 @@ <#ftl output_format="HTML"> - <#macro input name value="-99999" i18nkey="" errorfield="" type="text" size=-1 disabled=false help="" helpOptions=[] date=false requiredField=false maxlength=-1 withLabel=true tabindex=-1> + <#macro input name value="-99999" i18nkey="" errorfield="" type="text" size=-1 disabled=false help="" helpOptions=[] date=false requiredField=false maxlength=-1 withLabel=true tabindex=-1 placeholder="">
<#if withLabel>
@@ -13,6 +13,7 @@ type="${type}" id="${name}" name="${name}" + placeholder="${placeholder}" <#if (tabindex>0)>tabindex="${tabindex}" value="<#if value=="-99999"><@s.property value="${name}"/><#else>${value}" <#if (size>0)>size="${size}" @@ -75,12 +76,14 @@
- <#macro select name options value="" i18nkey="" errorfield="" size=1 disabled=false help="" includeEmpty=false javaGetter=true requiredField=false tabindex=-1 compareValues=false> + <#macro select name options value="" i18nkey="" errorfield="" size=1 disabled=false help="" includeEmpty=false javaGetter=true requiredField=false tabindex=-1 compareValues=false withLabel=true>
+ <#if withLabel>
<#include "/WEB-INF/pages/macros/help_icon.ftl"> <#include "/WEB-INF/pages/macros/form_field_label.ftl">
+ + +
+
+ + + + + + <#include "/WEB-INF/pages/inc/footer.ftl"> + diff --git a/src/main/webapp/WEB-INF/pages/manage/eml/additional.ftl b/src/main/webapp/WEB-INF/pages/manage/eml/additional.ftl index 22f15372d4..175872bcfb 100644 --- a/src/main/webapp/WEB-INF/pages/manage/eml/additional.ftl +++ b/src/main/webapp/WEB-INF/pages/manage/eml/additional.ftl @@ -218,16 +218,6 @@
- -
- <@text name="eml.purpose" i18nkey="eml.purpose" help="i18n"/> -
- - -
- <@text name="eml.updateFrequencyDescription" i18nkey="eml.updateFrequencyDescription" help="i18n" /> -
-
<@text name="eml.additionalInfo" i18nkey="eml.additionalInfo" help="i18n"/> diff --git a/src/main/webapp/WEB-INF/pages/manage/eml/additionalDescription.ftl b/src/main/webapp/WEB-INF/pages/manage/eml/additionalDescription.ftl new file mode 100644 index 0000000000..a2afa79531 --- /dev/null +++ b/src/main/webapp/WEB-INF/pages/manage/eml/additionalDescription.ftl @@ -0,0 +1,251 @@ +<#-- @ftlvariable name="" type="org.gbif.ipt.action.manage.MetadataAction" --> +<#escape x as x?html> + <#include "/WEB-INF/pages/inc/header.ftl"> + <#include "/WEB-INF/pages/macros/metadata.ftl"/> + <@s.text name='manage.metadata.additionalDescription.title'/> + + + + + + <#assign currentMenu="manage"/> + <#assign currentMetadataPage = "additionalDescription"/> + <#include "/WEB-INF/pages/inc/menu.ftl"> + <#include "/WEB-INF/pages/macros/forms.ftl"/> + +
+ <#include "/WEB-INF/pages/inc/action_alerts.ftl"> + + +
+ +
+
+
+
+
+
+ +
+
+ +
+

+ <@s.text name='manage.metadata.additionalDescription.title'/> +

+
+ + + +
+ <@s.submit cssClass="button btn btn-sm btn-outline-gbif-primary top-button" name="save" key="button.save"/> + <@s.submit cssClass="button btn btn-sm btn-outline-secondary top-button" name="cancel" key="button.back"/> +
+
+
+
+ + <#include "metadata_section_select.ftl"/> + +
+
+
+
+ <#include "eml_sidebar.ftl"/> +
+ +
+ +
+ <@textinline name="eml.purpose" help="i18n"/> + +
+

+ <@s.text name='eml.purpose.description'/> +

+
+ +
+ + +
+
+ + +
+ <@textinline name="manage.metadata.introduction" help="i18n"/> + +
+

+ <@s.text name='manage.metadata.introduction.description'/> +

+
+ +
+ + +
+
+ + +
+ <@textinline name="manage.metadata.gettingStarted" help="i18n"/> + +
+

+ <@s.text name='manage.metadata.gettingStarted.description'/> +

+
+ +
+ + +
+
+
+
+
+
+
+ + <#include "/WEB-INF/pages/inc/footer.ftl"> + diff --git a/src/main/webapp/WEB-INF/pages/manage/eml/basic.ftl b/src/main/webapp/WEB-INF/pages/manage/eml/basic.ftl index a4fbf49bc2..f2f26306e8 100644 --- a/src/main/webapp/WEB-INF/pages/manage/eml/basic.ftl +++ b/src/main/webapp/WEB-INF/pages/manage/eml/basic.ftl @@ -5,9 +5,12 @@ <#include "/WEB-INF/pages/macros/user_id_directories.ftl"/> <@s.text name='manage.metadata.basic.title'/> + + + <#include "/WEB-INF/pages/macros/metadata_agent.ftl"/> + <#assign currentMenu="manage"/> <#assign currentMetadataPage = "basic"/> <#include "/WEB-INF/pages/inc/menu.ftl"> @@ -420,9 +309,19 @@
<#include "/WEB-INF/pages/inc/action_alerts.ftl"> + +
-
+
@@ -481,40 +380,54 @@
<@input name="eml.title" help="i18n" requiredField=true />
+
-
- <@select name="eml.metadataLanguage" help="i18n" options=languages value="${metadataLanguageIso3!'eng'}" requiredField=true /> -
- -
- <@select name="resource.coreType" i18nkey="resource.coreType" help="i18n" options=types value="${resource.coreType!''}" requiredField=true /> +
+
+ <@input name="eml.shortName" help="i18n" />
-
+
<#if resource.organisation??> <@select name="id" i18nkey="eml.publishingOrganisation" help="i18n" options=organisations value="${resource.organisation.key!''}" requiredField=true /> <#else> <@select name="id" i18nkey="eml.publishingOrganisation" help="i18n" options=organisations requiredField=true />
+
+
-
- <@select name="eml.language" help="i18n" options=languages value="${languageIso3!'eng'}" requiredField=true /> +
+
+
+ <@select name="resource.coreType" i18nkey="resource.coreType" help="i18n" options=types value="${resource.coreType!''}" requiredField=true />
-
+
<@select name="resource.subtype" i18nkey="resource.subtype" help="i18n" options=listSubtypes value="${resource.subtype!''}" />
+
+
-
- <@select name="eml.updateFrequency" i18nkey="eml.updateFrequency" help="i18n" options=frequencies value="${eml.updateFrequency.identifier!'unkown'}" requiredField=true /> +
+
+
+ <@select name="eml.language" help="i18n" options=languages value="${languageIso3!'eng'}" requiredField=true />
+
+ <@select name="eml.metadataLanguage" help="i18n" options=languages value="${metadataLanguageIso3!'eng'}" requiredField=true /> +
+
+
+ +
+
<@select name="eml.intellectualRights.license" i18nkey="eml.intellectualRights.license" help="i18n" options=licenses value="${licenseKeySelected!}" requiredField=true/> -
+
<@licenseLogoClass eml.intellectualRights!/> <#if eml.intellectualRights?has_content> @@ -548,569 +461,29 @@
- <@textinline name="eml.description" help="i18n" requiredField=true/> -
- <#list eml.description as item> -
- - <@simpleText name="eml.description[${item_index}]" minlength=5 requiredField=true> -
- -
- + <@textinline name="eml.description" help="i18n" i18nkey="" requiredField=true/> -
- - <@textinline name="eml.contact.plural" help="i18n" requiredField=true/> -
- <#list eml.contacts as contact> -
- -
- <@input name="eml.contacts[${contact_index}].firstName" i18nkey="eml.contact.firstName"/> -
-
- <@input name="eml.contacts[${contact_index}].lastName" i18nkey="eml.contact.lastName" requiredField=true/> -
-
- <@input name="eml.contacts[${contact_index}].position" i18nkey="eml.contact.position" requiredField=true /> -
-
- <@input name="eml.contacts[${contact_index}].organisation" i18nkey="eml.contact.organisation" requiredField=true /> -
-
- <@input name="eml.contacts[${contact_index}].address.address" i18nkey="eml.contact.address.address" /> -
-
- <@input name="eml.contacts[${contact_index}].address.city" i18nkey="eml.contact.address.city" /> -
-
- <@input name="eml.contacts[${contact_index}].address.province" i18nkey="eml.contact.address.province" /> -
-
- <@select name="eml.contacts[${contact_index}].address.country" help="i18n" options=countries i18nkey="eml.contact.address.country" value="${eml.contacts[contact_index].address.country!}"/> -
-
- <@input name="eml.contacts[${contact_index}].address.postalCode" i18nkey="eml.contact.address.postalCode" /> -
-
- <@input name="eml.contacts[${contact_index}].phone" i18nkey="eml.contact.phone" /> -
-
- <@input name="eml.contacts[${contact_index}].email" i18nkey="eml.contact.email" /> -
-
- <@input name="eml.contacts[${contact_index}].homepage" i18nkey="eml.contact.homepage" type="url" /> -
-
- <#if (eml.contacts[contact_index].userIds[0].directory)??> - <@select name="eml.contacts[${contact_index}].userIds[0].directory" help="i18n" options=userIdDirectories i18nkey="eml.contact.directory" value="${userIdDirecotriesExtended[eml.contacts[contact_index].userIds[0].directory!]!}"/> - <#else> - <@select name="eml.contacts[${contact_index}].userIds[0].directory" help="i18n" options=userIdDirectories i18nkey="eml.contact.directory" value=""/> - -
-
- <#if eml.contacts[contact_index].userIds[0]??> - <@input name="eml.contacts[${contact_index}].userIds[0].identifier" help="i18n" i18nkey="eml.contact.identifier" value="${eml.contacts[contact_index].userIds[0].identifier!}"/> - <#else> - <@input name="eml.contacts[${contact_index}].userIds[0].identifier" help="i18n" i18nkey="eml.contact.identifier" value=""/> - -
-
- -
- - + <@textinline name="eml.maintenance" help="i18n"/> - - - - - -
-
- -
-
- - - - - <#include "/WEB-INF/pages/inc/footer.ftl"> - diff --git a/src/main/webapp/WEB-INF/pages/manage/eml/project.ftl b/src/main/webapp/WEB-INF/pages/manage/eml/project.ftl index 7c49468c7a..6af2128761 100644 --- a/src/main/webapp/WEB-INF/pages/manage/eml/project.ftl +++ b/src/main/webapp/WEB-INF/pages/manage/eml/project.ftl @@ -1,5 +1,6 @@ <#escape x as x?html> <#include "/WEB-INF/pages/inc/header.ftl"> + <#include "/WEB-INF/pages/macros/metadata.ftl"/> @@ -137,6 +138,34 @@ width: 50%; max-width: 600px; } + + .form-control, .form-select { + min-height: calc(1.5em + .5rem + 2px); + padding: .25rem .5rem; + font-size: .875rem; + border-radius: .2rem; + } + + .select2-container--bootstrap4 .select2-selection--single { + height: calc(1.5em + .5rem + 2px) !important; + font-size: .875rem !important; + } + + .select2-container--bootstrap4 .select2-selection--single .select2-selection__rendered { + line-height: calc(1.5em + .5rem) !important; + } + + .select2-container--bootstrap4 .select2-selection--single .select2-selection__placeholder { + line-height: calc(1.5em + .5rem) !important; + } + + .select2-container--bootstrap4 .select2-selection__clear { + margin-top: .625em !important; + } + + .select2-results__option, .select2-search__field { + font-size: .875rem; + } <#assign currentMetadataPage = "project"/> <#assign currentMenu="manage"/> @@ -149,7 +178,7 @@ <#include "/WEB-INF/pages/inc/action_alerts.ftl">
-
+
@@ -199,7 +228,11 @@ <#assign copyLink><@s.text name="eml.metadataAgent.copyLink"/> <#assign removeLink><@s.text name='manage.metadata.removethis'/> <@s.text name='rtf.project.personnel'/> + <#assign removeRelatedProjectLink><@s.text name='manage.metadata.removethis'/> <@s.text name='rtf.project.relatedProject'/> + <#assign removeAwardLink><@s.text name='manage.metadata.removethis'/> <@s.text name='rtf.project.award'/> <#assign addLink><@s.text name='manage.metadata.addnew'/> <@s.text name='rtf.project.personnel'/> + <#assign addRelatedProjectLink><@s.text name='manage.metadata.addnew'/> <@s.text name='rtf.project.relatedProject'/> + <#assign addAwardLink><@s.text name='manage.metadata.addnew'/> <@s.text name='rtf.project.award'/>
<@input name="eml.project.title" requiredField=true/> @@ -212,10 +245,179 @@
+
+ +
+ <@textinline name="eml.project.award" help="i18n"/> + +
+ <#list eml.project.awards as item> +
+ +
+ <@input name="eml.project.awards[" + item_index + "].title" help="i18n" i18nkey="eml.project.award.title" requiredField=true/> +
+
+ <@input name="eml.project.awards[" + item_index + "].funderName" help="i18n" i18nkey="eml.project.award.funderName" requiredField=true /> +
+
+ <@input name="eml.project.awards[" + item_index + "].awardNumber" help="i18n" i18nkey="eml.project.award.awardNumber" /> +
+
+ <@input name="eml.project.awards[" + item_index + "].awardUrl" help="i18n" i18nkey="eml.project.award.awardUrl" /> +
+
+ <@input name="eml.project.awards[" + item_index + "].funderIdentifiers[0]" help="i18n" i18nkey="eml.project.award.funderIdentifier" /> +
+
+ +
+ + +
+
+ +
+ +
+ <@textinline name="eml.project.relatedProjects" help="i18n"/> + +
+ <#list eml.project.relatedProjects as item> +
+ +
+ <@input name="eml.project.relatedProjects[" + item_index + "].title" i18nkey="eml.project.relatedProject.title" requiredField=true/> +
+
+ <@input name="eml.project.relatedProjects[" + item_index + "].identifier" i18nkey="eml.project.relatedProject.identifier" /> +
+
+ <@text name="eml.project.relatedProjects[" + item_index + "].description" i18nkey="eml.project.relatedProject.description" /> +
+
+
+
+ +
+ <#list (eml.project.relatedProjects[item_index].personnel)! as personnel> +
+ +
+ <@input name="eml.project.relatedProjects[${item_index}].personnel[${personnel_index}].firstName" i18nkey="eml.project.personnel.firstName" /> +
+
+ <@input name="eml.project.relatedProjects[${item_index}].personnel[${personnel_index}].lastName" i18nkey="eml.project.personnel.lastName" requiredField=true /> +
+
+ <@input name="eml.project.relatedProjects[${item_index}].personnel[${personnel_index}].salutation" i18nkey="eml.project.personnel.salutation" /> +
+
+ <@select name="eml.project.relatedProjects[${item_index}].personnel[${personnel_index}].userIds[0].directory" value="${(eml.project.relatedProjects[item_index].personnel[personnel_index].userIds[0].directory)!}" options=userIdDirectories help="i18n" i18nkey="eml.contact.directory" /> +
+
+ <@input name="eml.project.relatedProjects[${item_index}].personnel[${personnel_index}].userIds[0].identifier" help="i18n" i18nkey="eml.contact.identifier" /> +
+
+ <@select name="eml.project.relatedProjects[${item_index}].personnel[${personnel_index}].role" value="${(eml.project.relatedProjects[item_index].personnel[personnel_index].role)!}" i18nkey="eml.associatedParties.role" help="i18n" options=roles /> +
+
+ +
+ +
+
+ +
+ + +
+
+
- <@textinline name="eml.project.personnel" help="i18n"/> + <@textinline name="eml.project.personnel" help="i18n" requiredField=true />
<#list eml.project.personnel as item> @@ -248,12 +450,15 @@
-
+
<@input name="eml.project.personnel[${item_index}].firstName" i18nkey="eml.project.personnel.firstName"/>
-
+
<@input name="eml.project.personnel[${item_index}].lastName" i18nkey="eml.project.personnel.lastName" requiredField=true/>
+
+ <@input name="eml.project.personnel[${item_index}].salutation" i18nkey="eml.project.personnel.salutation" /> +
<#if eml.project.personnel[item_index]?? && eml.project.personnel[item_index].userIds[0]??> <@select name="eml.project.personnel[${item_index}].userIds[0].directory" help="i18n" options=userIdDirectories i18nkey="eml.contact.directory" value="${userIdDirecotriesExtended[eml.project.personnel[item_index].userIds[0].directory!]!}"/> @@ -286,7 +491,7 @@ - @@ -483,14 +496,8 @@ <@s.text name='portal.resource.description'/>
- <#if (eml.description?size>0)> - <#list eml.description as para> - <#if para?has_content> -

- <@para?interpret /> -

- - + <#if (eml.description??)> + <@eml.description?interpret /> <#else>

<@s.text name='portal.resource.no.description'/>

@@ -872,6 +879,34 @@ <@eml.project.designDescription?interpret /> + <#if eml.project.awards?has_content> + + <@s.text name='eml.project.award'/> + + <#list eml.project.awards as award> + ${award.title!}
+ <#list award.funderIdentifiers as fi>${fi}<#sep>,
+ <#if award.funderName?has_content>${award.funderName}
+ <#if award.awardNumber?has_content>${award.awardNumber}
+ <#if award.awardUrl?has_content>${award.awardUrl}
+ <#sep>
+ + + + + <#if eml.project.relatedProjects?has_content> + + <@s.text name='eml.project.relatedProjects'/> + + <#list eml.project.relatedProjects as relatedProject> + ${relatedProject.title}
+ <#if relatedProject.identifier?has_content>${relatedProject.identifier}
+ <#if relatedProject.description?has_content>${relatedProject.description}
+ <#sep>
+ + + +
@@ -1037,7 +1072,7 @@ - <#if eml.additionalInfo?has_content || eml.purpose?has_content || (eml.alternateIdentifiers?size > 0 )> + <#if eml.introduction?has_content || eml.gettingStarted?has_content || eml.acknowledgements?has_content || eml.additionalInfo?has_content || eml.purpose?has_content || (eml.alternateIdentifiers?size > 0 )>
@@ -1051,6 +1086,24 @@
+ <#if eml.acknowledgements?has_content> + + + + + + <#if eml.introduction?has_content> + + + + + + <#if eml.gettingStarted?has_content> + + + + + <#if eml.purpose?has_content> diff --git a/src/main/webapp/js/docbook/docbook.js b/src/main/webapp/js/docbook/docbook.js new file mode 100644 index 0000000000..e00a22821a --- /dev/null +++ b/src/main/webapp/js/docbook/docbook.js @@ -0,0 +1,127 @@ +// Function to convert HTML to DocBook +function convertToDocBook(html) { + // Trim first + html = html.trim(); + // Remove
tags + html = html.replace(/
/g, '').replace(//g, ''); + // Remove ` (tick) + html = html.replace(/`/g, ''); + + // Replace with + html = html + .replace(/<h1>/g, '<title>').replace(/<\/h1>/g, '') + .replace(/

/g, '').replace(/<\/h2>/g, '') + .replace(/

/g, '').replace(/<\/h3>/g, '') + .replace(/

/g, '').replace(/<\/h4>/g, '') + .replace(/

/g, '').replace(/<\/h5>/g, ''); + + // Replace
with
(ignore classes) + html = html + .replace(//g, '
') + .replace(/<\/div>/g, '
'); + + // Replace
<@s.text name='manage.metadata.acknowledgements'/><@eml.acknowledgements?interpret />
<@s.text name='manage.metadata.introduction'/><@eml.introduction?interpret />
<@s.text name='manage.metadata.gettingStarted'/><@eml.gettingStarted?interpret />
<@s.text name='eml.purpose'/>