Source

LinkedArtHelpers.js

/**
 * @file LinkedArtHelpers
 * @author Adam Brin, Pamela Lam, Alyx Rossetti, Charles Webb, Selina Zawacki, Nabil Kashyap
 * @module LinkedArtHelpers
 * @category Files
 * @description This file contains helpers for working with LinkedArt JSON-LD data
 */

import { doesObjectLanguageMatch } from "./LanguageHelpers";
import {
  normalizeAatId,
  normalizeFieldToArray,
  normalizeFieldWithParts
} from "./BasicHelpers";
import aat from "../data/aat.json";
import {
  ATTRIBUTED_BY,
  ASSIGNED_BY,
  CLASSIFIED_AS,
  CLASSIFIED_BY,
  IDENTIFIED_BY,
  NAME,
  PART,
  REFERRED_TO_BY,
  UNKNOWN,
  ASSIGNED_PROPERTY,
  ASSIGNED,
  AND,
  OR
} from "../data/constants.json";

/**
 * Given an object or an array of objects, find all entries that have an object in their classified_as
 * field with an id that matches the requestedClassification.
 *
 * @param {Object|Array} submittedResource - the object to inspect
 * @param {String|Array} requestedClassifications - the classification ID/IDS to match
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications

 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_by: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_by: [{id: 'title', classified_by: 'descriptive title'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description', classified_as: 'descriptive title'}]}
 * ]
 * getClassifiedAs(submittedResource, 'description') would return the third object in the array
 *
 * @returns {Array} an array of objects that match
 */
export function getClassifiedAs(
  submittedResource,
  requestedClassifications,
  { language = undefined, languageOptions = {}, operator } = {}
) {
  return getClassified(submittedResource, requestedClassifications, {
    classificationField: CLASSIFIED_AS,
    language,
    languageOptions,
    operator
  });
}

/**
 * Given an object or an array of objects, find all entries that have an object in their classified_by
 * field with an id that matches the requestedClassification.
 *
 * @param {Object|Array} submittedResource - the object to inspect
 * @param {String|Array} requestedClassifications - the classification ID/IDS to match
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_by: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_by: [{id: 'title', classified_by: 'descriptive title'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description', classified_as: 'descriptive title'}]}
 * ]
 * getClassifiedBy(submittedResource, 'title') would return the first and second objects in the array
 *
 * @returns {Array} an array of objects that match
 */
export function getClassifiedBy(
  submittedResource,
  requestedClassifications,
  { language = undefined, languageOptions = {}, operator } = {}
) {
  return getClassified(submittedResource, requestedClassifications, {
    classificationField: CLASSIFIED_BY,
    language,
    languageOptions,
    operator
  });
}

/**
 * Given an object or an array of objects, find all classification objects that are classified as
 * with the nestedClassification. (e.g. for Visual Items we need to get the rights statement
 * classification object, which we identify by its own classification see VisualItems.getClearanceLevel)
 *
 * @param {Object|Array} submittedResource - the object to inspect
 * @param {String|Array} nestedClassification - the classification ID/IDS to match
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_by: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_by: [{id: 'title', classified_by: 'descriptive title'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description', classified_as: 'descriptive title'}]}
 * ]
 * getClassifiedByWithClassification(submittedResource, 'descriptive title') would return the classification with
 * 'id': 'description' from the 'classified_as' attribute of the third object in the array
 *
 * @returns {Array} an array of objects that match
 */
export function getClassifiedAsWithClassification(
  submittedResource,
  nestedClassification,
  { language = undefined, languageOptions = {}, operator } = {}
) {
  return _getClassificationsWithNestedClass(
    submittedResource,
    nestedClassification,
    CLASSIFIED_AS,
    { language, languageOptions, operator }
  );
}

/**
 * Given an object or an array of objects, find all classification objects that are classified by
 * with the nestedClassification. (e.g. for Visual Items we need to get the rights statement
 * classification object, which we identify by its own classification see VisualItems.getClearanceLevel)
 *
 * @param {Object|Array} submittedResource - the object to inspect
 * @param {String|Array} nestedClassification - the classification ID/IDS to match
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_by: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_by: [{id: 'title', classified_by: 'descriptive title'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description', classified_as: 'descriptive title'}]}
 * ]
 * getClassifiedByWithClassification(submittedResource, 'descriptive title') would return the classification with
 * 'id': 'title' from the 'classified_by' attribute of the second object in the array
 *
 * @returns {Array} an array of objects that match
 */
