/*************************************************************************** * Copyright 2010 Global Biodiversity Information Facility Secretariat * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. ***************************************************************************/ package org.gbif.ipt.action.manage; import org.gbif.api.model.common.DOI; import org.gbif.api.model.common.DoiData; import org.gbif.api.model.common.DoiStatus; import org.gbif.doi.metadata.datacite.DataCiteMetadata; import org.gbif.doi.service.DoiException; import org.gbif.doi.service.DoiExistsException; import org.gbif.doi.service.InvalidMetadataException; import org.gbif.dwc.terms.Term; import org.gbif.dwc.terms.TermFactory; import org.gbif.dwca.io.Archive; import org.gbif.dwca.io.ArchiveFile; import org.gbif.utils.file.csv.CSVReader; import org.gbif.utils.file.csv.CSVReaderFactory; import org.gbif.ipt.config.AppConfig; import org.gbif.ipt.config.Constants; import org.gbif.ipt.model.Extension; import org.gbif.ipt.model.ExtensionMapping; import org.gbif.ipt.model.Organisation; import org.gbif.ipt.model.Resource; import org.gbif.ipt.model.User; import org.gbif.ipt.model.User.Role; import org.gbif.ipt.model.VersionHistory; import org.gbif.ipt.model.voc.IdentifierStatus; import org.gbif.ipt.model.voc.PublicationMode; import org.gbif.ipt.model.voc.PublicationStatus; import org.gbif.ipt.service.DeletionNotAllowedException; import org.gbif.ipt.service.InvalidConfigException; import org.gbif.ipt.service.PublicationException; import org.gbif.ipt.service.RegistryException; import org.gbif.ipt.service.UndeletNotAllowedException; import org.gbif.ipt.service.admin.ExtensionManager; import org.gbif.ipt.service.admin.RegistrationManager; import org.gbif.ipt.service.admin.UserAccountManager; import org.gbif.ipt.service.admin.VocabulariesManager; import org.gbif.ipt.service.manage.ResourceManager; import org.gbif.ipt.struts2.SimpleTextProvider; import org.gbif.ipt.task.GenerateDwca; import org.gbif.ipt.task.GenerateDwcaFactory; import org.gbif.ipt.task.ReportHandler; import org.gbif.ipt.task.StatusReport; import org.gbif.ipt.task.TaskMessage; import org.gbif.ipt.utils.DOIUtils; import org.gbif.ipt.utils.DataCiteMetadataBuilder; import org.gbif.ipt.utils.MapUtils; import org.gbif.ipt.utils.ResourceUtils; import org.gbif.ipt.validation.EmlValidator; import org.gbif.metadata.eml.Citation; import org.gbif.metadata.eml.Eml; import org.gbif.metadata.eml.EmlFactory; import org.gbif.metadata.eml.MaintenanceUpdateFrequency; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.net.URI; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.Files; import com.google.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.log4j.Level; import org.apache.log4j.Logger; import static org.gbif.ipt.task.GenerateDwca.CHARACTER_ENCODING; public class OverviewAction extends ManagerBaseAction implements ReportHandler { // logging private static final Logger LOG = Logger.getLogger(OverviewAction.class); private static final String PUBLISHING = "publishing"; private static final TermFactory TERM_FACTORY = TermFactory.instance(); private final UserAccountManager userManager; private final ExtensionManager extensionManager; private final VocabulariesManager vocabManager; private List<User> potentialManagers; private List<Extension> potentialCores; private List<Extension> potentialExtensions; private List<Organisation> organisations; private Organisation doiAccount; private final EmlValidator emlValidator; private boolean missingMetadata; private boolean missingRegistrationMetadata; private boolean missingValidPublishingOrganisation; private boolean metadataModifiedSinceLastPublication; private boolean mappingsModifiedSinceLastPublication; private boolean sourcesModifiedSinceLastPublication; private StatusReport report; private Date now; private boolean unpublish = false; private boolean reserveDoi = false; private boolean deleteDoi = false; private boolean delete = false; private boolean undelete = false; private boolean publish = false; private String summary; private Map<String, String> frequencies; // preview private GenerateDwcaFactory dwcaFactory; private List<String> columns; private List<String[]> peek; private Integer mid; private static final int PEEK_ROWS = 100; @Inject public OverviewAction(SimpleTextProvider textProvider, AppConfig cfg, RegistrationManager registrationManager, ResourceManager resourceManager, UserAccountManager userAccountManager, ExtensionManager extensionManager, VocabulariesManager vocabManager, GenerateDwcaFactory dwcaFactory) { super(textProvider, cfg, registrationManager, resourceManager); this.userManager = userAccountManager; this.extensionManager = extensionManager; this.emlValidator = new EmlValidator(cfg, registrationManager, textProvider); this.vocabManager = vocabManager; this.dwcaFactory = dwcaFactory; this.doiAccount = registrationManager.findPrimaryDoiAgencyAccount(); } /** * Triggered by add manager button on manage resource page. */ public String addManager() throws Exception { if (resource == null) { return NOT_FOUND; } User u = userManager.get(id); if (u != null && !potentialManagers.contains(u)) { addActionError(getText("manage.overview.manager.not.available", new String[] {id})); } else if (u != null) { resource.addManager(u); addActionMessage(getText("manage.overview.user.added", new String[] {u.getName()})); saveResource(); potentialManagers.remove(u); } return execute(); } /** * Cancel resource publication. Publication is all or nothing. If incomplete, the version number of the resource * must be rolled back. * * @return Struts2 result string */ public String cancel() throws Exception { if (resource == null) { return NOT_FOUND; } boolean cancelled = resourceManager.cancelPublishing(resource.getShortname(), this); if (cancelled) { // final logging BigDecimal version = resource.getEmlVersion(); String msg = getText("publishing.cancelled", new String[] {version.toPlainString(), resource.getShortname()}); LOG.warn(msg); addActionError(msg); // restore the previous version of the resource resourceManager.restoreVersion(resource, version, this); // Struts finishes before callable has a finish to update status report, therefore, // temporarily override StatusReport so that Overview page report displaying up-to-date STATE and Exception StatusReport tmpReport = new StatusReport(true, GenerateDwca.CANCELLED_STATE_MSG, report.getMessages()); report = tmpReport; return execute(); } addActionError(getText("manage.overview.failed.stop.publishing")); return ERROR; } /** * Deletes a resource different ways depending on whether it was ever assigned a DOI or not. * </br> * If this resource was assigned a DOI, it makes the DOI unavailable meaning it still resolves, but to a page * explaining the resource has been removed. Furthermore, its archived published versions must be preserved in case * the resource is made available again in the future. * </br> * If this resource was not assigned a DOI, it's safe to just delete the resource and all its archived published * versions. * </br> * Regardless of whether the resource was assigned a DOI, it deletes the resource from GBIF if this resource was * registered with GBIF. * </br> */ @Override public String delete() { if (resource == null) { return NOT_FOUND; } if (delete) { if (resource.getStatus().equals(PublicationStatus.DELETED)) { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); return INPUT; } try { DOI doi = resource.getDoi(); if (doi != null) { // prevent deletion if it will trigger a DOI operation, but no DOI agency account has been activated yet if (registrationManager.getDoiService() == null) { String msg = getText("manage.overview.doi.operation.failed.noAccount"); LOG.error(msg); addActionError(msg); return INPUT; } // de-register resource, but don't delete resource directory if (resource.isRegistered()) { resourceManager.delete(resource, false); } // next try to deactivate as many DOIs assigned to the resource as possible (and delete DOI if reserved) doDeactivateDOI(doi); resource.setIdentifierStatus( (resource.getIdentifierStatus().equals(IdentifierStatus.PUBLIC_PENDING_PUBLICATION)) ? IdentifierStatus.UNRESERVED : IdentifierStatus.UNAVAILABLE); // delete previously assigned DOIs also Set<String> deleted = Sets.newHashSet(doi.toString()); if (!resource.getVersionHistory().isEmpty()) { for (VersionHistory history: resource.getVersionHistory()) { DOI formerDoi = history.getDoi(); if (formerDoi != null && !deleted.contains(formerDoi.toString())) { doDeactivateDOI(formerDoi); deleted.add(formerDoi.toString()); } } } resource.setStatus(PublicationStatus.DELETED); resource.updateAlternateIdentifierForDOI(); resource.updateCitationIdentifierForDOI(); // unset DOI as citation identifier saveResource(); addActionMessage(getText("manage.overview.resource.deleted", new String[] {resource.toString()})); } else { // de-register resource, and delete resource directory resourceManager.delete(resource, true); } return HOME; } catch (IOException e) { String msg = getText("manage.resource.delete.failed"); LOG.error(msg, e); addActionError(msg); addActionExceptionWarning(e); } catch (DeletionNotAllowedException e) { String msg = getText("manage.resource.delete.failed"); LOG.error(msg, e); addActionError(msg); addActionExceptionWarning(e); } } else { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); } return SUCCESS; } /** * Resolve DOI, then depending on its state delete it (if reserved) or deactivate it (if registered). * * @param doi DOI to delete/deactivate * * @throws org.gbif.ipt.service.DeletionNotAllowedException if deletion failed */ private void doDeactivateDOI(DOI doi) throws DeletionNotAllowedException { Preconditions.checkNotNull(registrationManager.getDoiService()); Preconditions.checkNotNull(doi); try { DoiData doiData = registrationManager.getDoiService().resolve(doi); if (doiData != null && doiData.getStatus() != null) { if (doiData.getStatus().equals(DoiStatus.RESERVED)) { LOG.info("Deleting reserved DOI: " + doi.toString() + "..."); registrationManager.getDoiService().delete(doi); String msg = getText("manage.overview.publishing.doi.delete.success", new String[] {doi.toString()}); LOG.info(msg); addActionMessage(msg); } else if (doiData.getStatus().equals(DoiStatus.REGISTERED)) { LOG.info("Deactivating registered DOI: " + doi.toString() + "..."); registrationManager.getDoiService().delete(doi); String msg = getText("manage.overview.publishing.doi.deactivate.success", new String[]{doi.toString()}); LOG.info(msg); addActionMessage(msg); } else { LOG.error( "Not appropriate to delete DOI: " + doi.toString() + ". DOI status=" + doiData.getStatus().toString()); } } else { throw new DeletionNotAllowedException(DeletionNotAllowedException.Reason.DOI_REGISTRATION_AGENCY_ERROR, getText("manage.overview.publishing.doi.delete.failed.notResolved", new String[] {doi.toString()})); } } catch (DoiException e) { throw new DeletionNotAllowedException(DeletionNotAllowedException.Reason.DOI_REGISTRATION_AGENCY_ERROR, getText("manage.overview.publishing.doi.delete.failed.exception", new String[]{doi.toString(), e.getMessage()})); } } /** * Undeletes a resource (only applicable to resources that were previously assigned a DOI). * </br> * Undeleting a resource makes the DOI available, resolving to the resource homepage of the last published version. * It also undeletes all previous DOIs assigned to the resource, resolving them to the appropriate versioned resource * homepages. * </br> * Undeleting a resource should also undelete the resource from GBIF if it was previously registered with GBIF, * however, at this time the GBIF registry api or GBIF registry legacy apis do not support the undelete operation and * thus require communication with the GBIF Helpdesk. * */ public String undelete() { if (resource == null) { return NOT_FOUND; } if (undelete) { if (!resource.getStatus().equals(PublicationStatus.DELETED)) { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); return INPUT; } // note: the DOI of last published version is undeleted DOI doi = resource.getAssignedDoi(); if (doi != null) { try { // prevent deletion if it will trigger a DOI operation, but no DOI agency account has been activated yet if (registrationManager.getDoiService() == null) { String msg = getText("manage.overview.doi.operation.failed.noAccount"); LOG.error(msg); addActionError(msg); return INPUT; } Organisation organisation = resource.getOrganisation(); if (organisation == null) { throw new InvalidConfigException(InvalidConfigException.TYPE.RESOURCE_CONFIG, "Resource being undeleted missing publishing organisation!"); } else { Organisation retrieved = registrationManager.get(organisation.getKey()); if (retrieved == null) { throw new UndeletNotAllowedException(UndeletNotAllowedException.Reason.ORGANISATION_NOT_ASSOCIATED_TO_IPT, getText("manage.overview.publishing.doi.undelete.failed.noOrganisation", new String[] {organisation.getKey().toString()})); } else { Organisation doiAccountActivated = registrationManager.findPrimaryDoiAgencyAccount(); if (doiAccountActivated != null && doiAccountActivated.getDoiPrefix() != null && !doi.getDoiName().toLowerCase().startsWith(doiAccountActivated.getDoiPrefix().toLowerCase())) { throw new UndeletNotAllowedException(UndeletNotAllowedException.Reason.DOI_PREFIX_NOT_MATCHING, getText("manage.overview.publishing.doi.undelete.failed.badPrefix", new String[] {doi.toString(), doiAccountActivated.getDoiPrefix()})); } } } // reconstruct version being undeleted String shortname = resource.getShortname(); BigDecimal versionToUndelete = resource.getLastPublishedVersionsVersion(); UUID key = resource.getKey(); File versionToUndeleteEmlFile = cfg.getDataDir().resourceEmlFile(shortname, versionToUndelete); Resource reconstructed = ResourceUtils.reconstructVersion(versionToUndelete, shortname, doi, organisation, resource.findVersionHistory(versionToUndelete), versionToUndeleteEmlFile, key); URI target = cfg.getResourceUri(shortname); // perform undelete doUndeleteDOI(doi, reconstructed, target); // reassign DOI of last published version resource.setDoi(doi); resource.setIdentifierStatus(IdentifierStatus.PUBLIC); resource.updateCitationIdentifierForDOI(); // undelete previously assigned DOIs also, which were all deleted/deactivated Set<String> undeleted = Sets.newHashSet(doi.toString()); if (!resource.getVersionHistory().isEmpty()) { for (VersionHistory history : resource.getVersionHistory()) { DOI formerDoi = history.getDoi(); if (formerDoi != null && !undeleted.contains(formerDoi.toString())) { // reconstruct version being undeleted BigDecimal formerVersionToUndelete = new BigDecimal(history.getVersion()); File formerVersionToUndeleteEmlFile = cfg.getDataDir().resourceEmlFile(shortname, formerVersionToUndelete); Resource formerVersionReconstructed = ResourceUtils .reconstructVersion(formerVersionToUndelete, shortname, formerDoi, organisation, resource.findVersionHistory(formerVersionToUndelete), formerVersionToUndeleteEmlFile, key); // prepare target URI equal to version resource page URI formerTarget = cfg.getResourceVersionUri(shortname, formerVersionToUndelete); // perform undelete doUndeleteDOI(formerDoi, formerVersionReconstructed, formerTarget); undeleted.add(formerDoi.toString()); } } } // revert resource status back to PUBLIC/REGISTERED if (reconstructed.isRegistered()) { resource.setStatus(PublicationStatus.REGISTERED); // TODO: undelete it from GBIF if it was registered (requires GBIF API change) addActionWarning(getText("manage.overview.resource.undelete.warning.gbif")); } else { resource.setStatus(PublicationStatus.PUBLIC); } saveResource(); addActionMessage( getText("manage.overview.resource.undeleted", new String[] {resource.getTitleAndShortname()})); return SUCCESS; } catch (UndeletNotAllowedException e) { String msg = getText("manage.resource.undelete.failed"); LOG.error(msg, e); addActionError(msg); addActionExceptionWarning(e); } catch (IllegalArgumentException e) { String msg = getText("manage.resource.undelete.failed"); LOG.error(msg, e); addActionError(msg); addActionExceptionWarning(e); } } else { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); } } else { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); } return INPUT; } /** * Resolve DOI, then depending on its state reactivate/undelete it if deleted. * * @param doi DOI to undelete * @param resource resource version to undelete * @param target target URI of DOI to undelete * * @throws org.gbif.ipt.service.UndeletNotAllowedException if undelete failed */ private void doUndeleteDOI(DOI doi, Resource resource, URI target) throws UndeletNotAllowedException { Preconditions.checkNotNull(registrationManager.getDoiService()); Preconditions.checkNotNull(doi); Preconditions.checkNotNull(resource); Preconditions.checkNotNull(target); try { DoiData doiData = registrationManager.getDoiService().resolve(doi); if (doiData != null && doiData.getStatus() != null) { if (doiData.getStatus().equals(DoiStatus.DELETED)) { LOG.info("Undeleting deleted DOI: " + doi.toString() + "..."); DataCiteMetadata dataCiteMetadata = DataCiteMetadataBuilder.createDataCiteMetadata(doi, resource); registrationManager.getDoiService().register(doi, target, dataCiteMetadata); String msg = getText("manage.overview.publishing.doi.undelete.success", new String[]{doi.toString()}); LOG.info(msg); addActionMessage(msg); } else { throw new UndeletNotAllowedException(UndeletNotAllowedException.Reason.DOI_NOT_DELETED, getText("manage.overview.publishing.doi.undelete.failed.badStatus", new String[] {doi.toString(), doiData.getStatus().toString()})); } } else { throw new UndeletNotAllowedException(UndeletNotAllowedException.Reason.DOI_DOES_NOT_EXIST, getText("manage.overview.publishing.doi.undelete.failed.notResolved", new String[] {doi.toString()})); } } catch (DoiException e) { throw new UndeletNotAllowedException(UndeletNotAllowedException.Reason.DOI_REGISTRATION_AGENCY_ERROR, getText("manage.overview.publishing.doi.undelete.failed.exception", new String[] {doi.toString(), e.getMessage()})); } } /** * Triggered by delete manager link on manage resource page. */ public String deleteManager() throws Exception { if (resource == null) { return NOT_FOUND; } User u = userManager.get(id); if (u == null || !resource.getManagers().contains(u)) { addActionError(getText("manage.overview.manager.not.available", new String[] {id})); } else { resource.getManagers().remove(u); addActionMessage(getText("manage.overview.user.removed", new String[] {u.getName()})); saveResource(); potentialManagers.add(u); } return execute(); } @Override public String execute() throws Exception { if (resource == null) { return NOT_FOUND; } return SUCCESS; } /** * Validate whether or not to show a confirmation message to overwrite the file(s) recently uploaded. * * @return true if a file exist in the user session. False otherwise. */ public boolean getConfirmOverwrite() { return session.get(Constants.SESSION_FILE) != null; } /** * On the manage resource page page, this map is used to populate the publishing intervals dropdown. * * @return update frequencies map */ public Map<String, String> getFrequencies() { return frequencies; } /** * Determine whether the metadata has been modified since the last publication. * * @param resource resource * * @return true if metadata has been modified since last publication, false otherwise */ public boolean setMetadataModifiedSinceLastPublication(@NotNull Resource resource) { if (resource.getLastPublished() == null) { return resource.getMetadataModified() != null; } else { if (resource.getMetadataModified() != null) { return resource.getMetadataModified().compareTo(resource.getLastPublished()) > 0; } } return false; } /** * Determine whether the source mappings has been modified since the last publication. * * @param resource resource * * @return true if source mappings has been modified since last publication, false otherwise */ public boolean setMappingsModifiedSinceLastPublication(@NotNull Resource resource) { if (resource.getLastPublished() == null) { return resource.getMappingsModified() != null; } else { if (resource.getMappingsModified() != null) { return resource.getMappingsModified().compareTo(resource.getLastPublished()) > 0; } } return false; } /** * Determine whether the sources have been modified since the last publication. * * @param resource resource * * @return true if sources have been modified since last publication, false otherwise */ public boolean setSourcesModifiedSinceLastPublication(@NotNull Resource resource) { if (resource.getLastPublished() == null) { return resource.getSourcesModified() != null; } else { if (resource.getSourcesModified() != null) { return resource.getSourcesModified().compareTo(resource.getLastPublished()) > 0; } } return false; } /** * @return true if there are something missing metadata, false otherwise. */ public boolean getMissingRegistrationMetadata() { return missingRegistrationMetadata; } /** * @return true if resource is missing valid publishing organisation, false otherwise. */ public boolean isMissingValidPublishingOrganisation() { return missingValidPublishingOrganisation; } public Date getNow() { return now; } public List<Organisation> getOrganisations() { return organisations; } public List<Extension> getPotentialCores() { return potentialCores; } public List<Extension> getPotentialExtensions() { return potentialExtensions; } public List<User> getPotentialManagers() { return potentialManagers; } public StatusReport getReport() { return report; } /** * Checks if the resource currently has minimum mandatory metadata filled in, and has been published prior. This * check is performed before registering with the GBIF Network. * * @param resource resource * @return true if the resource meets the minimum requirements to be published */ private boolean hasMinimumRegistryInfo(Resource resource) { if (missingMetadata) { return false; } return resource.isPublished(); } /** * Checks if the resource's publishing organisation is a valid organisation. The default * organisation named "No organisation" can only be used to designate the resource has no publishing organisation, * and cannot be used during registration. * * @return true if resource has a valid publishing organisation, or false otherwise */ public boolean hasValidPublishingOrganisation(Resource resource) { if (resource.getOrganisation() == null) { return false; } else if (resource.getOrganisation().getKey().equals(Constants.DEFAULT_ORG_KEY)) { return false; } else { return true; } } public boolean isMissingMetadata() { return missingMetadata; } public String locked() throws Exception { now = new Date(); if (report != null && report.isCompleted()) { addActionMessage(getText("manage.overview.resource.published")); return "cancel"; } return SUCCESS; } /** * Change the visibility of a resource from public to private. This operation cannot be performed, if the resource * has been assigned a DOI (DOI that is registered, not reserved), or if the resource has been registered with GBIF. */ public String makePrivate() throws Exception { if (resource == null) { return NOT_FOUND; } if (unpublish) { if (PublicationStatus.PUBLIC == resource.getStatus() && !resource.isAlreadyAssignedDoi()) { // makePrivate try { resourceManager.visibilityToPrivate(resource, this); addActionMessage(getText("manage.overview.changed.publication.status", new String[] {resource.getStatus() .toString()})); } catch (InvalidConfigException e) { LOG.error("Cant unpublish resource " + resource, e); } } else { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); } } else { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); } return execute(); } public String makePublic() throws Exception { if (resource == null) { return NOT_FOUND; } if (PublicationStatus.PRIVATE == resource.getStatus()) { try { resourceManager.visibilityToPublic(resource, this); addActionMessage(getText("manage.overview.changed.publication.status", new String[] {resource.getStatus() .toString()})); } catch (InvalidConfigException e) { LOG.error("Cant publish resource " + resource, e); } } else { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); } return execute(); } /** * Reserve a DOI for a resource. Constructs the DOI metadata document, and registers it without making it public. * * Can be done for any resource with any status. * * To accommodate resources with existing DOIs, this method checks if the resource has an existing DOI. * If the prefix of the existing DOI matches the prefix of the IPT primary DOI account, the DOI will be automatically * reused. Otherwise, the user must remove the DOI to reserve a new one, or update the DOI prefix used by the IPT * primary DOI account. * * Must add DOI to EML alternative identifiers, and set DOI as EML citation identifier (unpublished EML) * * DOI can be used on mapping core (datasetID field). * * If the resource has an existing DOI already, its an indication the resource is being transitioned to a new DOI. * In this case, the previous DOI must be replaced by the new DOI. */ public String reserveDoi() throws Exception { if (resource == null) { return NOT_FOUND; } if (reserveDoi) { // prevent reservation if no DOI registration agency account configured if (registrationManager.getDoiService() == null) { String msg = getText("manage.overview.doi.operation.failed.noAccount"); LOG.error(msg); addActionError(msg); return INPUT; } DOI existingDoi = findExistingDoi(resource); if ((existingDoi == null && resource.getIdentifierStatus() == IdentifierStatus.UNRESERVED && !resource .isAlreadyAssignedDoi()) || (resource.getIdentifierStatus() == IdentifierStatus.PUBLIC && resource .isAlreadyAssignedDoi())) { DOI doi = DOIUtils.mintDOI(doiAccount.getDoiRegistrationAgency(), doiAccount.getDoiPrefix()); LOG.info("Reserving " + doi.toString() + " for " + resource.getTitleAndShortname()); try { doReserveDOI(doi, resource); String msg = getText("manage.overview.publishing.doi.reserve.success", new String[] {doi.toString()}); LOG.info(msg); addActionMessage(msg); } catch (DoiExistsException e) { LOG.error("Failed to reserve " + doi.toString() + " because it exists already. Trying again...", e); reserveDoi(); } catch (InvalidMetadataException e) { String errorMsg = getText("manage.overview.publishing.doi.reserve.failed.metadata", new String[] {doi.toString(), e.getMessage()}); LOG.error(errorMsg, e); addActionError(errorMsg); } catch (DoiException e) { String errorMsg = getText("manage.overview.publishing.doi.reserve.failed", new String[]{doi.toString(), e.getMessage()}); LOG.error(errorMsg, e); addActionError(errorMsg); } } else if (existingDoi != null && resource.getIdentifierStatus() == IdentifierStatus.UNRESERVED && !resource .isAlreadyAssignedDoi()) { String prefixAllowed = doiAccount.getDoiPrefix(); // ensure the prefix of the DOI account configured for this IPT matches the prefix of the existing DOI if (prefixAllowed != null && existingDoi.getDoiName().startsWith(prefixAllowed.toLowerCase())) { try { DoiData doiData = registrationManager.getDoiService().resolve(existingDoi); // verify the existing DOI is either reserved or registered already if (doiData != null && doiData.getStatus().equals(DoiStatus.RESERVED)) { LOG.info("Assigning " + existingDoi.toString() + " (existing reserved DOI) to " + resource.getTitleAndShortname()); doReuseDOI(existingDoi, resource); } else if (doiData != null && doiData.getStatus().equals(DoiStatus.REGISTERED)) { LOG.info("Assigning " + existingDoi.toString() + " (existing registered DOI) to " + resource.getTitleAndShortname()); // the DOI is registered and should resolve to this resource's public homepage, so verify the homepage is publicly accessible LOG.debug("Resource " + resource.getShortname() + " has status=" + resource.getStatus()); if (!resource.isPubliclyAvailable()) { String errorMsg = getText("manage.overview.publishing.doi.reserve.failed.notPublic", new String[]{existingDoi.toString()}); LOG.error(errorMsg); addActionError(errorMsg); } else { // the DOI is registered and its target URI should be equal to the public resource homepage URI URI target = doiData.getTarget(); LOG.debug(existingDoi.toString() + " has target URI=" + target); URI homepage = cfg.getResourceUri(resource.getShortname()); if (target != null && target.equals(homepage)) { LOG.debug("Verified target URI of existing registered DOI is equal to public resource homepage URI"); doReuseDOI(existingDoi, resource); } else { String errorMsg = getText("manage.overview.publishing.doi.reserve.failed.invalid.target", new String[]{existingDoi.toString(), homepage.toString()}); LOG.error(errorMsg); addActionError(errorMsg); } } } else { String errorMsg = getText("manage.overview.publishing.doi.reserve.reused.failed", new String[] {existingDoi.toString()}); LOG.error(errorMsg); addActionError(errorMsg); } } catch (DoiException e) { String errorMsg = getText("manage.overview.publishing.doi.reserve.reused.failed.exception", new String[] {existingDoi.toString(), e.getMessage()}); LOG.error(errorMsg, e); addActionError(errorMsg); } } else { addActionError(getText("manage.overview.publishing.doi.reserve.notRreused", new String[] {existingDoi.toString(), prefixAllowed})); } } else { addActionWarning(getText("manage.overview.resource.doi.invalid.operation", new String[] {resource.getShortname(), resource.getIdentifierStatus().toString()})); } } else { addActionWarning(getText("manage.overview.resource.doi.invalid.operation", new String[] {resource.getShortname(), resource.getIdentifierStatus().toString()})); } return execute(); } /** * Do the changes to the resource, necessary to reuse an existing DOI. * * @param doi existing DOI to reuse * @param resource resource to apply changes to */ private void doReuseDOI(DOI doi, Resource resource) { resource.setDoi(doi); resource.setDoiOrganisationKey(registrationManager.findPrimaryDoiAgencyAccount().getKey()); resource.setIdentifierStatus(IdentifierStatus.PUBLIC_PENDING_PUBLICATION); resource.updateAlternateIdentifierForDOI(); resource.updateCitationIdentifierForDOI(); // set DOI as citation identifier saveResource(); String msg = getText("manage.overview.publishing.doi.reserve.reused", new String[] {doi.toString()}); LOG.info(msg); addActionMessage(msg); } /** * Reserve a DOI for the resource. * * @param doi DOI to reserve * @param resource resource to reserve DOI for * * @throws DoiExistsException if the DOI being reserved already exists so that reserving can be retried with new DOI */ private void doReserveDOI(DOI doi, Resource resource) throws DoiException { Preconditions.checkNotNull(registrationManager.getDoiService()); // reserve a new DOI for this resource using the primary DOI account, and update EML alternateIdentifier list DataCiteMetadata dataCiteMetadata = DataCiteMetadataBuilder.createDataCiteMetadata(doi, resource); registrationManager.getDoiService().reserve(doi, dataCiteMetadata); resource.setDoi(doi); resource.setDoiOrganisationKey(registrationManager.findPrimaryDoiAgencyAccount().getKey()); resource.setIdentifierStatus(IdentifierStatus.PUBLIC_PENDING_PUBLICATION); resource.updateAlternateIdentifierForDOI(); resource.updateCitationIdentifierForDOI(); // set DOI as citation identifier saveResource(); } /** * Delete a DOI for the resource. Optionally reassign DOI. * * @param reservedDoi DOI to delete * @param resource resource to delete DOI for (optional) * @param reassignedDoi DOI to reassign * * @throws DoiException if the deletion failed */ private void doDeleteReservedDOI(DOI reservedDoi, Resource resource, @Nullable DOI reassignedDoi) throws DoiException { Preconditions.checkNotNull(registrationManager.getDoiService()); // safeguard - prevent deleting existing registered DOIs DoiData doiData = registrationManager.getDoiService().resolve(reservedDoi); if (doiData != null && doiData.getStatus() != null && !doiData.getStatus().equals(DoiStatus.REGISTERED)) { LOG.debug("Deleting reserved DOI=" + reservedDoi.toString()); // delete reserved DOI for this resource registrationManager.getDoiService().delete(reservedDoi); } else { LOG.debug("Deleting reserved DOI bypassed because this is an existing registered DOI: " + reservedDoi.toString()); } // reset resource DOI resource.setIdentifierStatus(IdentifierStatus.UNRESERVED); resource.updateAlternateIdentifierForDOI(); // remove DOI from list of alternate ids resource.updateCitationIdentifierForDOI(); // unset DOI as citation identifier resource.setDoi(null); resource.setDoiOrganisationKey(null); // reassign resource DOI if necessary if ((reassignedDoi != null)) { resource.setIdentifierStatus(IdentifierStatus.PUBLIC); resource.setDoi(reassignedDoi); resource.updateAlternateIdentifierForDOI(); // add DOI to list of alternate ids resource.updateCitationIdentifierForDOI(); // set DOI as citation identifier resource.setDoiOrganisationKey(registrationManager.findPrimaryDoiAgencyAccount().getKey()); } saveResource(); } /** * Return the existing DOI assigned to this resource. An existing DOI is set as the citation identifier. * * @return the existing DOI assigned to this resource, or null if none was found. */ @VisibleForTesting public DOI findExistingDoi(Resource resource) { if (resource != null && resource.getEml() != null) { Citation citation = resource.getEml().getCitation(); if (citation != null) { // be sure to trim identifier, user may have added extra space which causes resolve to fail! if (DOI.isParsable(StringUtils.trimToNull(citation.getIdentifier()))) { return new DOI(citation.getIdentifier()); } } } return null; } /** * Deletes a reserved DOI. * * Can only be done to a resource whose DOI is reserved but not public. If the resource previously had been assigned * a DOI, that DOI is reassigned. */ public String deleteDoi() throws Exception { if (resource == null) { return NOT_FOUND; } if (registrationManager.getDoiService() == null) { String msg = getText("manage.overview.doi.operation.failed.noAccount"); LOG.error(msg); addActionError(msg); } if (deleteDoi) { DOI reservedDoi = resource.getDoi(); if (reservedDoi != null && resource.getIdentifierStatus() == IdentifierStatus.PUBLIC_PENDING_PUBLICATION) { DOI assignedDoi = resource.getAssignedDoi(); if (assignedDoi != null) { LOG.info("Deleting reserved " + reservedDoi.toString() + " and reassigning " + assignedDoi.toString()); try { // delete reserved DOI, reassign previous DOI to resource, and update EML alternateIdentifier list doDeleteReservedDOI(reservedDoi, resource, assignedDoi); String msg = getText("manage.overview.publishing.doi.delete.reassign.success", new String[] {reservedDoi.toString(), assignedDoi.toString()}); LOG.info(msg); addActionMessage(msg); } catch (DoiException e) { String errorMsg = getText("manage.overview.publishing.doi.delete.failed.exception", new String[] {resource.getDoi().toString(), e.getMessage()}); LOG.error(errorMsg, e); addActionError(errorMsg); } } else { LOG.info("Deleting reserved " + reservedDoi.toString()); try { // delete reserved DOI, and update EML alternateIdentifier list doDeleteReservedDOI(reservedDoi, resource, null); String msg = getText("manage.overview.publishing.doi.delete.success", new String[] {reservedDoi.toString()}); LOG.info(msg); addActionMessage(msg); } catch (DoiException e) { String errorMsg = getText("manage.overview.publishing.doi.delete.failed.exception", new String[]{resource.getDoi().toString(), e.getMessage()}); LOG.error(errorMsg, e); addActionError(errorMsg); } } } else { addActionWarning(getText("manage.overview.resource.doi.invalid.operation", new String[] {resource.getShortname(), resource.getIdentifierStatus().toString()})); } } else { addActionWarning(getText("manage.overview.resource.doi.invalid.operation", new String[] {resource.getShortname(), resource.getIdentifierStatus().toString()})); } return execute(); } /** * Populate frequencies map, representing the publishing interval choices uses have when configuring * auto-publishing. The frequencies list is derived from an XML vocabulary, and will contain values in the requested * locale, defaulting to English. */ private void populateFrequencies() { frequencies = new LinkedHashMap<String, String>(); if (resource.usesAutoPublishing()) { frequencies.put("off", getText("autopublish.off")); } else { frequencies.put("", getText("autopublish.interval")); } // update frequencies list, that qualify for auto-publishing Map<String, String> filteredFrequencies = vocabManager.getI18nVocab(Constants.VOCAB_URI_UPDATE_FREQUENCIES, getLocaleLanguage(), false); MapUtils.removeNonMatchingKeys(filteredFrequencies, MaintenanceUpdateFrequency.NON_ZERO_DAYS_UPDATE_PERIODS); frequencies.putAll(filteredFrequencies); } @Override public void prepare() { super.prepare(); if (resource != null) { // refresh archive report updateReport(); // get potential new managers potentialManagers = userManager.list(Role.Publisher); potentialManagers.addAll(userManager.list(Role.Manager)); // remove already associated ones for (User u : resource.getManagers()) { potentialManagers.remove(u); } // enabled registry organisations organisations = registrationManager.list(); // remove all DwC mappings with 0 terms mapped // this is important to do before populating potential extensions since an empty mapping to occurrence can // indicate the resource hasCore is true for (ExtensionMapping em : resource.getCoreMappings()) { if (em.getFields().isEmpty()) { resource.deleteMapping(em); } } // Does the resource already have a source mapped to core type? // The core type can be set from the basic metadata page, and determines which core extensions to show String coreRowType = resource.getCoreRowType(); potentialCores = Lists.newArrayList(); potentialExtensions = Lists.newArrayList(); if (!resource.getSources().isEmpty()) { if (coreRowType != null) { Extension core = extensionManager.get(coreRowType); if (core == null) { addActionError(getText("manage.overview.no.DwC.extension", new String[] {coreRowType})); } else { // core always appears first in list potentialCores.add(core); // are there other cores that can be used as extensions for this core? List<Extension> otherCores = extensionManager.listCore(coreRowType); potentialExtensions.addAll(otherCores); // are there other extensions suited for this core? List<Extension> others = extensionManager.list(coreRowType); potentialExtensions.addAll(others); } } else { potentialCores = extensionManager.listCore(); if (potentialCores.isEmpty()) { addActionError(getText("manage.overview.no.DwC.extensions")); } } } // check EML missingMetadata = !emlValidator.isValid(resource, null); // check resource has been assigned a valid publishing organisation missingValidPublishingOrganisation = !hasValidPublishingOrganisation(resource); // check resource meets all the conditions required in order to be registered missingRegistrationMetadata = !hasMinimumRegistryInfo(resource); // check the metadata has been modified since the last publication metadataModifiedSinceLastPublication = setMetadataModifiedSinceLastPublication(resource); // check the source mappings has been modified since the last publication mappingsModifiedSinceLastPublication = setMappingsModifiedSinceLastPublication(resource); // check if the sources have been modified since the last publication sourcesModifiedSinceLastPublication = setSourcesModifiedSinceLastPublication(resource); // populate frequencies map populateFrequencies(); } } /** * Updates report to be displayed on overview page. */ private void updateReport() { report = resourceManager.status(resource.getShortname()); } /** * Executes the instruction to publish the resource, handles update GBIF registrations, and handles all DOI * registrations and DOI update registrations. * </br> * If the resource is public, and its DOI is reserved (not public), its DOI is registered if publication is successful * otherwise the previous version has to be restored. This is done for all new major versions (e.g. the first time a * DOI is assigned to the resource, and to transition the resource to a new DOI in case of major scientific changes). * It is impossible to transition a DOI from one prefix to another, so a check should ensure this doesn't happen. * </br> * If the resource is public, and its DOI is registered (public), its DOI registration is updated if publication is * successful otherwise the previous version has to be restored. This is done for all new minor versions. * </br> * Publication should fail, and the previous version restored if the DOI operation fails. * </br> * The method must check if resource has been configured to be auto-published. * </br> * In addition, the method must clear the processFailures for the resource being published. If a resource has * exceeded the maximum number of failed publish events during auto-publication, auto-publication for the resource is * suspended. By publishing the resource manually, it is assumed the manager is trying to debug the problem. Without * this safeguard in place, a resource can auto-publish in an endless number of failures. * * @return Struts2 result string * * @throws Exception if method fails */ public String publish() throws Exception { if (resource == null) { return NOT_FOUND; } if (publish) { // prevent publishing if resource is registered but it hasn't been assigned a GBIF-supported license if (resource.isRegistered() && !resource.isAssignedGBIFSupportedLicense()) { String msg = getText("manage.overview.prevented.resource.publishing.noGBIFLicense"); addActionError(msg); LOG.error(msg); return INPUT; } // prevent publishing if publishing will trigger a DOI operation, but no DOI agency account has been activated yet if (resource.getDoi() != null && resource.isPubliclyAvailable()) { if (registrationManager.getDoiService() == null) { String msg = getText("manage.overview.doi.operation.failed.noAccount"); LOG.error(msg); addActionError(msg); return INPUT; } // prevent publishing if DOI does not resolve/exist, or if DOI agency account cannot resolve this DOI try { DoiData doiData = registrationManager.getDoiService().resolve(resource.getDoi()); if (doiData != null && doiData.getStatus() != null) { if (doiData.getStatus().compareTo(DoiStatus.RESERVED) == 0 || doiData.getStatus().compareTo(DoiStatus.REGISTERED) == 0) { LOG.info("Pre-publication check: successfully resolved " + resource.getDoi().toString()); } else { String errorMsg = getText("manage.overview.publishing.doi.publish.check.registered.failed", new String[] {resource.getDoi().toString(), doiData.getStatus().toString()}); LOG.error(errorMsg); addActionError(errorMsg); return INPUT; } } else { String errorMsg = getText("manage.overview.publishing.doi.publish.check.existing.failed", new String[] {resource.getDoi().toString()}); LOG.error(errorMsg); addActionError(errorMsg); return INPUT; } } catch (DoiException e) { String errorMsg = getText("manage.overview.publishing.doi.publish.check.existing.failed.exception", new String[] {resource.getDoi().toString(), e.getMessage()}); LOG.error(errorMsg, e); addActionError(errorMsg); return INPUT; } } // clear the processFailures for the resource, allowing auto-publication to proceed if (resourceManager.getProcessFailures().containsKey(resource.getShortname())) { logProcessFailures(resource); LOG.info("Clearing publish event failures for resource: " + resource.getTitleAndShortname()); resourceManager.getProcessFailures().removeAll(resource.getShortname()); } // look for parameters publication mode and publication frequency String pm = StringUtils.trimToNull(req.getParameter(Constants.REQ_PARAM_PUBLICATION_MODE)); if (!Strings.isNullOrEmpty(pm)) { try { // auto-publishing being turned OFF if (PublicationMode.AUTO_PUBLISH_OFF == PublicationMode.valueOf(pm) && resource.usesAutoPublishing()) { resourceManager.publicationModeToOff(resource); } // auto-publishing being turned ON, or auto-publishing settings being updated else { String pf = StringUtils.trimToNull(req.getParameter(Constants.REQ_PARAM_PUBLICATION_FREQUENCY)); if (!Strings.isNullOrEmpty(pf)) { resource.setUpdateFrequency(pf); resource.setPublicationMode(PublicationMode.valueOf(pm)); } else { LOG.debug("No change to auto-publishing settings"); } } } catch (IllegalArgumentException e) { LOG.error("Exception encountered while parsing parameters: " + e.getMessage(), e); } finally { // update frequencies map populateFrequencies(); } } BigDecimal nextVersion = new BigDecimal(resource.getNextVersion().toPlainString()); // set resource's change summary as entered in confirm popup (defaults to empty string) resource.setChangeSummary(getSummary()); try { // publish a new version of the resource if (resourceManager.publish(resource, nextVersion, this)) { addActionMessage(getText("publishing.started", new String[] {String.valueOf(nextVersion), resource.getShortname()})); // refresh archive report updateReport(); return PUBLISHING; } else { // show action warning there is no source data and mapping, as long as resource isn't metadata-only if (resource.getCoreType() != null && !resource.getCoreType().equalsIgnoreCase(Constants.DATASET_TYPE_METADATA_IDENTIFIER)) { addActionWarning(getText("manage.overview.data.missing")); } missingRegistrationMetadata = !hasMinimumRegistryInfo(resource); metadataModifiedSinceLastPublication = setMetadataModifiedSinceLastPublication(resource); mappingsModifiedSinceLastPublication = setMappingsModifiedSinceLastPublication(resource); // refresh archive report updateReport(); return SUCCESS; } } catch (PublicationException e) { if (PublicationException.TYPE.LOCKED == e.getType()) { addActionError(getText("manage.overview.resource.being.published", new String[] {resource.getTitleAndShortname()})); } else { // alert user publication failed addActionError(getText("publishing.failed", new String[] {String.valueOf(nextVersion), resource.getShortname(), e.getMessage()})); // restore the previous version since publication was unsuccessful resourceManager.restoreVersion(resource, nextVersion, this); // keep track of how many failures on auto publication have happened resourceManager.getProcessFailures().put(resource.getShortname(), new Date()); } } catch (InvalidConfigException e) { // with this type of error, the version cannot be rolled back - just alert user publication failed String msg = getText("publishing.failed", new String[] {String.valueOf(nextVersion), resource.getShortname(), e.getMessage()}); LOG.error(msg, e); addActionError(msg); } } else { // just return the user to the manage resources page return HOME; } return ERROR; } public String registerResource() throws Exception { if (resource == null) { return NOT_FOUND; } // prevent registration if last published version was not public (at the time of publishing) if (!resource.isLastPublishedVersionPublic()) { String msg = getText("manage.overview.failed.resource.registration.notPublic"); addActionError(msg); LOG.error(msg); return INPUT; } // prevent registration if last published version was not assigned a GBIF-supported license // this requirement applies to occurrence datasets, or datasets with associated occurrence records if (resource.hasOccurrenceMapping() && !isLastPublishedVersionAssignedGBIFSupportedLicense(resource)) { String msg = getText("manage.overview.prevented.resource.registration.noGBIFLicense"); addActionError(msg); LOG.error(msg); return INPUT; } if (PublicationStatus.PUBLIC == resource.getStatus()) { if (unpublish) { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); } else { // plain managers are not allowed to register a resource if (getCurrentUser().hasRegistrationRights()) { Organisation org = null; try { org = resource.getOrganisation(); // http://code.google.com/p/gbif-providertoolkit/issues/detail?id=594 // It is safe to test the Organisation here. A resource cannot be registered without an organisation being // provided, and the issue 594 is an example how one can produce this sequence of events. A more // robust improvement might be to submit a state transition from the form "makePublic", // "makePrivate" which would be more atomic. if (org == null) { return execute(); } // perform registration resourceManager.register(resource, org, registrationManager.getIpt(), this); } catch (InvalidConfigException e) { if (e.getType() == InvalidConfigException.TYPE.INVALID_RESOURCE_MIGRATION) { String msg = getText("manage.resource.migrate.failed"); // concatenate reason msg = (Strings.isNullOrEmpty(msg)) ? e.getMessage() : msg + ": " + e.getMessage(); addActionError(msg); LOG.error(msg); } else { String msg = getText("manage.overview.failed.resource.registration"); addActionError(msg); LOG.error(msg); } } catch (RegistryException e) { // log as specific error message as possible about why the Registry error occurred String msg = RegistryException.logRegistryException(e.getType(), this); // add error message about Registry error addActionError(msg); LOG.error(msg); // add error message that explains the consequence of the Registry error msg = getText("manage.overview.failed.resource.registration"); addActionError(msg); LOG.error(msg); } } else { StringBuilder sb = new StringBuilder(); sb.append(getText("manage.resource.status.registration.forbidden")); sb.append(" "); sb.append(getText("manage.resource.role.change")); addActionError(sb.toString()); } } } else { addActionWarning(getText("manage.overview.resource.invalid.operation", new String[] {resource.getShortname(), resource.getStatus().toString()})); } return execute(); } /** * Used before registering current (last) published version. * * @return true if the last published version has been assigned a GBIF-supported license, false otherwise */ public boolean isLastPublishedVersionAssignedGBIFSupportedLicense(Resource resource) { List<VersionHistory> history = resource.getVersionHistory(); if (!history.isEmpty()) { VersionHistory latestVersionHistory = history.get(0); BigDecimal latestVersion = new BigDecimal(latestVersionHistory.getVersion()); File emlFile = cfg.getDataDir().resourceEmlFile(resource.getShortname(), latestVersion); if (emlFile.exists()) { try { LOG.debug("Loading EML from file: " + emlFile.getAbsolutePath()); InputStream in = new FileInputStream(emlFile); Eml eml = EmlFactory.build(in); if (eml.parseLicenseUrl() != null) { LOG.debug("Checking if license (URL=" + eml.parseLicenseUrl() + ") is supported by GBIF.."); return Constants.GBIF_SUPPORTED_LICENSES.contains(eml.parseLicenseUrl()); } } catch (Exception e) { LOG.error( "Failed to check if last published version of resource has been assigned a GBIF-supported license: " + e .getMessage(), e); } } } return false; } /** * @return license URL assigned to the last published version or null if none was assigned */ public String getLastPublishedVersionAssignedLicense(Resource resource) { List<VersionHistory> history = resource.getVersionHistory(); if (!history.isEmpty()) { VersionHistory latestVersionHistory = history.get(0); BigDecimal latestVersion = new BigDecimal(latestVersionHistory.getVersion()); File emlFile = cfg.getDataDir().resourceEmlFile(resource.getShortname(), latestVersion); if (emlFile.exists()) { try { LOG.debug("Loading EML from file: " + emlFile.getAbsolutePath()); InputStream in = new FileInputStream(emlFile); Eml eml = EmlFactory.build(in); return eml.parseLicenseUrl(); } catch (Exception e) { LOG.error( "Failed to check if last published version of resource has been assigned a GBIF-supported license: " + e .getMessage(), e); } } } return null; } /** * To hold the state transition request, so the same request triggered purely by a URL will not work. * * @param unpublish form variable */ public void setUnpublish(String unpublish) { this.unpublish = StringUtils.trimToNull(unpublish) != null; } /** * To hold the identifier state transition request, so the same request triggered purely by a URL will not work. * * @param reserveDoi form variable */ public void setReserveDoi(String reserveDoi) { this.reserveDoi = StringUtils.trimToNull(reserveDoi) != null; } /** * To hold the identifier state transition request, so the same request triggered purely by a URL will not work. * * @param deleteDoi form variable */ public void setDeleteDoi(String deleteDoi) { this.deleteDoi = StringUtils.trimToNull(deleteDoi) != null; } /** * To hold the state transition request, so the same request triggered purely by a URL will not work. * * @param delete form variable */ public void setDelete(String delete) { this.delete = StringUtils.trimToNull(delete) != null; } /** * To hold the state transition request, so the same request triggered purely by a URL will not work. * * @param undelete form variable */ public void setUndelete(String undelete) { this.undelete = StringUtils.trimToNull(undelete) != null; } /** * To hold the publish request, so the same request triggered purely by a URL will not work. * * @param publish form variable */ public void setPublish(String publish) { this.publish = StringUtils.trimToNull(publish) != null; } /** * Log how many times publication has failed for a resource, also detailing when the failures occurred. * * @param resource resource */ @VisibleForTesting protected void logProcessFailures(Resource resource) { StringBuilder sb = new StringBuilder(); sb.append("Resource ["); sb.append(resource.getTitleAndShortname()); sb.append("] has "); if (resourceManager.getProcessFailures().containsKey(resource.getShortname())) { List<Date> failures = resourceManager.getProcessFailures().get(resource.getShortname()); sb.append(String.valueOf(failures.size())); sb.append(" failed publications on: "); Iterator<Date> iter = failures.iterator(); while (iter.hasNext()) { sb.append(DateFormatUtils.format(iter.next(), "yyyy-MM-dd HH:mm:SS")); if (iter.hasNext()) { sb.append(", "); } else { sb.append("."); } } } else { sb.append("0 failed publications"); } LOG.debug(sb.toString()); } /** * Called from manage resource page. * * @return true if metadata has been modified since last publication, false otherwise */ public boolean isMetadataModifiedSinceLastPublication() { return metadataModifiedSinceLastPublication; } /** * Called from manage resource page. * * @return true if source mappings has been modified since last publication, false otherwise. */ public boolean isMappingsModifiedSinceLastPublication() { return mappingsModifiedSinceLastPublication; } /** * Called from manage resource page. * * @return true if sources have been modified since last publication, false otherwise. */ public boolean isSourcesModifiedSinceLastPublication() { return sourcesModifiedSinceLastPublication; } /** * Preview the first "peekRows" number of rows for a given mapping. The mapping is specified by the combination * of rowType and mapping ID. */ public String peek() { if (resource == null) { return NOT_FOUND; } peek = Lists.newArrayList(); columns = Lists.newArrayList(); Exception exception = null; List<TaskMessage> messages = new LinkedList<TaskMessage>(); // find the rowType Term rowType = null; if (id != null) { rowType = TERM_FACTORY.findTerm(id); } if (rowType != null && mid != null) { ExtensionMapping mapping = resource.getMappings(id).get(mid); if (mapping != null) { try { GenerateDwca worker = dwcaFactory.create(resource, this); worker.report(); File tmpDir = Files.createTempDir(); worker.setDwcaFolder(tmpDir); Archive archive = new Archive(); worker.setArchive(archive); // create the data file inside the temp directory worker.addDataFile(Lists.newArrayList(mapping), PEEK_ROWS); // preview the data file, by writing header and rows File[] files = tmpDir.listFiles(); if (files != null && files.length > 0) { // file either represents a core file or an extension ArchiveFile core = archive.getCore(); ArchiveFile ext = archive.getExtension(rowType); String delimiter = (core == null) ? ext.getFieldsTerminatedBy() : core.getFieldsTerminatedBy(); Character quotes = (core == null) ? ext.getFieldsEnclosedBy() : core.getFieldsEnclosedBy(); int headerRows = (core == null) ? ext.getIgnoreHeaderLines() : core.getIgnoreHeaderLines(); CSVReader reader = CSVReaderFactory.build(files[0], CHARACTER_ENCODING, delimiter, quotes, headerRows); while (reader.hasNext()) { peek.add(reader.next()); if (columns.isEmpty()) { columns = Arrays.asList(reader.header); } } } else { messages.add(new TaskMessage(Level.ERROR, getText("mapping.preview.not.found"))); } } catch (Exception e) { exception = e; messages.add(new TaskMessage(Level.ERROR, getText("mapping.preview.error", new String[] {e.getMessage()}))); } } else { messages.add(new TaskMessage(Level.ERROR, getText("mapping.preview.mapping.not.found", new String[] {id, String.valueOf(mid)}))); } } else { messages.add(new TaskMessage(Level.ERROR, getText("mapping.preview.bad.request"))); } // add messages to those collected while generating preview if (!messages.isEmpty()) { report.getMessages().addAll(messages); } report = (exception == null) ? new StatusReport(true, "succeeded", report.getMessages()) : new StatusReport(exception, "failed", messages); return SUCCESS; } public List<String[]> getPeek() { return peek; } public List<String> getColumns() { return columns; } public Integer getMid() { return mid; } public void setMid(Integer mid) { this.mid = mid; } @Override public void report(String resourceShortname, StatusReport report) { this.report = report; } /** * @return the organisation associated to this IPT that has a DOI registration agency account, which has been * activated enabling it to register DOIs for datasets. */ public Organisation getOrganisationWithPrimaryDoiAccount() { return doiAccount; } /** * * @param summary change summary for new published version, entered by the user in the confirm dialog defaulting to * empty string */ public void setSummary(String summary) { this.summary = StringUtils.trimToEmpty(summary); } /** * @return the change summary for new published version, entered by the user in the confirm dialog */ public String getSummary() { return Strings.emptyToNull(summary); } }