/**
* Copyright Intellectual Reserve, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gedcomx.util;
import org.gedcomx.Gedcomx;
import org.gedcomx.common.ResourceReference;
import org.gedcomx.common.TextValue;
import org.gedcomx.common.URI;
import org.gedcomx.conclusion.*;
import org.gedcomx.records.*;
import org.gedcomx.source.Coverage;
import org.gedcomx.source.SourceDescription;
import org.gedcomx.types.RecordType;
import java.util.*;
/**
* Class for helping to deal with connecting field values with record descriptors for historical records
* and image browse data.
* Historical records have both structured data (persons with names, gender, facts; and relationships with facts)
* and 'fields'. A field often represents what was actually stated within some area of text on a historical
* document (or what was strongly implied by that document, such as the male gender of a father in a birth record).
* Each field has a list of field values, which can be of FieldValueType.Original, meaning what the document
* originally said (or structurally implied); or FieldValueType.Interpreted, meaning that a user or system
* interpreted. For example, a 'gender' field might have an original field value of "M", which, because it is
* in a Mexican census, means "Mujer", which means "Female". So the "Interpreted" field value might say
* "Mujer", or might say "Female"; or it is possible that both are included, since it is a list.
* These field values will be found within a Field of type "http://gedcomx.org/Gender", and that field will be
* found inside of the gender inside of the person it applies to. That gender will have a conclusional 'type'
* (e.g., "http://gedcomx.org/Female") that software will typically use.
* The 'structured data' of the record is typically used when dealing with what the record 'meant' or in displaying
* genealogical data to be used in copying over to a conclusion tree.
* The 'fields' of a record are typically used when display what the record originally 'said', in order to help
* communicate to a user some of the genealogical nuances that each record can have, including some that don't
* translate directly into the standardized conclusional structure that GedcomX supports.
* Having structured data with embedded fields (plus a list of fields at the person, relationship and record level)
* allows general-purpose genealogical use of 'what the record is telling us' (via the structure) as well as
* preservation of the special-purpose nuances of a particular record type (via the fields).
*
* A GedcomX document that represents a historical record will often have a SourceDescription that has a
* record 'descriptor' reference, which is the URL of a GedcomX document that represents the Collection that
* the record is found in, along with a "#" and local id of the record descriptor withing that collection's
* document, that describes the display labels to be used for displaying a field-value pair view of the
* record's field data.
* This class takes two GedcomX documents: a record and its collection (or, equivalently, the DocMap for both)
* and walks the structure of the record to find all of the field values, and builds the maps necessary to
* find all the field labelIds, the localized display labels for each labelId, and the values for each labelId
* found in the record. (Note that because a Field can have multiple FieldValues of the same type (original or
* interpreted), then each labelId can map to a list of values).
* Also, Census records are different from other records in that each person in a census household can have the
* same list of fields, so when displaying field values from census data, it must be done one person at a time.
* Therefore, each record will either support getValues(labelId) [i.e., for non-census] or
* getValues(person, labelId) [census], but not both.
* User: Randy Wilson
* Date: 7/31/2014
* Time: 11:07 AM
*/
public class FieldMap {
// DocMaps for the collection and record.
private DocMap collectionDocMap;
private DocMap recordDocMap;
// Main record descriptor for this record from this collection.
private RecordDescriptor recordDescriptor;
// Flag for whether this record was a census record (with person-specific fields) or not.
// A census collection can have the same field IDs over again for each person.
private boolean isCensus;
// Map of labelId -> FieldValueDescriptor for that label id.
private Map<String, FieldValueDescriptor> labelFieldValueDescriptorMap;
// map of labelId -> list of Strings that appeared in field values with that labelId.
private Map<String, List<String>> labelValueMap;
// map of Person -> labelId -> list of field value Strings (for census records).
// Includes a 'null' Person for record-level field values, if any.
private Map<Person, Map<String, List<String>>> personLabelValueMap;
/**
* Constructor for a collection and record GedcomX document.
* @param record - GedcomX document for a record (which is in the given collection).
* @param collection - GedcomX document for a collection (which contains the RecordDescriptor for the record).
*/
public FieldMap(Gedcomx record, Gedcomx collection) {
this(new DocMap(record), new DocMap(collection));
}
/**
* Constructor using a DocMap for a collection and record. Use this constructor if you've already built a DocMap
* for the record and/or collection.
* @param recordDocMap - DocMap for a GedcomX document for a record (which is in the given collection)
* @param collectionDocMap - DocMap for a GedcomX document for a collection (which contains the RecordDescriptor for the record).
*/
public FieldMap(DocMap recordDocMap, DocMap collectionDocMap) {
this.collectionDocMap = collectionDocMap;
this.recordDocMap = recordDocMap;
recordDescriptor = getRecordDescriptor(collectionDocMap, recordDocMap);
labelFieldValueDescriptorMap = getLabelFieldValueDescriptorMap(recordDescriptor);
isCensus = isCensus(recordDocMap);
if (isCensus) {
personLabelValueMap = getPersonLabelValueMap(recordDocMap.getDocument());
}
else {
labelValueMap = getLabelValuesMap(getAllFields(recordDocMap.getDocument()));
}
}
/**
* Get the DocMap for the collection that the record is found in.
* @return DocMap for the collection that the record is found in.
*/
public DocMap getCollectionDocMap() {
return collectionDocMap;
}
/**
* Get the DocMap for the record.
* @return DocMap for the record.
*/
public DocMap getRecordDocMap() {
return recordDocMap;
}
/**
* Get the GedcomX document for the collection that the record is found in.
* @return GedcomX document for the collection that the record is found in.
*/
public Gedcomx getCollection() {
return collectionDocMap.getDocument();
}
/**
* Get the GedcomX document for the record.
* @return GedcomX document for the record.
*/
public Gedcomx getRecord() {
return collectionDocMap.getDocument();
}
/**
* Get the display label for the given labelId in the closest language available to the one given.
* @param labelId labelId to get the display value for (e.g., "PR_NAME")
* @param language Preferred language to get the display label in. If null use "en-US".
* @return Display label to use for the labelId, or null if there is not one.
*/
public String getDisplayLabel(String labelId, String language) {
if (labelFieldValueDescriptorMap != null) {
FieldValueDescriptor fieldValueDescriptor = labelFieldValueDescriptorMap.get(labelId);
if (fieldValueDescriptor != null && fieldValueDescriptor.getDisplayLabels() != null) {
TextValue bestValue = LocaleUtil.findClosestLocale(fieldValueDescriptor.getDisplayLabels(), new Locale(language));
return bestValue == null ? null : bestValue.getValue();
}
}
return null;
}
/**
* Get a list of values that had the given labelId in the record. Must be a non-census record, or else the person
* must be specified.
* @param labelId - LabelId to get the values for
* @return List of values for the given labelId, or null if none.
*/
public List<String> getValues(String labelId) {
if (isCensus) {
throw new IllegalArgumentException("Can't call getValues(labelId) on census collection. Use getValues(person, labelId) instead.");
}
return labelValueMap.get(labelId);
}
/**
* Get a list of values that had the given labelId in the record for the given person.
* Must be a census record.
* @param person person to get values for (if null, get record-level values for the given labelId, if any)
* @param labelId LabelId to get the values for
* @return List of values for the given labelId, or null if none.
*/
public List<String> getValues(Person person, String labelId) {
if (!isCensus) {
throw new IllegalArgumentException("Can't call getValues(person, labelId) on non-census collection");
}
Map<String, List<String>> labelValueMap = personLabelValueMap.get(person);
return labelValueMap == null ? null : labelValueMap.get(labelId);
}
/**
* Get the FieldValueDescriptor for the given field value label ID.
* @param labelId - Label ID to find the FieldValueDescriptor for.
* @return FieldValueDescriptor with the given label ID, or null if not found.
*/
public FieldValueDescriptor getFieldValueDescriptor(String labelId) {
return labelFieldValueDescriptorMap == null ? null : labelFieldValueDescriptorMap.get(labelId);
}
/**
* Get a map of Person to labelId to list of Strings for field values that had that label ID within that person.
* Includes a null Person in the map if there were any record-level field values.
* @param record - Record to build map for.
* @return map of Person to labelId to list of field values.
*/
private static Map<Person, Map<String, List<String>>> getPersonLabelValueMap(Gedcomx record) {
Map<Person, List<Field>> personFieldMap = getPersonFieldMap(record);
Map<Person, Map<String, List<String>>> personMap = new LinkedHashMap<Person, Map<String, List<String>>>();
for (Person person : personFieldMap.keySet()) {
personMap.put(person, getLabelValuesMap(personFieldMap.get(person)));
}
return personMap;
}
/**
* Get a map of labelId to list of values for that labelId for the given person.
* Used only with census collections.
* @param person - Person to get the map for
* @return map of labelId to list of values for that labelId for the given person.
*/
public Map<String, List<String>> getPersonLabelValueMap(Person person) {
if (!isCensus) {
throw new IllegalArgumentException("Can't call getPersonLabelValueMap(person) on non-census collection");
}
return personLabelValueMap.get(person);
}
/**
* Get a map of labelId to list of values for that labelId.
* Used only with non-census collections.
* @return map of labelId to list of values for that labelId for the given person.
*/
public Map<String, List<String>> getLabelValueMap() {
if (isCensus) {
throw new IllegalArgumentException("Can't call getLabelValueMap() on census collection. Use getPersonLabelValueMap(person), and use person=null for record-level values.");
}
return labelValueMap;
}
/**
* Get the map of labelId to FieldValueDescriptor used by this FieldMap.
* @return map of labelId to FieldValueDescriptor used by this FieldMap, or null if there were no FieldDescriptors.
*/
public Map<String, FieldValueDescriptor> getLabelFieldValueDescriptorMap() {
return labelFieldValueDescriptorMap;
}
/**
* Get the RecordDescriptor that goes with this record.
* @return RecordDescriptor that goes with this record.
*/
public RecordDescriptor getRecordDescriptor() {
return recordDescriptor;
}
/**
* Find the RecordDescriptor from the collection document that is referenced by the main source description
* from the record document, i.e., find the record's record descriptor in the collection.
* @param collectionDocMap - DocMap for the collection GedcomX document.
* @param recordDocMap - DocMap for the record GedcomX document.
* @return Record's RecordDescriptor, or null if not found.
*/
public static RecordDescriptor getRecordDescriptor(DocMap collectionDocMap, DocMap recordDocMap) {
ResourceReference ref = recordDocMap.getMainSourceDescription().getDescriptorRef();
if (ref != null && ref.getResource() != null) {
String uri = ref.getResource().toString();
if (uri != null) {
int pos = uri.indexOf("#");
if (pos > 0) {
return collectionDocMap.getRecordDescriptor(uri.substring(pos + 1));
}
}
}
return null;
}
/**
* Get a map of labelId to FieldValueDescriptor for that label id.
* @param recordDescriptor - RecordDescriptor to build the map from.
* @return Map of labelId to FieldValueDescriptor, or null if the RecordDescriptor had no fields.
*/
public static Map<String, FieldValueDescriptor> getLabelFieldValueDescriptorMap(RecordDescriptor recordDescriptor) {
if (recordDescriptor != null && recordDescriptor.getFields() != null) {
Map<String, FieldValueDescriptor> map = new LinkedHashMap<String, FieldValueDescriptor>();
for (FieldDescriptor fieldDescriptor : recordDescriptor.getFields()) {
if (fieldDescriptor.getValues() != null) {
for (FieldValueDescriptor fieldValueDescriptor : fieldDescriptor.getValues()) {
if (map.get(fieldValueDescriptor.getLabelId()) != null) {
throw new IllegalStateException("Multiple field value descriptors for label id '" + fieldValueDescriptor.getLabelId() + "'");
}
map.put(fieldValueDescriptor.getLabelId(), fieldValueDescriptor);
}
}
}
return map;
}
return null;
}
/**
* Get a map of Person to the list of Fields for that person. A 'null' person is also
* included if there are any record-level fields.
* @param record - record to get field map from
* @return map of Person (or null for record-level fields) to the list of fields for that person.
*/
public static Map<Person, List<Field>> getPersonFieldMap(Gedcomx record) {
Map<Person, List<Field>> personFieldsMap = new LinkedHashMap<Person, List<Field>>();
addFields(record.getFields(), null, personFieldsMap);
if (record.getPersons() != null) {
for (Person person : record.getPersons()) {
addFields(person.getFields(), person, personFieldsMap);
if (person.getGender() != null) {
addFields(person.getGender().getFields(), person, personFieldsMap);
}
if (person.getNames() != null) {
for (Name name : person.getNames()) {
if (name.getNameForms() != null) {
for (NameForm nameForm : name.getNameForms()) {
addFields(nameForm.getFields(), person, personFieldsMap);
if (nameForm.getParts() != null) {
for (NamePart namePart : nameForm.getParts()) {
addFields(namePart.getFields(), person, personFieldsMap);
}
}
}
}
}
}
if (person.getFacts() != null) {
for (Fact fact : person.getFacts()) {
addFields(fact.getFields(), person, personFieldsMap);
if (fact.getDate() != null) {
addFields(fact.getDate().getFields(), person, personFieldsMap);
}
if (fact.getPlace() != null) {
addFields(fact.getPlace().getFields(), person, personFieldsMap);
}
}
}
}
}
return personFieldsMap;
}
/**
* Create a list of all of the fields occurring in the given GedcomX record, including those found
* within the various values. Note that this is a flat list that can't distinguish between the
* fields with the same label that belong to different persons, so only call when you're sure there
* will be no duplicates (as with a non-census record or a mapping Template).
* @param record - GedcomX to get fields for.
* @return list of fields occurring in the GedcomX record.
*/
public static List<Field> getAllFields(Gedcomx record) {
List<Field> fields = new ArrayList<Field>();
// Add fields that appear in persons
Map<Person, List<Field>> personFieldMap = getPersonFieldMap(record);
if (record.getPersons() != null) {
for (Person person : record.getPersons()) {
addFields(personFieldMap.get(person), fields);
}
}
// Add fields that appear in relationships
if (record.getRelationships() != null) {
for (Relationship relationship : record.getRelationships()) {
addFields(relationship.getFields(), fields);
if (relationship.getFacts() != null) {
for (Fact fact : relationship.getFacts()) {
addFields(fact.getFields(), fields);
if (fact.getDate() != null) {
addFields(fact.getFields(), fields);
}
if (fact.getPlace() != null) {
addFields(fact.getFields(), fields);
}
}
}
}
}
// Add fields that appear in SourceDescriptions
if (record.getSourceDescriptions() != null) {
for (SourceDescription sourceDescription : record.getSourceDescriptions()) {
addFields(sourceDescription.getFields(), fields);
}
}
// Add record-level fields.
addFields(personFieldMap.get(null), fields);
return fields;
}
/**
* Get a map of labelId to values from all of the FieldValues that appear in the given list of Fields.
* If two FieldValues have the same labelId, then they are put into the same list.
* @param fields - list of Fields to look in for FieldValues.
* @return map of labelId to FieldValue values.
*/
public static Map<String, List<String>> getLabelValuesMap(List<Field> fields) {
Map<String, List<String>> labelValueMap = new LinkedHashMap<String, List<String>>();
if (fields != null) {
for (Field field : fields) {
if (field.getValues() != null) {
for (FieldValue fieldValue : field.getValues()) {
List<String> values = labelValueMap.get(fieldValue.getLabelId());
if (values == null) {
values = new ArrayList<String>();
labelValueMap.put(fieldValue.getLabelId(), values);
}
values.add(fieldValue.getText());
}
}
}
}
return labelValueMap;
}
/**
* Add the given list of Fields (if not empty or null) to the list of fields for the given person,
* in the personFieldMap. If there is no list for the given person, then one is created and
* added to the map.
* @param listToAdd - List of Fields to add (or null if none).
* @param person - Person to whose list the fields should be added (null => record-level fields)
* @param personFieldMap - Map to add the list of fields to.
*/
private static void addFields(List<Field> listToAdd, Person person, Map<Person, List<Field>> personFieldMap) {
if (listToAdd != null) {
List<Field> fieldList = personFieldMap.get(person);
if (fieldList == null) {
fieldList = new ArrayList<Field>();
personFieldMap.put(person, fieldList);
}
fieldList.addAll(listToAdd);
}
}
/**
* Add the given list of fields to the master list.
* @param listToAdd - List of fields to add.
* @param allFields - List to add the fields to.
*/
private static void addFields(List<Field> listToAdd, List<Field> allFields) {
if (listToAdd != null) {
allFields.addAll(listToAdd);
}
}
/**
* Tell whether the given record is a Census record, i.e., if it has a SourceDescription with a Coverage with
* a RecordType of Census.
* @param record - GedcomX record to examine
* @return true if this is a census record, false otherwise.
*/
public static boolean isCensus(Gedcomx record) {
URI descriptionRef = record.getDescriptionRef();
if (descriptionRef != null) {
for (SourceDescription sourceDescription : record.getSourceDescriptions()) {
if (("#" + sourceDescription.getId()).equals(descriptionRef.toString())) {
return isCensus(sourceDescription);
}
}
}
return false;
}
/**
* Tell whether the record with the given DocMap is a Census record, i.e., if it has a SourceDescription with a Coverage with
* a RecordType of Census.
* @param recordDocMap - DocMap of the GedcomX record to examine
* @return true if this is a census record, false otherwise.
*/
public static boolean isCensus(DocMap recordDocMap) {
return isCensus(recordDocMap.getMainSourceDescription());
}
/**
* Tell whether the given SourceDescription has 'coverage' with a RecordType of census.
* @param sourceDescription - SourceDescription to examine for coverage.
* @return true if this is a census record, false otherwise.
*/
public static boolean isCensus(SourceDescription sourceDescription) {
if (sourceDescription != null && sourceDescription.getCoverage() != null) {
for (Coverage coverage : sourceDescription.getCoverage()) {
if (coverage.getKnownRecordType() == RecordType.Census) {
return true;
}
}
}
return false;
}
}