export function getClassifiedByWithClassification(
  submittedResource,
  nestedClassification,
  { language = undefined, languageOptions = {}, operator } = {}
) {
  return _getClassificationsWithNestedClass(
    submittedResource,
    nestedClassification,
    CLASSIFIED_BY,
    { language, languageOptions, operator }
  );
}

/**
 * Given an object or an array of objects, find all objects that are classified as an object
 * which is classified by nestedClassification
 *
 * @param {Object|Array} submittedResource - the object to inspect
 * @param {String|Array} nestedClassification - the classification ID/IDS to match
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_by: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_by: [{id: 'title', classified_by: 'descriptive title'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description', classified_as: 'descriptive title'}]}
 * ]
 * getObjectsClassifiedByWithClassification(submittedResource, 'descriptive title') would return the
 * third object in the array
 *
 * @returns {Array} an array of objects that match
 */
export function getObjectsClassifiedAsWithClassification(
  submittedResource,
  nestedClassification,
  { language = undefined, languageOptions = {}, operator } = {}
) {
  return _getObjectWithNestedClass(
    submittedResource,
    nestedClassification,
    CLASSIFIED_AS,
    { language, languageOptions, operator }
  );
}

/**
 * Given an object or an array of objects, find all objects that are classified by an object
 * which is classified by nestedClassification
 *
 * @param {Object|Array} submittedResource - the object to inspect
 * @param {String|Array} nestedClassification - the classification ID/IDS to match
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_by: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_by: [{id: 'title', classified_by: 'descriptive title'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description', classified_as: 'descriptive title'}]}
 * ]
 * getObjectsClassifiedByWithClassification(submittedResource, 'descriptive title') would return the
 * second object in the array
 *
 * @returns {Array} an array of objects that match
 */
export function getObjectsClassifiedByWithClassification(
  submittedResource,
  nestedClassification,
  { language = undefined, languageOptions = {} } = {}
) {
  return _getObjectWithNestedClass(
    submittedResource,
    nestedClassification,
    CLASSIFIED_BY,
    { language, languageOptions }
  );
}
/**
 * Gets the primary name of the JSON-LD object based on an AAT value or other qualifier, uses the AAT value of Preferred Term as the default
 *
 * @category Accessors
 * @param {Object} submittedResource - the JSON-LD object
 * @param {Object} options - additional options
 * @param {String|Array} options.requestedClassifications - the requested classifications (default is aat.PRIMARY_TERM)
 * @param {String} options.language - the requested language (default undefined)
 * @param {Object} options.languageOptions - additional language options
 * @param {Object} options.languageOptions.lookupMap - a map of terms -> values for translating language keys (eg. "en": "aat:11111")
 * @param {String} options.languageOptions.fallbackLanguage - if a language is specified, this provides a fallback language if that language is not available in the data, e.g. use english if there is no french
 *
 * @example gets the primary name using defaults getPrimaryName(object)
 * @example gets the primary name in french getPrimaryName(object, {language:'fr'})
 * @example gets the primary name using a different AAT term getPrimaryName(object, {requestedClassifications: 'http://vocab.getty.edu/aat/300417193'})
 *
 * @returns {String} of items identified as primary names
 */
export function getPrimaryName(
  submittedResource,
  {
    requestedClassifications = aat.PREFERRED_TERM,
    language,
    languageOptions = {}
  } = {}
) {
  return getPrimaryNames(submittedResource, {
    requestedClassifications,
    language,
    languageOptions
  })[0];
}

