EntryInfo.js

import moment from 'moment';
import {Graph, namespaces} from '@entryscape/rdfjson';
import factory from './factory.js';
import terms from './terms.js';

/**
 *
 * @param entry
 * @param vocab
 * @returns {*}
 */
const getResourceTypeHelper = (entry, vocab) => {
  const stmts = entry._graph.find(entry.getResourceURI(), terms.rdf.type);
  for (let i = 0; i < stmts.length; i++) {
    const t = vocab[stmts[i].getValue()];
    if (t != null) {
      return t;
    }
  }

  return vocab.default;
};

/**
 * EntryInfo is a class that contains all the administrative information of an entry.
 * @exports store/EntryInfo
 */
export default class EntryInfo {
  /**
   * @param {String} entryURI must be provided unless the graph contains a statement with
   * the store:resource property which allows us to infer the entryURI.
   * @param {rdfjson/Graph} graph corresponds to a rdfjson.Graph class with the entryinfo as
   * statements
   * @param {EntryStore} entryStore
   */
  constructor(entryURI, graph, entryStore) {
    this._entryURI = entryURI || graph.find(null, terms.resource)[0].getSubject();
    this._graph = graph || new Graph();
    this._entryStore = entryStore;
  }

  /**
   * @returns {Entry}
   */
  getEntry() {
    return this._entry;
  }

  /**
   * @param {rdfjson/Graph} graph
   */
  setGraph(graph) {
    this._graph = graph;
  }

  /**
   * @return {rdfjson/Graph}
   */
  getGraph() {
    return this._graph;
  }

  /**
   * Pushes the entry information to the repository, e.g. posts to
   * basepath/{contextId}/entry/{entryId}
   * @params {boolean} ignoreIfUnmodifiedSinceCheck if explicitly set to true no check is done
   * if information is stale, also it will not automatically refresh with the latest date
   * @returns {Promise.<EntryInfo>}
   */
  async commit(ignoreIfUnmodifiedSinceCheck = false) {
    const es = this._entry.getEntryStore();
    const promise = es.getREST().put(this.getEntryURI(),
      JSON.stringify(this._graph.exportRDFJSON()),
      ignoreIfUnmodifiedSinceCheck ? undefined : this.getModificationDate());
    es.handleAsync(promise, 'commitEntryInfo');
    const response = await promise;
    this.setModificationDate(response.header['last-modified']);
    return this;
  }

  /**
   * @returns {String}
   */
  getEntryURI() {
    return this._entryURI;
  }

  /**
   * @returns {String} the id of the entry
   */
  getId() {
    return factory.getEntryId(this._entryURI);
  }

  /**
   * If the entry is a user, group or context there can be a name.
   * In general the name is accessed on the resource, but in certain
   * situations we do not have the resource yet(not loaded) but we still
   * have the name (from a search where the name is provided but not the resource),
   * in this case we can access this name here.
   *
   * @returns {String} a username, groupname or contextname of the entry
   */
  getName() {
    return this._name;
  }

  /**
   * If the entry is a user there can be a disabled state.
   * In general the disabled state is accessed on the resource, but in certain
   * situations we do not have the resource yet(not loaded) but we still
   * have the disabled state (from a search where the disabled state is provided
   * but not the resource), in this case we can access the disabled state here.
   *
   * @returns {boolean} a disabled state of a user
   */
  isDisabled() {
    return this._disabled;
  }

  /**
   * @returns {String}
   */
  getMetadataURI() {
    return factory.getMetadataURIFromURI(this._entryStore, this._entryURI);
  }

  /**
   * @returns {String}
   */
  getExternalMetadataURI() {
    // TODO will only exist for LinkReferences and References.
    return this._graph.findFirstValue(this._entryURI, terms.externalMetadata);
  }

  /**
   * @param {String} uri
   */
  setExternalMetadataURI(uri) {
    this._graph.findAndRemove(this._entryURI, terms.externalMetadata);
    this._graph.create(this._entryURI, terms.externalMetadata, { type: 'uri', value: uri });
  }

