org.gbif.ipt.model.Resource.java Source code

Java tutorial

Introduction

Here is the source code for org.gbif.ipt.model.Resource.java

Source

package org.gbif.ipt.model;

import org.gbif.api.model.common.DOI;
import org.gbif.dwc.terms.Term;
import org.gbif.dwc.terms.TermFactory;
import org.gbif.ipt.config.Constants;
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.AlreadyExistingException;
import org.gbif.ipt.utils.ResourceUtils;
import org.gbif.metadata.eml.Agent;
import org.gbif.metadata.eml.Citation;
import org.gbif.metadata.eml.Eml;
import org.gbif.metadata.eml.MaintenanceUpdateFrequency;

import java.io.Serializable;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
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.Function;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import static com.google.common.base.Objects.equal;

/**
 * The main class to represent an IPT resource.
 * Its enumerated type property defines the kind of resource (Metadata, Checklist, Occurrence)
 * A resource can be identified by its short name which has to be unique within an IPT instance.
 */
public class Resource implements Serializable, Comparable<Resource> {

    public enum CoreRowType {
        OCCURRENCE, CHECKLIST, SAMPLINGEVENT, METADATA, OTHER
    }

    private static Logger log = Logger.getLogger(Resource.class);

    private static final TermFactory TERM_FACTORY = TermFactory.instance();

    private static final long serialVersionUID = 3832626162173352190L;
    private String shortname; // unique
    private Eml eml = new Eml();
    private String coreType;
    private String subtype;
    // update frequency
    private MaintenanceUpdateFrequency updateFrequency;
    // publication status
    private PublicationStatus status = PublicationStatus.PRIVATE;
    // publication mode
    private PublicationMode publicationMode;
    // is resource citation to be auto-generated?
    private boolean citationAutoGenerated;
    // resource version and eml version are the same
    private BigDecimal emlVersion;
    // resource version replaced
    private BigDecimal replacedEmlVersion;
    // last time resource was successfully published
    private Date lastPublished;
    // next time resource is scheduled to be pubished
    private Date nextPublished;
    // core record count
    private int recordsPublished = 0;
    // record counts by extension: Map<rowType, count>
    private Map<String, Integer> recordsByExtension = Maps.newHashMap();
    // registry data - only exists when status=REGISTERED
    private UUID key;
    private Organisation organisation;
    // resource meta-metadata
    private User creator;
    private Date created;
    private User modifier;
    private Date modified;
    private Date metadataModified;
    private Date mappingsModified;
    private Date sourcesModified;
    private Set<User> managers = new HashSet<User>();
    // mapping configs
    private Set<Source> sources = new HashSet<Source>();
    private List<ExtensionMapping> mappings = Lists.newArrayList();

    private String changeSummary;
    private List<VersionHistory> versionHistory = Lists.newLinkedList();

    private IdentifierStatus identifierStatus = IdentifierStatus.UNRESERVED;
    private DOI doi;
    private UUID doiOrganisationKey;

    public void addManager(User manager) {
        if (manager != null) {
            this.managers.add(manager);
        }
    }

    /**
     * Add new VersionHistory, as long as a VersionHistory with same version hasn't been added yet. The new version
     * gets added at the top of the list.
     *
     * @param history VersionHistory to add
     */
    public void addVersionHistory(VersionHistory history) {
        Preconditions.checkNotNull(history);
        boolean exists = false;
        for (VersionHistory vh : getVersionHistory()) {
            if (vh.getVersion().equals(history.getVersion())) {
                exists = true;
            }
        }
        if (!exists) {
            log.debug("Adding new version history: " + history.getVersion());
            versionHistory.add(0, history);
        }
    }

    /**
     * Remove a VersionHistory with specific version.
     *
     * @param version version of VersionHistory to remove
     */
    public void removeVersionHistory(BigDecimal version) {
        if (version != null) {
            Iterator<VersionHistory> iter = getVersionHistory().iterator();
            while (iter.hasNext()) {
                BigDecimal historyVersion = new BigDecimal(iter.next().getVersion());
                if (version.compareTo(historyVersion) == 0) {
                    iter.remove();
                }
            }
        }
    }

    /**
     * Find and return a VersionHistory with specific version.
     *
     * @param version version of VersionHistory searched for
     *
     * @return VersionHistory with specific version or null if not found
     */
    public VersionHistory findVersionHistory(BigDecimal version) {
        if (version != null) {
            for (VersionHistory vh : getVersionHistory()) {
                if (version.toPlainString().equals(new BigDecimal(vh.getVersion()).toPlainString())) {
                    return vh;
                }
            }
        }
        return null;
    }