/**
 * Gets the primary names of the JSON-LD object based on an AAT value or other qualifier, uses the AAT value of Preferred Term as the default
 *
 * @category Accessors
 * @param {Object} submittedResource - the JSON-LD object
 * @param {Object} options - additional options
 * @param {String|Array} options.requestedClassifications - the requested classifications (default is aat.PRIMARY_TERM)
 * @param {String} options.language - the requested language (default undefined)
 * @param {Object} options.languageOptions - additional language options
 * @param {Object} options.languageOptions.lookupMap - a map of terms -> values for translating language keys (eg. "en": "aat:11111")
 * @param {String} options.languageOptions.fallbackLanguage - if a language is specified, this provides a fallback language if that language is not available in the data, e.g. use english if there is no french
 *
 * @example gets the primary name using defaults getPrimaryName(object)
 * @example gets the primary name in french getPrimaryName(object, {language:'fr'})
 * @example gets the primary name using a different AAT term getPrimaryName(object, {requestedClassifications: 'http://vocab.getty.edu/aat/300417193'})
 *
 * @returns {Array} of items identified as primary names
 */
export function getPrimaryNames(
  submittedResource,
  {
    requestedClassifications = aat.PREFERRED_TERM,
    language,
    languageOptions = {}
  } = {}
) {
  if (submittedResource == undefined) {
    return UNKNOWN;
  }
  let identified_by = normalizeFieldToArray(submittedResource, IDENTIFIED_BY);
  let names = identified_by.filter((item) => item.type == NAME);
  let name = getValuesByClassification(names, requestedClassifications, {
    language,
    languageOptions
  });

  if (name.length > 0) {
    return name;
  }

  // if we don't have a name with a classification, and we're using the 'default' values, look for a Name
  if (names.length > 0 && requestedClassifications == aat.PREFERRED_TERM) {
    let values = [...names];
    for (let i in values) {
      values[i] = getValueOrContent(values[i]);
    }
    return values;
  }

  // fallback for error case
  let label = UNKNOWN;
  if (submittedResource && submittedResource.id) {
    label += " (" + submittedResource.id + ")";
  }
  return [label];
}

/**
 * Given an object or an array of objects, find all objects that with classifications
 * that match either all of the requestedClassifications or any of the requestedClassifications.
 *
 * @param {Object|Array} submittedResource - the object to inspect
 * @param {String|Array} requestedClassifications - either a string or an array of classification strings
 * @param {String} classificationField - the field to investigate for an object's classification (e.g. classified_as, classified_by)
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_as: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_as: [{id: 'title'}, {id: 'description'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description'}]}
 * ]
 * getClassified(submittedResource, ['title', 'description']) would return the second object in the array
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_as: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_as: [{id: 'title'}, {id: 'description'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description'}]}
 * ]
 * getClassified(submittedResource, ['title', 'description'], 'classified_as', undefined, {}, 'OR')
 * would return all objects in the array
 *
 * @returns {Array} an array of objects that match
 */
export function getClassified(
  submittedResource,
  requestedClassifications,
  {
    classificationField = CLASSIFIED_AS,
    language = undefined,
    languageOptions = {},
    operator = AND
  } = {}
) {
  let results = [];
  let resourceArray = _convertToArrayIfNeeded(submittedResource);
  let requestedClassArray = _convertToArrayIfNeeded(requestedClassifications);
  if (resourceArray.length == 0 || requestedClassArray.length == 0) {
    return results;
  }
  for (const resource of resourceArray) {
    // If there is no classifications, or the classification is just a string,
    // continue
    var classified_as = normalizeFieldToArray(resource, classificationField);
    let languageMatch = doesObjectLanguageMatch(
      resource,
      language,
      languageOptions
    );
    if (!classified_as.length) {
      continue;
    }
    let classificationIDs = classified_as.map((obj) =>
      typeof obj == "string" ? obj : obj.id
    );

    // function to check whether the ID matches
    let inClassificationIDs = (id) =>
      (languageMatch && classificationIDs.includes(id)) ||
      classificationIDs.includes(normalizeAatId(id));

    if (operator.toUpperCase() == AND) {
      if (requestedClassArray.every(inClassificationIDs)) {
        results.push(resource);
      }
    } else if (operator.toUpperCase() == OR) {
      if (requestedClassArray.some(inClassificationIDs)) {
        results.push(resource);
      }
    } else {
      throw `unknown operator specified: ${operator}, please use and/or`;
    }
  }
  return results;
}