  /**
   * @returns {String}
   */
  getCachedExternalMetadataURI() {
    return factory.getCachedExternalMetadataURI(this._entryURI);
  }

  /**
   * @returns {String}
   */
  getResourceURI() {
    return this._graph.findFirstValue(this._entryURI, terms.resource);
  }

  /**
   * @param {String} uri
   */
  setResourceURI(uri) {
    const oldResourceURI = this.getResourceURI();
    this._graph.findAndRemove(this._entryURI, terms.resource);
    this._graph.create(this._entryURI, terms.resource, { type: 'uri', value: uri });
    if (oldResourceURI) {
      const stmts = this._graph.find(oldResourceURI);
      for (let i = 0; i < stmts.length; i++) {
        stmts[i].setSubject(uri);
      }
    }
  }

  /**
   * @returns {String} one of the entryTypes
   * @see terms#entryType
   */
  getEntryType() {
    const et = this._graph.findFirstValue(this._entryURI, terms.rdf.type);
    return terms.entryType[et || 'default'];
  }


  /**
   * the resource type of the entry, e.g. "Information", "Resolvable" etc.
   * The allowed values are available in types beginning with 'RT_'.
   * E.g. to check if the entry is an information resource:
   * if (ei.getResourceType() === types.RT_INFORMATIONRESOURCE) {...}
   *
   * @returns {String}
   */
  getResourceType() {
    return getResourceTypeHelper(this, terms.resourceType);
  }

  /**
   * the graph type of the entry, e.g. "User", "List", "String", etc.
   * The allowed values are available in types beginning with 'GT_'.
   * E.g. to check if the entry is a list:
   * if (ei.getGraphType() === types.GT_LIST) {...}
   *
   * @returns {String}
   */
  getGraphType() {
    return getResourceTypeHelper(this, terms.graphType);
  }

  // TODO: change to entryURI instead of resourceURI for principalURIs.
  /**
   * The acl object returned looks like:
   * {
   *   admin:  [principalURI1, principalURI2, ...],
   *   rread:  [principalURI3, ...],
   *   rwrite: [principalURI4, ...],
   *   mread:  [principalURI5, ...],
   *   mwrite: [principalURI6, ...]
   * }
   *
   * There will always be an array for each key, it might be empty though.
   * The principalURI* will always be an URI to the resource of a user or group entry.
   *
   * Please note that a non empty acl overrides any defaults from the surrounding context.
   *
   * @param {boolean} asIds - if true the principalURIs are shortened to entry identifiers.
   * @return {Object} an acl object.
   */
  getACL(asIds = false) {
    const f = (stmt) => {
      if (asIds) {
        return factory.getEntryId(stmt.getValue());
      }
      return stmt.getValue();
    };  // Statement > object value.
    const ru = this.getResourceURI();
    const mu = this.getMetadataURI();
    const acl = {
      admin: this._graph.find(this._entryURI, terms.acl.write).map(f),
      rread: this._graph.find(ru, terms.acl.read).map(f),
      rwrite: this._graph.find(ru, terms.acl.write).map(f),
      mread: this._graph.find(mu, terms.acl.read).map(f),
      mwrite: this._graph.find(mu, terms.acl.write).map(f),
    };
    acl.contextOverride = acl.admin.length !== 0 || acl.rread.length !== 0
      || acl.rwrite.length !== 0 || acl.mread.length !== 0 || acl.mwrite.length !== 0;
    return acl;
  }

  /**
   * if the entry has an explicit ACL or if the containing contexts ACL is used.
   *
   * @returns {boolean}
   */
  hasACL() {
    return this.getACL().contextOverride;
  }