    /**
     * Adds a new extension mapping to the resource. For non core extensions a core extension must exist already.
     * It returns the list index for this mapping according to getMappings(rowType)
     *
     * @return list index corresponding to getMappings(rowType) or null if the mapping couldnt be added
     *
     * @throws IllegalArgumentException if no core mapping exists when adding a non core mapping
     */
    public Integer addMapping(@Nullable ExtensionMapping mapping) throws IllegalArgumentException {
        if (mapping != null && mapping.getExtension() != null) {
            if (!mapping.isCore() && !hasCore()) {
                throw new IllegalArgumentException("Cannot add extension mapping before a core mapping exists");
            }
            Integer index = getMappings(mapping.getExtension().getRowType()).size();
            this.mappings.add(mapping);
            return index;
        }
        return null;
    }

    public void addSource(Source src, boolean allowOverwrite) throws AlreadyExistingException {
        // make sure we talk about the same resource
        src.setResource(this);
        if (!allowOverwrite && sources.contains(src)) {
            throw new AlreadyExistingException();
        }
        if (allowOverwrite && sources.contains(src)) {
            // If source file is going to be overwritten, it should be actually re-add it.
            sources.remove(src);
            // Changing the SourceBase in the ExtensionMapping object from the mapping list.
            for (ExtensionMapping ext : this.getMappings()) {
                if (ext.getSource().equals(src)) {
                    ext.setSource(src);
                }
            }
        }
        sources.add(src);
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    public int compareTo(Resource o) {
        return shortname.compareToIgnoreCase(o.shortname);
    }

    /**
     * Delete a Resource's mapping. If the mapping gets successfully deleted, and the mapping is a core type mapping,
     * and there are no additional core type mappings, all other mappings are also cleared.
     *
     * @param mapping ExtensionMapping
     *
     * @return if deletion was successful or not
     */
    public boolean deleteMapping(ExtensionMapping mapping) {
        if (mapping != null && mappings.contains(mapping)) {
            // what's the core row type?
            String coreRowType = getCoreRowType();
            // is this the resource's core mapping?
            if (coreRowType != null && coreRowType.equalsIgnoreCase(mapping.getExtension().getRowType())) {
                // are there multiple core mappings?
                List<ExtensionMapping> coreMappings = getMappings(coreRowType);
                if (coreMappings.size() > 1) {
                    // if it's the first mapping in the list, swap it with next mapping to retain coreType
                    if (mappings.indexOf(mapping) == 0) {
                        ExtensionMapping next = coreMappings.get(1);
                        int nextIndex = mappings.indexOf(next);
                        log.debug("Swapping first core mapping with next core mapping with index#"
                                + String.valueOf(nextIndex));
                        Collections.swap(mappings, 0, nextIndex);
                    }
                    log.debug("Deleting core mapping...");
                    return mappings.remove(mapping);
                }
                // if this was the only core mapping, delete all mappings
                else {
                    log.debug("Deleting only core mapping and thus clearing all mappings...");
                    mappings.clear();
                    return true;
                }
            } else {
                log.debug("Deleting non-core mapping...");
                return mappings.remove(mapping);
            }
        }
        log.debug("Mapping was null, or resource no longer has this mapping, thus it could not be deleted!");
        return false;
    }

    public boolean deleteSource(Source src) {
        boolean result = false;
        if (src != null) {
            result = sources.remove(src);
            // also remove existing mappings
            List<ExtensionMapping> ems = new ArrayList<ExtensionMapping>(mappings);
            for (ExtensionMapping em : ems) {
                if (em.getSource() != null && src.equals(em.getSource())) {
                    deleteMapping(em);
                    log.debug("Cascading source delete to mapping " + em.getExtension().getTitle());
                }
            }
        }
        return result;
    }

    /**
     * @return true if Source is mapped, false otherwise
     */
    public boolean hasMappedSource(Source src) {
        if (src != null) {
            for (ExtensionMapping em : new ArrayList<ExtensionMapping>(mappings)) {
                if (em.getSource() != null && src.equals(em.getSource())) {
                    log.debug("Source mapped to " + em.getExtension().getTitle());
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof Resource)) {
            return false;
        }
        Resource o = (Resource) other;
        return equal(shortname, o.shortname);
    }

    /**
     * @return all core mappings, excluding extension mappings with core row types
     */
    public List<ExtensionMapping> getCoreMappings() {
        List<ExtensionMapping> cores = new ArrayList<ExtensionMapping>();
        String coreRowType = getCoreRowType();
        for (ExtensionMapping m : mappings) {
            if (m.isCore() && coreRowType != null && coreRowType.equalsIgnoreCase(m.getExtension().getRowType())) {
                cores.add(m);
            }
        }
        return cores;
    }

    /**
     * @return the row type of the first core extension mapping, which always determines the core row type
     */
    @Nullable
    public String getCoreRowType() {
        for (ExtensionMapping m : mappings) {
            if (m.isCore()) {
                return m.getExtension().getRowType();
            }
        }
        return null;
    }

    /**
     * At first the core type can be set during resource creation or on the basic metadata page. But once
     * a core mapping has been done, it is derived from the core mapping.
     *
     * @return the core type.
     */
    @Nullable
    public String getCoreType() {
        String coreRowType = getCoreRowType();
        if (coreRowType != null) {
            if (coreRowType.equalsIgnoreCase(Constants.DWC_ROWTYPE_TAXON)) {
                coreType = StringUtils.capitalize(CoreRowType.CHECKLIST.toString());
            } else if (coreRowType.equalsIgnoreCase(Constants.DWC_ROWTYPE_OCCURRENCE)) {
                coreType = StringUtils.capitalize(CoreRowType.OCCURRENCE.toString());
            } else if (coreRowType.equalsIgnoreCase(Constants.DWC_ROWTYPE_EVENT)) {
                coreType = StringUtils.capitalize(CoreRowType.SAMPLINGEVENT.toString());
            } else {
                coreType = StringUtils.capitalize(CoreRowType.OTHER.toString());
            }
        }
        return coreType;
    }

    public Term getCoreTypeTerm() {
        List<ExtensionMapping> cores = getCoreMappings();
        if (!cores.isEmpty()) {
            return TERM_FACTORY.findTerm(cores.get(0).getExtension().getRowType());
        }
        return null;
    }

    public Date getCreated() {
        return created;
    }

    @NotNull
    public User getCreator() {
        return creator;
    }

    public Eml getEml() {
        return eml;
    }

    /**
     * Get resource version. Same as EML version.
     *
     * @return resource version
     */
    @NotNull
    public BigDecimal getEmlVersion() {
        return (emlVersion == null) ? eml.getEmlVersion() : emlVersion;
    }

    /**
     * Get the next resource version. If the resource has never been published, the next resource version
     * is 1.0. If no new DOI has been reserved for the resource, the version is bumped by a minor resource version.
     * If a new DOI has been reserved for the resource, and the resource's visibility is public, the version is bumped by
     * a major resource version.
     *
     * @return next resource version
     */
    @NotNull
    public BigDecimal getNextVersion() {
        // first publication retrieve existing version
        if (lastPublished == null) {
            return getEml().getEmlVersion();
        }
        // There are two cases that warrant a new major version, provided a doi has been reserved for resource
        // #1: no DOI has been assigned yet, and resource's visibility is public (or registered)
        // #2: a DOI has been assigned already
        if (doi != null && identifierStatus == IdentifierStatus.PUBLIC_PENDING_PUBLICATION) {
            if (!isAlreadyAssignedDoi()
                    && (status == PublicationStatus.PUBLIC || status == PublicationStatus.REGISTERED)) {
                return getEml().getNextEmlVersionAfterMajorVersionChange();
            } else if (isAlreadyAssignedDoi()) {
                return getEml().getNextEmlVersionAfterMajorVersionChange();
            }
        }
        // all other cases warrant a minor version increment
        return getEml().getNextEmlVersionAfterMinorVersionChange();
    }

    /**
     * @return true if the resource has already been assigned a DOI, false otherwise. Remember only DOIs that are public
     * have officially been assigned/registered.
     */
    public boolean isAlreadyAssignedDoi() {
        VersionHistory last = getLastPublishedVersion();
        if (last != null) {
            DOI doi = last.getDoi();
            IdentifierStatus status = last.getStatus();
            if (doi != null && status == IdentifierStatus.PUBLIC) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return the DOI assigned/registered to the last published public version.
     */
    @Nullable
    public DOI getAssignedDoi() {
        VersionHistory last = getLastPublishedVersion();
        if (isAlreadyAssignedDoi() && last != null) {
            return last.getDoi();
        }
        return null;
    }

    /**
     * @return version number of last published version, or null if last published version doesn't exist
     */
    @Nullable
    public BigDecimal getLastPublishedVersionsVersion() {
        VersionHistory last = getLastPublishedVersion();
        if (last != null) {
            return new BigDecimal(last.getVersion());
        }
        return null;
    }

    /**
     * @return change summary of last published resource, or null if last published version had none
     */
    @Nullable
    public String getLastPublishedVersionsChangeSummary() {
        VersionHistory last = getLastPublishedVersion();
        if (last != null) {
            return Strings.emptyToNull(last.getChangeSummary());
        }
        return null;
    }

    /**
     * @return publication status of last published version, defaulting to status=private if it is not definitively known
     */
    @NotNull
    public PublicationStatus getLastPublishedVersionsPublicationStatus() {
        VersionHistory last = getLastPublishedVersion();
        if (last != null) {
            return last.getPublicationStatus();
        } else if (status.equals(PublicationStatus.REGISTERED)) {
            return PublicationStatus.REGISTERED;
        } else {
            return PublicationStatus.PRIVATE;
        }
    }

    /**
     * @return VersionHistory of last published version, or null if last published version doesn't exist
     */
    @Nullable
    public VersionHistory getLastPublishedVersion() {
        // first version history with released date represents last published version
        for (VersionHistory vh : getVersionHistory()) {
            if (vh.getReleased() != null) {
                return vh;
            }
        }
        return null;
    }

    /**
     * @return version number of last version (VersionHistory), or null if last version doesn't exist
     */
    @Nullable
    public BigDecimal getLastVersionHistoryVersion() {
        if (!getVersionHistory().isEmpty()) {
            return new BigDecimal(getVersionHistory().get(0).getVersion());
        }
        return null;
    }

    /**
     * @return GBIF UUID, key assigned when resource is registered with GBIF
     */
    public UUID getKey() {
        return key;
    }

    /**
     * Return the date the resource was last published successfully.
     *
     * @return the date the resource was last published successfully
     */
    public Date getLastPublished() {
        return lastPublished;
    }

    /**
     * Return the date the resource is scheduled to be published next.
     *
     * @return the date the resource is scheduled to be published next.
     */
    public Date getNextPublished() {
        return nextPublished;
    }

    public Set<User> getManagers() {
        return managers;
    }

    /**
     * @return a list of extensions that have been mapped to, starting with the extension that was mapped first (core
     * mapping), and ending with the extension that was mapped last. Elements in the list are unique.
     */
    public List<Extension> getMappedExtensions() {
        LinkedHashSet<Extension> extensions = Sets.newLinkedHashSet();
        for (ExtensionMapping em : mappings) {
            if (em.getExtension() != null && em.getSource() != null) {
                extensions.add(em.getExtension());
            } else {
                log.error("ExtensionMapping referencing NULL Extension or Source for resource: " + getShortname());
            }
        }
        return Lists.newArrayList(extensions);
    }

    public ExtensionMapping getMapping(String rowType, Integer index) {
        if (rowType != null && index != null) {
            List<ExtensionMapping> maps = getMappings(rowType);
            if (maps.size() >= index) {
                return maps.get(index);
            }
        }
        return null;
    }

    public List<ExtensionMapping> getMappings() {
        return mappings;
    }

    /**
     * Get the list of mappings for the requested extension rowtype.
     * The order of mappings in the list is guaranteed to be stable and the same as the underlying original mappings
     * list.
     *
     * @param rowType identifying the extension
     *
     * @return the list of mappings for the requested extension rowtype
     */
    public List<ExtensionMapping> getMappings(String rowType) {
        List<ExtensionMapping> maps = new ArrayList<ExtensionMapping>();
        if (rowType != null) {
            for (ExtensionMapping m : mappings) {
                if (rowType.equals(m.getExtension().getRowType())) {
                    maps.add(m);
                }
            }
        }
        return maps;
    }

    public Date getModified() {
        return modified;
    }

    public User getModifier() {
        return modifier;
    }

    public Organisation getOrganisation() {
        return organisation;
    }

    public int getRecordsPublished() {
        return recordsPublished;
    }

    public String getShortname() {
        return shortname;
    }

    public Source getSource(String name) {
        if (name == null) {
            return null;
        }
        name = SourceBase.normaliseName(name);
        for (Source s : sources) {
            if (s.getName().equals(name)) {
                return s;
            }
        }
        return null;
    }

    public List<Source> getSources() {
        return Ordering.natural().nullsLast().onResultOf(new Function<Source, String>() {
            @Nullable
            public String apply(@Nullable Source src) {
                return (src == null) ? null : src.getName();
            }
        }).sortedCopy(sources);
    }

    @NotNull
    public PublicationStatus getStatus() {
        return status;
    }

    /**
     * @return the status of the DOI
     */
    @NotNull
    public IdentifierStatus getIdentifierStatus() {
        return identifierStatus;
    }

    /**
     * @param identifierStatus the status of the DOI (should be paired with resource.doi)
     */
    public void setIdentifierStatus(@NotNull IdentifierStatus identifierStatus) {
        this.identifierStatus = identifierStatus;
    }

    /**
     * @return the DOI of the resource, always in prefix/suffix format excluding "doi:", e.g. 10.1234/qu83ng
     */
    @Nullable
    public DOI getDoi() {
        return doi;
    }

    /**
     * @param doi the DOI of the resource (should be paired with resource.identifierStatus)
     */
    public void setDoi(@Nullable DOI doi) {
        this.doi = doi;
    }

    /**
     * @return the key of the organisation that assigned the DOI to the resource
     */
    @Nullable
    public UUID getDoiOrganisationKey() {
        return doiOrganisationKey;
    }

    /**
     * @param doiOrganisationKey the key of the organisation that assigned the DOI to the resource
     */
    public void setDoiOrganisationKey(@Nullable UUID doiOrganisationKey) {
        this.doiOrganisationKey = doiOrganisationKey;
    }

    /**
     * Return the PublicationMode of the resource. Default is PublicationMode.AUTO_PUBLISH_OFF meaning that the
     * resource must be republished manually, and that the resource has not been configured yet for auto-publishing.
     *
     * @return the PublicationMode of the resource, or PublicationMode.AUTO_PUBLISH_OFF if not set yet
     */
    public PublicationMode getPublicationMode() {
        return (publicationMode == null) ? PublicationMode.AUTO_PUBLISH_OFF : publicationMode;
    }

    public String getSubtype() {
        return subtype;
    }

    /**
     * Return the frequency with which changes and additions are made to the dataset after the initial dataset is
     * completed.
     *
     * @return the maintenance update frequency
     */
    @Nullable
    public MaintenanceUpdateFrequency getUpdateFrequency() {
        return updateFrequency;
    }

    public String getTitle() {
        if (eml != null) {
            return eml.getTitle();
        }
        return null;
    }

    /**
     * Build and return string composed of resource title and shortname in brackets if the title and shortname are
     * different. This string can be called to construct log messages.
     *
     * @return constructed string
     */
    public String getTitleAndShortname() {
        StringBuilder sb = new StringBuilder();
        if (eml != null) {
            sb.append(eml.getTitle());
            if (!shortname.equalsIgnoreCase(eml.getTitle())) {
                sb.append(" (").append(shortname).append(")");
            }
        }
        return sb.toString();
    }

    /**
     * @return true if this resource is mapped to at least one core extension
     */
    public boolean hasCore() {
        return getCoreTypeTerm() != null;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(shortname);
    }

    public boolean hasMappedData() {
        for (ExtensionMapping cm : getCoreMappings()) {
            // test each core mapping if there is at least one field mapped
            if (!cm.getFields().isEmpty()) {
                return true;
            }
        }
        return false;
    }

    public boolean hasPublishedData() {
        return recordsPublished > 0;
    }

    public boolean isPublished() {
        return lastPublished != null;
    }

    /**
     * @return true if the last published version is public, false otherwise
     */
    public boolean isLastPublishedVersionPublic() {
        VersionHistory last = getLastPublishedVersion();
        if (last != null) {
            return last.getPublicationStatus().equals(PublicationStatus.PUBLIC);
        }
        return false;
    }

    /**
     * Used before publishing a new version.
     *
     * @return true if the resource has been assigned a GBIF-supported license, false otherwise
     */
    public boolean isAssignedGBIFSupportedLicense() {
        return eml.parseLicenseUrl() != null && Constants.GBIF_SUPPORTED_LICENSES.contains(eml.parseLicenseUrl());
    }

    public boolean isRegistered() {
        return key != null && status.equals(PublicationStatus.REGISTERED);
    }

    public void setCoreType(@Nullable String coreType) {
        this.coreType = Strings.isNullOrEmpty(coreType) ? null : coreType;
    }

    public void setCreated(Date created) {
        this.created = created;
        if (modified == null) {
            modified = created;
        }
    }

    public void setCreator(User creator) {
        this.creator = creator;
        if (modifier == null) {
            modifier = creator;
        }
    }

    public void setEml(Eml eml) {
        this.eml = eml;
    }

    /**
     * Set the new eml (resource) version. If the new version is greater than the existing version, the previous version
     * is stored.
     * </br>
     * Please note that comparison on the minor version number must include trailing zeros, e.g. 1.10 > 1.9
     *
     * @param v new eml (resource) version
     */
    public void setEmlVersion(BigDecimal v) {
        if (ResourceUtils.assertVersionOrder(v, emlVersion)) {
            setReplacedEmlVersion(new BigDecimal(emlVersion.toPlainString()));
        }
        emlVersion = v;
        eml.setEmlVersion(v);
    }

    public void setKey(UUID key) {
        this.key = key;
    }

    public void setLastPublished(Date lastPublished) {
        this.lastPublished = lastPublished;
    }

    public void setNextPublished(Date nextPublished) {
        this.nextPublished = nextPublished;
    }

    public void setManagers(Set<User> managers) {
        this.managers = managers;
    }

    public void setMappings(List<ExtensionMapping> extensions) {
        this.mappings = extensions;
    }

    public void setModified(Date modified) {
        this.modified = modified;
    }

    public void setModifier(User modifier) {
        this.modifier = modifier;
    }

    public void setOrganisation(Organisation organisation) {
        this.organisation = organisation;
    }

    public void setRecordsPublished(int recordsPublished) {
        this.recordsPublished = recordsPublished;
    }

    public void setShortname(String shortname) {
        this.shortname = shortname;
        if (eml != null && eml.getTitle() == null) {
            eml.setTitle(shortname);
        }
    }

    public void setStatus(PublicationStatus status) {
        this.status = status;
    }

    /**
     * Sets the resource PublicationMode. Its value must come from the Enumeration PublicationMode.
     *
     * @param publicationMode PublicationMode
     */
    public void setPublicationMode(PublicationMode publicationMode) {
        this.publicationMode = publicationMode;
    }

    /**
     * Sets the resource subtype. If it is null or an empty string, it is set to null. Otherwise, it is simply set
     * in lowercase.
     *
     * @param subtype subtype String
     */
    public void setSubtype(String subtype) {
        this.subtype = (Strings.isNullOrEmpty(subtype)) ? null : subtype.toLowerCase();
    }

    /**
     * Sets the maintenance update frequency. Its value comes in as a String, and gets matched to the Enumeration
     * MainUpFreqType. If no match occurs, the value is set to null.
     *
     * @param updateFrequency MainUpFreqType Enum
     */
    public void setUpdateFrequency(String updateFrequency) {
        this.updateFrequency = MaintenanceUpdateFrequency.findByIdentifier(updateFrequency);
    }

    public void setTitle(String title) {
        if (eml != null) {
            this.eml.setTitle(title);
        }
    }

    @Override
    public String toString() {
        return "Resource " + shortname;
    }

    /**
     * Check if the resource has been configured for auto-publishing. To qualify, the resource must have an update
     * frequency suitable for auto-publishing (annually, biannually, monthly, weekly, daily) or have a next published
     * date that isn't null, and must have auto-publishing mode turned on.
     *
     * @return true if the resource uses auto-publishing
     */
    public boolean usesAutoPublishing() {
        return publicationMode == PublicationMode.AUTO_PUBLISH_ON && updateFrequency != null;
    }

    /**
     * @return the change summary, explaining what has changed in this version compared with the last
     */
    public String getChangeSummary() {
        return changeSummary;
    }

    /**
     * @param changeSummary the change summary, explaining what has changed in this version compared with the last
     */
    public void setChangeSummary(String changeSummary) {
        this.changeSummary = changeSummary;
    }

    /**
     * @return the version history
     */
    @NotNull
    public List<VersionHistory> getVersionHistory() {
        if (versionHistory == null) {
            versionHistory = Lists.newLinkedList();
        }
        return versionHistory;
    }

    public void setVersionHistory(List<VersionHistory> versionHistory) {
        this.versionHistory = versionHistory;
    }

    /**
     * @return the version about to be replaced by the next publication (if publication is in progress),
     * or the version that has been replaced by the latest publication (if publication finished).
     */
    @NotNull
    public BigDecimal getReplacedEmlVersion() {
        return (replacedEmlVersion == null) ? Constants.INITIAL_RESOURCE_VERSION : replacedEmlVersion;
    }

    /**
     * Set the replacedEmlVersion, only if that version exists in VersionHistory.
     *
     * @param replacedEmlVersion version to be replaced, or that has been replaced
     */
    public void setReplacedEmlVersion(BigDecimal replacedEmlVersion) {
        VersionHistory vh = findVersionHistory(replacedEmlVersion);
        if (vh == null) {
            log.error("Replaced version (" + replacedEmlVersion.toPlainString()
                    + ") does not exist in version history!");
        } else {
            this.replacedEmlVersion = replacedEmlVersion;
        }
    }

    /**
     * @return true if resource is publicly available, or false otherwise (e.g. it is private or deleted)
     */
    public boolean isPubliclyAvailable() {
        return status.equals(PublicationStatus.PUBLIC) || status.equals(PublicationStatus.REGISTERED);
    }

    /**
     * @return true if the resource citation (EML) should be auto-generated during publication, false otherwise
     */
    public boolean isCitationAutoGenerated() {
        return citationAutoGenerated;
    }

    /**
     * @param citationAutoGenerated true if the citation should be auto-generated during publication, false otherwise
     */
    public void setCitationAutoGenerated(boolean citationAutoGenerated) {
        this.citationAutoGenerated = citationAutoGenerated;
    }

    /**
     * @return map containing record counts (map value) by extension (map key)
     */
    public Map<String, Integer> getRecordsByExtension() {
        return recordsByExtension;
    }

    /**
     * @param recordsByExtension map of record counts (map value) by extension (map key)
     */
    public void setRecordsByExtension(Map<String, Integer> recordsByExtension) {
        this.recordsByExtension = recordsByExtension;
    }

    /**
     * Construct the resource citation from various parts for the version specified.
     * </br>
     * This method is called from the Citation metadata page, in order to preview the resource citation for the upcoming
     * version for example.
     * </br>
     * The citation format is:
     * Creators (PublicationYear): Title. Version. Publisher. ResourceType. Identifier
     *
     * @param version  resource version to use in citation
     * @param homepage homepage URI
     *
     * @return generated resource citation string, or null if it failed to be generated
     */
    public String generateResourceCitation(@NotNull String version, @NotNull String homepage) {
        try {
            return generateResourceCitation(new BigDecimal(version), new URI(homepage));
        } catch (URISyntaxException e) {
            log.error("Failed to generate URI for homepage string: " + homepage, e);
        }
        return null;
    }

    /**
     * Construct the resource citation from various parts for the version specified.
     * </br>
     * The citation format is:
     * Creators (PublicationYear): Title. Version. Publisher. ResourceType. Identifier
     *
     * @param version  resource version to use in citation
     * @param homepage homepage URI
     *
     * @return generated resource citation string
     */
    public String generateResourceCitation(@NotNull BigDecimal version, @NotNull URI homepage) {
        StringBuilder sb = new StringBuilder();

        // make list of verified authors (having first and last name)
        List<String> verifiedAuthorList = Lists.newArrayList();
        for (Agent creator : getEml().getCreators()) {
            String authorName = getAuthorName(creator);
            if (authorName != null) {
                verifiedAuthorList.add(authorName);
            }
        }

        // add comma separated authors
        Iterator<String> iter = verifiedAuthorList.iterator();
        while (iter.hasNext()) {
            sb.append(iter.next());
            if (iter.hasNext()) {
                sb.append(", ");
            }
        }

        // add year resource was first published (captured in EML dateStamp)
        int publicationYear = getPublicationYear(getEml().getDateStamp());
        if (publicationYear > 0) {
            sb.append(" (");
            sb.append(publicationYear);
            sb.append("): ");
        }

        // add title
        sb.append((StringUtils.trimToNull(getTitle()) == null) ? getShortname() : StringUtils.trim(getTitle()));
        sb.append(". ");

        // add version
        sb.append("v");
        sb.append(version.toPlainString());
        sb.append(". ");

        // add publisher
        String publisher = (getOrganisation() == null) ? null : StringUtils.trimToNull(getOrganisation().getName());
        if (publisher != null) {
            sb.append(publisher);
            sb.append(". ");
        }

        // add ResourceTypeGeneral/ResourceType, e.g. Dataset/Occurrence, Dataset/Checklist
        sb.append("Dataset");
        if (getCoreType() != null) {
            sb.append("/");
            sb.append(StringUtils.capitalize(getCoreType().toLowerCase()));
        }
        sb.append(". ");

        // add DOI as the identifier. DataCite recommends using linkable, permanent URL
        if (getDoi() != null) {
            sb.append(getDoi().getUrl());
        }
        // otherwise add the citation identifier instead
        else if (getEml().getCitation() != null && !Strings.isNullOrEmpty(getEml().getCitation().getIdentifier())) {
            sb.append(getEml().getCitation().getIdentifier());
        }
        // otherwise use its IPT homepage as the identifier
        else {
            sb.append(homepage.toString());
        }
        return sb.toString();
    }

    /**
     * Construct author name for citation. Name must have a last name and at least one first name to be included. If
     * both the first and last name are left blank on purpose, the organisation name can be used as an alternative.
     *
     * @param creator creator
     *
     * @return author name
     */
    @VisibleForTesting
    protected String getAuthorName(Agent creator) {
        StringBuilder sb = new StringBuilder();
        String lastName = StringUtils.trimToNull(creator.getLastName());
        String firstNames = StringUtils.trimToNull(creator.getFirstName());
        String organisation = StringUtils.trimToNull(creator.getOrganisation());
        if (lastName != null && firstNames != null) {
            sb.append(lastName);
            sb.append(" ");
            // add first initial of each first name, capitalized
            String[] names = firstNames.split("\\s+");
            for (int i = 0; i < names.length; i++) {
                sb.append(StringUtils.upperCase(String.valueOf(names[i].charAt(0))));
                if (i < names.length - 1) {
                    sb.append(" ");
                }
            }
        } else if (lastName == null && firstNames == null && organisation != null) {
            sb.append(organisation);
        }
        return sb.toString();
    }

    /**
     * Get the year from the publication date.
     *
     * @param publicationDate date resource was published
     *
     * @return publication year
     */
    @VisibleForTesting
    protected int getPublicationYear(Date publicationDate) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(publicationDate);
        return calendar.get(Calendar.YEAR);
    }

    /**
     * @return the date the metadata was last modified.
     */
    public Date getMetadataModified() {
        return metadataModified;
    }

    /**
     * Set metadataModified date. Update modified date at same time.
     *
     * @param metadataModified date metadata was last modified
     */
    public void setMetadataModified(Date metadataModified) {
        this.modified = metadataModified;
        this.metadataModified = metadataModified;
    }

    /**
     * @return the date any source mapping was last modified.
     */
    public Date getMappingsModified() {
        return mappingsModified;
    }

    /**
     * Set mappingsModified date. Update modified date at same time.
     *
     * @param mappingsModified date mappings were last modified
     */
    public void setMappingsModified(Date mappingsModified) {
        this.modified = mappingsModified;
        this.mappingsModified = mappingsModified;
    }

    /**
     * @return the date any source was last modified.
     */
    public Date getSourcesModified() {
        return sourcesModified;
    }

    /**
     * Set sourcesModified date. Update modified date at same time.
     *
     * @param sourcesModified date sources were last modified
     */
    public void setSourcesModified(Date sourcesModified) {
        this.modified = sourcesModified;
        this.sourcesModified = sourcesModified;
    }

    /**
     * Updates the resource's list of alternate identifiers for the resource's DOI, adding it or removing it depending on
     * the status of the DOI:
     * </br>
     * If the status of the DOI is reserved or public, the resource DOI will be added as the first alternative identifier
     * in the list. Please note that multiple (different) DOIs are allowed in the list of alternate identifiers.
     * </br>
     * If the status of the DOI is unavailable, the resource DOI will be removed from the list.
     */
    public synchronized void updateAlternateIdentifierForDOI() {
        Preconditions.checkNotNull(eml);

        if (doi != null) {
            // retrieve a list of the resource's alternate identifiers
            List<String> ids = eml.getAlternateIdentifiers();
            if (identifierStatus.equals(IdentifierStatus.PUBLIC_PENDING_PUBLICATION)
                    || identifierStatus.equals(IdentifierStatus.PUBLIC)) {
                // make sure the DOI always appears first
                List<String> reorderedList = Lists.newArrayList();
                reorderedList.add(doi.toString());
                // make sure the DOI doesn't appear twice
                for (String id : ids) {
                    if (!id.equalsIgnoreCase(doi.toString())) {
                        reorderedList.add(id);
                    }
                }
                // replace the original list with the reordered one
                if (!ids.isEmpty()) {
                    ids.clear();
                }
                ids.addAll(reorderedList);
                log.debug("DOI=" + doi.toString() + " added to resource's list of alt ids as first element");
            } else if (identifierStatus.equals(IdentifierStatus.UNAVAILABLE)
                    || identifierStatus.equals(IdentifierStatus.UNRESERVED)) {
                for (Iterator<String> iterator = ids.iterator(); iterator.hasNext();) {
                    String id = iterator.next();
                    // make sure a DOI that has been made unavailable, or that has been deleted, no longer appears in the list
                    if (id.equalsIgnoreCase(doi.toString())) {
                        iterator.remove();
                        log.debug("DOI=" + doi.toString() + " removed from resource's list of alt ids");
                    }
                }
            }
        }
    }

    /**
     * Updates the resource's citation identifier for the resource's DOI, adding it or removing it depending on
     * the status of the DOI:
     * </br>
     * If the status of the DOI is reserved or public, the resource DOI will be set as the resource citation identifier.
     * </br>
     * If the status of the DOI is unavailable or unreserved, the resource DOI will be unset as the citation identifier.
     */
    public synchronized void updateCitationIdentifierForDOI() {
        Preconditions.checkNotNull(eml);

        if (doi != null) {
            // retrieve resource's citation identifier
            Citation citation = eml.getCitation();
            if (identifierStatus.equals(IdentifierStatus.PUBLIC_PENDING_PUBLICATION)
                    || identifierStatus.equals(IdentifierStatus.PUBLIC)) {
                // make sure the DOI set as resource citation identifier
                if (citation == null) {
                    // resource must have citation if it has a DOI
                    setCitationAutoGenerated(true);
                    eml.setCitation(
                            new Citation("Will be replaced by auto-generated citation", doi.getUrl().toString()));
                } else {
                    citation.setIdentifier(doi.getUrl().toString());
                }
                log.debug("DOI=" + doi.getUrl().toString() + " set as resource's citation identifier");
            } else if (identifierStatus.equals(IdentifierStatus.UNAVAILABLE)
                    || identifierStatus.equals(IdentifierStatus.UNRESERVED)) {
                // make sure the DOI no longer set as resource citation identifier
                if (citation == null) {
                    // resource must have had a citation if it had a DOI
                    setCitationAutoGenerated(true);
                    Citation generated = new Citation();
                    generated.setCitation("Will be replaced by auto-generated citation");
                    eml.setCitation(generated);
                } else {
                    citation.setIdentifier(null);
                }
                log.debug("DOI=" + doi.getUrl().toString() + " unset as resource's citation identifier");
            }
        }
    }

    /**
     * Determine if this resource has at least one mapping to the occurrence core extension, no matter if the mapping
     * is a core or extension mapping.
     *
     * @return true if resource has at least one mapping to the occurrence core extension, false otherwise
     */
    public boolean hasOccurrenceMapping() {
        return !getMappings(Constants.DWC_ROWTYPE_OCCURRENCE).isEmpty();
    }
}