/**
 * Get the value of the first object that is classified as the requestedClassification
 * parameter.
 *
 * @param {Object|Array} submittedResource - the JSON-LD Object
 * @param {String|Array} requestedClassifications - the requested classifications
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_as: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_as: [{id: 'title'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description'}]}
 * ]
 * getValueByClassification(submittedResource, 'title') would return 'Irises'
 *
 * @returns {String|number} the matching value
 */
export function getValueByClassification(
  submittedResource,
  requestedClassifications,
  { language, languageOptions, operator } = {}
) {
  let results = getClassifiedAs(submittedResource, requestedClassifications, {
    language,
    languageOptions,
    operator
  });
  if (results.length) {
    let result = getValueOrContent(results[0]);
    return result;
  }
  return undefined;
}

/**
 * Get an array of all the values of objects that are classified as the
 * requestedClassification parameter.
 *
 * @param {Object|Array} submittedResource - the JSON-LD Object
 * @param {String|Array} requestedClassifications - the requested classifications
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 * @example for the submittedResource:
 * [
 *   {content: 'Irises', classified_as: [{id: 'title'}]},
 *   {content: 'Blue Irises', classified_as: [{id: 'title'}]},
 *   {content: 'Van Gogh painting', classified_as: [{id: 'description'}]}
 * ]
 * getValuesByClassification(submittedResource, 'title') would return ['Irises', 'Blue Irises']
 *
 * @returns {Array} the matching values (string|Array)
 */
export function getValuesByClassification(
  submittedResource,
  requestedClassifications,
  { language, languageOptions, operator } = {}
) {
  let results = getClassifiedAs(submittedResource, requestedClassifications, {
    language,
    languageOptions,
    operator
  });
  if (results.length) {
    let values = [];
    results.forEach((result) => {
      const value = getValueOrContent(result);
      if (value) {
        values.push(value);
      }
    });
    return values;
  }
  return [];
}

/**
 * Given an object get the value of the 'value' or 'content' attribute (if
 * an object has both returns the 'value' attribute)
 *
 * @param {Object} object - the JSON-LD object
 *
 * @example getValueOrContent({'value': 123, 'content': '456'}) would return 123
 *
 * //fixme: if this should only return a string or number should we add a type check?
 * @returns {String|number} the value or content depending on which exists.
 */
export function getValueOrContent(object) {
  if (object == undefined) {
    return undefined;
  }

  if (object.value !== undefined) {
    return object.value;
  }
  if (object.content) {
    return object.content;
  }
  return undefined;
}

/**
 * Gets all the values of objects in a LinkedArt object's 'referred_to_by' field
 * which are classified by the classification parameter.
 *
 * @param {Object} object - the Actor object to inspect
 * @param {String|Array} classification - the classification
 *
 * @example for the object:
 * {
 *  'referred_to_by': [
 *     {content: 'Irises', classified_as: [{id: 'title'}]},
 *     {content: 'Van Gogh painting', classified_as: [{id: 'description'}]}
 *   ]
 * }
 * getReferredToByClassification(object, 'title') would return ['Irises']
 *
 * @returns {String}  or undefined
 */
export function getReferredToByClassification(object, classification) {
  if (!object) return undefined;
  const referredToBy = object[REFERRED_TO_BY];
  if (!referredToBy) return undefined;

  return getValueByClassification(referredToBy, classification);
}

/**
 * Gets all of the objects from a LinkedArt Object's 'attributed_by' attribute which have
 * an 'assigned_property' attribute that matches the assignedProperty parameter.
 *
 * @param {Object} object - the LinkedArt Object
 * @param {String} assignedProperty  - the assigned property
 * 
 * @example getAttributedBy(object, 'identified_by') would return the object with 'type': 'Name'
 * from the example object below
 * {
 *  "attributed_by": [
     {
       "id": "https://data.getty.edu/museum/collection/object/5be2eb9f-1b4e-49f6-bfc4-0fc7ab67a1c5/name/f04fde5c-e645-4b4d-986c-443d7c6aa2ef/attribute-assignment",
       "type": "AttributeAssignment",
       "assigned_property": "identified_by",
       "assigned": {
         "id": "https://data.getty.edu/museum/collection/object/5be2eb9f-1b4e-49f6-bfc4-0fc7ab67a1c5/name/f04fde5c-e645-4b4d-986c-443d7c6aa2ef",
         "type": "Name",
         "content": "Initial B: David Playing the Harp",
         "classified_as": [
         ]
       },
     }]
   }
 * 
 * @return {Array} the list of values that map to the attributed value
 */