  /**
   * Replaces the current acl with the provided acl.
   * The acl object is the same as you get from the getACL call.
   * The first difference is that the acl object from this method is allowed to be empty
   * or leave out some keys that are not to be set.
   * The second difference is that it allows entryIds as values in the arrays,
   * not only full resource URIs, both have to refer to principals though.
   *
   * @param {Object} acl same kind of object you get from getACL.
   */
  setACL(acl) {
    const g = this._graph;
    const f = (subj, pred, principals, base) => {
      g.findAndRemove(subj, pred);
      (principals || []).forEach((principal) => {
        if (principal.length < base.length || principal.indexOf(base) !== 0) {
          // principal is entry id.
          g.add(subj, pred, { type: 'uri', value: base + principal });
        } else {
          // principal is a full entry resource uri.
          g.add(subj, pred, { type: 'uri', value: principal });
        }
      });
    };
    const _acl = acl || {};
    const ru = this.getResourceURI();
    const mu = this.getMetadataURI();
    const base = factory.getResourceBase(this._entry.getEntryStore(), '_principals');
    f(this._entryURI, terms.acl.write, _acl.admin, base);
    f(ru, terms.acl.read, _acl.rread, base);
    f(ru, terms.acl.write, _acl.rwrite, base);
    f(mu, terms.acl.read, _acl.mread, base);
    f(mu, terms.acl.write, _acl.mwrite, base);
  }

  /**
   * Checks if there are any metadata revisions for this entry,
   * in practise this is always true if provenance is enabled for this entry.
   *
   * @return {boolean} true if there is at least one metadata revision.
   */
  hasMetadataRevisions() {
    // const mdURI = this.getMetadataURI();
    return this._graph.findFirstValue(null, 'owl:sameAs') != null;
  }

  /**
   * Extracts an array of metadata revisions from the graph.
   * Each revision is an object that contains:
   *   * time - when the change was made (Date)
   *   * by   - the user who performed the change (entryURI)
   *   * rev  - the revision number (string)
   *   * uri  - an URI to this revision (string)
   *
   * The uri of the revision can be used by the method getMetadataRevisionGraph
   * to get a hold of the actual new graph that caused the revision.
   *
   * @return {Array.<{time: Date, by: string, rev: string, uri: string}>} a sorted array of revisions, latest revision first.
   */
  getMetadataRevisions() {
    const revs = [];
    const mdURI = this.getMetadataURI();
    const stmts = this._graph.find(null, 'owl:sameAs', mdURI);

    if (stmts.length !== 1) {
      return revs;
    }
    let uri = stmts[0].getSubject();
    const es = this._entryStore;
    while (uri) {
      revs.push({
        uri,
        rev: uri.substr(mdURI.length + 5),
        time: moment(this._graph.findFirstValue(uri, 'prov:generatedAtTime')).toDate(),
        by: es.getEntryURIFromURI(this._graph.findFirstValue(uri, 'prov:wasAttributedTo')),
      });
      uri = this._graph.findFirstValue(uri, 'prov:wasRevisionOf');
    }
    revs.sort((r1, r2) => {
      if (r1.time > r2.time) {
        return -1;
      } else if (r1.time < r2.time) {
        return 1;
      }
      return 0;
    });
    return revs;
  }

  /**
   * Retrieves the metadata graph of a certain revision from its graph.
   * @param revisionURI
   * @return {Promise.<rdfjson/Graph>}
   */
  async getMetadataRevisionGraph(revisionURI) {
    const data = await this._entryStore.getREST().get(revisionURI);
    return new Graph(data);
  }

  /**
   * @returns {string} the label of the resource of this entry,
   * typically set when uploading a file.
   */
  getLabel() {
    return this._graph.findFirstValue(this.getResourceURI(), 'http://www.w3.org/2000/01/rdf-schema#label');
  }