export function getAttributedBy(object, assignedProperty) {
  let attributed = normalizeFieldToArray(object, ATTRIBUTED_BY);
  if (attributed.length == 0) {
    return [];
  }

  return _getAssignedProperty(attributed, assignedProperty);
}

/**
 * Gets all of the objects from a LinkedArt Object's 'assigned_by' attribute which have
 * an 'assigned_property' attribute that matches the assignedProperty parameter.
 *
 * @param {Object} object - the LinkedArt Object
 * @param {String} assignedProperty  - the assigned properly (optional)
 * 
 * @example getAssignedBy(object, 'identified_by') would return the object with 'type': 'Name'
 * from the example object below
 *  {
 *   "assigned_by": [
     {
       "id": "https://data.getty.edu/museum/collection/object/5be2eb9f-1b4e-49f6-bfc4-0fc7ab67a1c5/name/f04fde5c-e645-4b4d-986c-443d7c6aa2ef/attribute-assignment",
       "type": "AttributeAssignment",
       "assigned_property": "identified_by",
       "assigned": {
         "id": "https://data.getty.edu/museum/collection/object/5be2eb9f-1b4e-49f6-bfc4-0fc7ab67a1c5/name/f04fde5c-e645-4b4d-986c-443d7c6aa2ef",
         "type": "Name",
         "content": "Initial B: David Playing the Harp",
         "classified_as": [
         ]
       },
     }]
     }
 * 
 * @return {Array} the list of values that map to the attributed value
 */
export function getAssignedBy(object, assignedProperty) {
  let assigned = normalizeFieldToArray(object, ASSIGNED_BY);
  if (assigned.length == 0) {
    return [];
  }

  return _getAssignedProperty(assigned, assignedProperty);
}

/**
 * Removes all duplicate objects (those with the same 'id' property) from an array
 * of objects. (keeps 1st object with a duplicated 'id' attribute and removes following
 * objects with the same 'id' attribute)
 *
 * @param {Array} _array - the array of objects to de-duplicate
 *
 * @example removeDuplicatesById([{id: '123', type: 'Object'}, {id: '123', type: 'Artist'}])
 * would return [{id: '123', type: 'Object'}]
 *
 * @private
 * @return {Array} the array of objects with duplicates removed
 */
export function removeDuplicatesById(_array) {
  const result = Array.from(new Set(_array.map((a) => a.id))).map((id) => {
    return _array.find((a) => a.id === id);
  });

  return result;
}

/**
 * Checks to see if an object's requested field has a part and returns the value. Otherwise,
 * returns either the requested field (if available) or an empty array (if neither is available)
 * 
 * @param {Object} object - the LinkedArt Object
 * @param {String} field  - the requested field to search for parts
 * 
 * @example (an example of the data)
 *  "produced_by": {
      "id": "https://data.getty.edu/museum/collection/object/f8fd6961-6da3-4c39-94ad-e8e9367fa51b/production",
      "type": "Production",
      "_label": "Production of Artwork",
      "part": [
        {
          "id": "https://data.getty.edu/museum/collection/object/f8fd6961-6da3-4c39-94ad-e8e9367fa51b/production/1663467e-66d8-4170-91b0-2937ba6447e6/producer-description",
          "type": "LinguisticObject",
          "_label": "Artist/Maker (Producer) Description",
          "classified_as": [
          ]
        },
      ]
    }
 * 
 * @return {Array} If the value of the object's field has a 'part' attribute, return it. If not, 
 * return the value of the object's field as an array. If the object does not have an attribute 
 * matching the field parameter return an empty array.
 */
export function getObjectParts(object, field) {
  const fieldRequested = object[field];
  if (!fieldRequested) {
    return [];
  }

  let fieldPart = normalizeFieldToArray(fieldRequested, PART);
  if (fieldPart.length == 0) {
    return [fieldRequested];
  }

  return fieldPart;
}