  /**
   * Sets a new label of the resource in the graph, call
   * {@link EntryInfo#commit commit} to push
   * the updated graph to the repository.
   *
   * @param {string} label - a new label for the resource.
   */
  setLabel(label) {
    this._graph.findAndRemove(this.getResourceURI(), 'http://www.w3.org/2000/01/rdf-schema#label');
    if (label != null && label !== '') {
      this._graph.add(this.getResourceURI(), 'http://www.w3.org/2000/01/rdf-schema#label', {
        type: 'literal',
        value: label,
      });
    }
  }

  /**
   * @returns {string} the format of the resource of this entry.
   */
  getFormat() {
    return this._graph.findFirstValue(this.getResourceURI(), 'http://purl.org/dc/terms/format');
  }

  /**
   * Sets a new format of the resource in the graph, call {@link EntryInfo#commit commit}
   * to push the updated graph to the repository.
   *
   * @param {string} format - a format in the form application/json or text/plain.
   */
  setFormat(format) {
    this._graph.findAndRemove(this.getResourceURI(), 'http://purl.org/dc/terms/format');
    if (format != null && format !== '') {
      this._graph.addL(this.getResourceURI(), 'http://purl.org/dc/terms/format', format);
    }
  }

  /**
   * @returns {string} the status of this entry, always a URI.
   */
  getStatus() {
    return this._graph.findFirstValue(this.getEntryURI(), terms.status.property);
  }

  /**
   * Sets a new status for this entry
   *
   * @param {string} status
   */
  setStatus(status) {
    this._graph.findAndRemove(this.getEntryURI(), terms.status.property);
    if (status != null && status !== '' && status.indexOf('http') === 0) {
      this._graph.add(this.getEntryURI(), terms.status.property, status);
    }
  }

  /**
   * @returns {Date} the date when the entry was created.
   */
  getCreationDate() {
    const d = this._graph.findFirstValue(this.getEntryURI(), 'http://purl.org/dc/terms/created');
    return moment(d).toDate(); // Must always exist.
  }

  /**
   * @returns {Date} the date of last modification (according to the repository,
   * local changes are not reflected).
   */
  getModificationDate() {
    const d = this._graph.findFirstValue(this.getEntryURI(), 'http://purl.org/dc/terms/modified');
    if (d != null) {
      return moment(d).toDate();
    }
    return this.getCreationDate();
  }

  /**
   * A method to be used internally / within the library after modifying operations return successfull with a new modified date.
   * @param {string} date
   * @protected
   */
  setModificationDate(date) {
    const d = new Date(date);
    // Add a second since the date provided (from http header 'last-modified') does not contain milliseconds.
    d.setSeconds(d.getSeconds()+1);
    const stmts = this._graph.find(this.getEntryURI(), 'http://purl.org/dc/terms/modified');
    const newModificationDate = moment(d).toISOString();
    if (stmts.length > 0) {
      stmts[0].setValue(newModificationDate, true);
    } else {
      this._graph.create(this.getEntryURI(), 'http://purl.org/dc/terms/modified', {
        type: 'literal',
        value: newModificationDate,
        datatype: namespaces.expand('xsd:dateTime')
      }, true, true);
    }
  }

  /**
   * @returns {String} a URI to creator, the user Entry resource URI is used, e.g. "http://somerepo/_principals/resource/4", never null.
   */
  getCreator() {
    return this._graph.findFirstValue(this.getEntryURI(), 'http://purl.org/dc/terms/creator');
  }

  /**
   * @returns {number|undefined}
   */
  getSize() {
    const extent = this._graph.findFirstValue(this.getResourceURI(), 'http://purl.org/dc/terms/extent');
    if (parseInt(extent, 10) === parseInt(extent, 10)) {
      return parseInt(extent, 10);
    }
    return undefined;
  }

  /**
   * @returns {Array} an array of URIs to the contributors using their Entry resource URIs,
   * e.g. ["http://somerepo/_principals/resource/4"], never null although the array might be empty.
   */
  getContributors() {
    return this._graph.find(this.getEntryURI(), 'http://purl.org/dc/terms/contributor').map(stmt => stmt.getValue());
  }
};