/**
 * Given an object or an array of objects, return the array or return an array containing the object.
 * Used to handle parameters like submittedResource where we need to handle either an object or array.
 *
 * @param {Object|Array} resourceParam - the parameter that needs to be transformed or confirmed to be
 * an array.
 * @private
 *
 * @returns {Array} an array
 */
function _convertToArrayIfNeeded(resourceParam) {
  let results = [];
  let resourceArray = resourceParam;

  if (resourceParam == undefined) {
    return results;
  }

  // make a single submittedResource into an array of submittedResources
  if (Array.isArray(resourceParam) == false) {
    resourceArray = [resourceParam];
  }
  return resourceArray;
}

/**
 * Gets the assigned property objects
 *
 * @param {Array} assigned
 * @param {String} assignedProperty (optional)
 * @private
 *
 * @returns {Array} - the assigned values
 */
function _getAssignedProperty(assigned, assignedProperty) {
  let accumulator = [];
  assigned.forEach((attr) => {
    if (
      attr[ASSIGNED_PROPERTY] == assignedProperty ||
      assignedProperty == undefined
    ) {
      if (Array.isArray(attr[ASSIGNED])) {
        accumulator = accumulator.concat(attr[ASSIGNED]);
      } else {
        accumulator.push(attr[ASSIGNED]);
      }
    }
  });
  return accumulator;
}

/**
 * Given an object or an array of objects, find all objects that are classified by an object
 * which is classified by nestedClassification
 *
 * @param {Object|Array} submittedResource - the object to inspect
 * @param {String|Array} nestedClassification - the classification ID/IDS to match
 * @param {String} classificationField - the field to investigate for an object's classification (e.g. classified_as, classified_by)
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 * @private
 *
 * @returns {Array} an array of objects that match
 */
export function _getObjectWithNestedClass(
  submittedResource,
  nestedClassification,
  classificationField = CLASSIFIED_AS,
  { language = undefined, languageOptions = {}, operator } = {}
) {
  let { objects } = _getObjectsAndClassificationsWithNestedClass(
    submittedResource,
    nestedClassification,
    classificationField,
    { language, languageOptions, operator }
  );

  return objects;
}

/**
 * Given an object or an array of objects, find all classification objects that are classified
 * with the nestedClassification. (e.g. for Visual Items we need to get the rights statement
 * classification object, which we identify by its own classification see VisualItems.getClearanceLevel)
 *
 * @param {Object} submittedResource - the object to inspect
 * @param {String|Array} nestedClassifications - the classification ID/IDS to match
 * @param {String} classificationField - the field to investigate for an object's classification (e.g. classified_as, classified_by)
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 * @private
 *
 * @returns {Array} an array of objects that match
 */
export function _getClassificationsWithNestedClass(
  submittedResource,
  nestedClassifications,
  classificationField = CLASSIFIED_AS,
  { language = undefined, languageOptions = {}, operator } = {}
) {
  let { classifications } = _getObjectsAndClassificationsWithNestedClass(
    submittedResource,
    nestedClassifications,
    classificationField,
    { language, languageOptions, operator }
  );

  return classifications;
}

/**
 * Given an object or an array of objects, find all classification objects that are classified
 * with the nestedClassification and return those classification objects as well as the full objects
 * from the original submittedResource to which those classification objects apply.
 *
 * (i.e. Some Linked Data objects will be classified by rights statements objects, such objects
 * will have different IDs depending upon the rights of the Linked Data object, but will themselves
 * be classified as "rights statements". This function could be used to grab all of the "rights
 * statements" classification objects from a set of Linked Data objects as well as all of the Linked
 * Data objects that are classified by a "rights statement" type of classification)
 *
 * @param {Object} submittedResource - the object to inspect
 * @param {String|Array} nestedClassifications - the classification ID/IDS to match
 * @param {String} classificationField - the field to investigate for an object's classification (e.g. classified_as, classified_by)
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 * @private
 *
 * @returns {Object} an object with 'classifications' and 'objects' attributes containing arrays of
 *   classification objects and full objects from the submittedResource respectively
 */
export function _getObjectsAndClassificationsWithNestedClass(
  submittedResource,
  nestedClassifications,
  classificationField = CLASSIFIED_AS,
  { language = undefined, languageOptions = {}, operator }
) {
  let returnObject = { classifications: [], objects: [] };
  let resourceArray = _convertToArrayIfNeeded(submittedResource);
  if (resourceArray.length == 0) {
    return returnObject;
  }

  for (const resource of resourceArray) {
    // If there are no classifications, or the classification is just a string,
    // continue
    var classified_as = normalizeFieldToArray(resource, classificationField);
    if (!classified_as.length) {
      continue;
    }
    let classifications = getClassified(classified_as, nestedClassifications, {
      classificationField,
      language,
      languageOptions,
      operator
    });
    returnObject.classifications =
      returnObject.classifications.concat(classifications);
    if (classifications.length > 0) {
      returnObject.objects.push(resource);
    }
  }

  return returnObject;
}

/**
 * Gets the specified sub-field values for a field that may have parts.
 *
 * @description
 * Helper function that returns an array of requested production/creation information, tries to reconcile where the production may have parts.
 * This can be useful to get the Timespan for a creation, or the creator, or other nested fields
 *
 * @param {Object} object - the JSON-LD HumanMadeObject or InformationObject
 * @param {String} field - the data field in the object to look for the subfield
 * @param {String} subfield - the subfield to look for
 *
 * @example gets the subfield regardless of whether the field has parts or not
 *  getFieldPartSubfield({produced_by: { part: [{carried_out_by: {id:123}}}]}, 'produced_by', 'carried_out_by'),  would return an array with one item [{id:123}]
 *
 * @returns {Array} an array of the matching values
 */
export function getFieldPartSubfield(object, field, subfield) {
  let parts = normalizeFieldWithParts(object, field);
  let accumulator = [];
  parts.forEach((part) => {
    let target = normalizeFieldToArray(part, subfield);
    accumulator = accumulator.concat(target);
  });

  return accumulator;
}

/**
 * Builds on getValuesByClassification to look inside a field for those values, e.g. "referred_to_by" or "identified_by" and returns the entries classified by the values
 *
 * @param {Object} submittedResource - the JSON-LD Object
 * @param {String} field - the field to look in (e.g. referred_to_by)
 * @param {String|Array} requestedClassifications - the AAT or local classifications
 * @param {Object} options additional options
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 * @param {String} options.operator - a boolean operator (AND|OR) for how to treat the nested classifications
 *
 *  @example getFieldValuesByClassifications(object, "referred_to_by", aat.ACKNOWLEDGEMENTS, 'ja') // would return all of the acknowledgement entries in japanese
 *
 * @returns {Array} an array of objects
 */
export function getFieldValuesByClassifications(
  submittedResource,
  field,
  requestedClassifications,
  { language, languageOptions, operator } = {}
) {
  return getValuesByClassification(
    normalizeFieldToArray(submittedResource, field),
    requestedClassifications,
    { language, languageOptions, operator }
  );
}

/**
 * @description Gets the descriptions(s) associated with an object if available.
 * @param {Object} submittedResource - JSON-LD object
 * @param {Object} options - additional options
 * @param {String|Array} options.requestedClassifications - AAT descriptions (default: {@link http://vocab.getty.edu/aat/300080091|aat.DESCRIPTION})
 * @param {String} options.language - limits the results to just a specific language (or leave undefined for all results)
 * @param {Object} options.languageOptions - any additional options when working with language(s) @see LanguageHelpers.doesObjectLanguageMatch
 *
 * @example getDescriptions(object) // gets descriptions(s) using defaults
 * @example getDescriptions(object, {language:'fr'}) // gets descriptions(s) in French
 *
 * @returns {Array} content of descriptions(s)
 */
export function getDescriptions(
  submittedResource,
  {
    requestedClassifications = aat.DESCRIPTION,
    language,
    languageOptions = {}
  } = {}
) {
  return getFieldValuesByClassifications(
    submittedResource,
    REFERRED_TO_BY,
    requestedClassifications,
    { language, languageOptions }
  );
}