nl.knaw.huygens.timbuctoo.tools.importer.dcar.DutchCaribbeanImporter.java Source code

Java tutorial

Introduction

Here is the source code for nl.knaw.huygens.timbuctoo.tools.importer.dcar.DutchCaribbeanImporter.java

Source

package nl.knaw.huygens.timbuctoo.tools.importer.dcar;

/*
 * #%L
 * Timbuctoo tools
 * =======
 * Copyright (C) 2012 - 2015 Huygens ING
 * =======
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */

import static com.google.common.base.Preconditions.checkNotNull;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_ARCHIVER_KEYWORD;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_ARCHIVER_PERSON;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_ARCHIVER_PLACE;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_ARCHIVE_KEYWORD;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_ARCHIVE_PERSON;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_ARCHIVE_PLACE;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_LEGISLATION_KEYWORD;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_LEGISLATION_PERSON;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_LEGISLATION_PLACE;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_PARENT_ARCHIVE;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_SIBLING_ARCHIVE;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.HAS_SIBLING_ARCHIVER;
import static nl.knaw.huygens.timbuctoo.model.dcar.RelTypeNames.IS_CREATOR_OF;

import java.io.File;
import java.util.Map;

import nl.knaw.huygens.timbuctoo.Repository;
import nl.knaw.huygens.timbuctoo.index.IndexManager;
import nl.knaw.huygens.timbuctoo.model.Archive;
import nl.knaw.huygens.timbuctoo.model.Archiver;
import nl.knaw.huygens.timbuctoo.model.Keyword;
import nl.knaw.huygens.timbuctoo.model.Legislation;
import nl.knaw.huygens.timbuctoo.model.Person;
import nl.knaw.huygens.timbuctoo.model.Reference;
import nl.knaw.huygens.timbuctoo.model.RelationType;
import nl.knaw.huygens.timbuctoo.model.dcar.DCARArchive;
import nl.knaw.huygens.timbuctoo.model.dcar.DCARArchiver;
import nl.knaw.huygens.timbuctoo.model.dcar.DCARKeyword;
import nl.knaw.huygens.timbuctoo.model.dcar.DCARLegislation;
import nl.knaw.huygens.timbuctoo.model.dcar.DCARPerson;
import nl.knaw.huygens.timbuctoo.model.dcar.DCARRelation;
import nl.knaw.huygens.timbuctoo.model.dcar.XRelated;
import nl.knaw.huygens.timbuctoo.model.util.PersonName;
import nl.knaw.huygens.timbuctoo.model.util.PersonNameComponent.Type;
import nl.knaw.huygens.timbuctoo.tools.config.ToolsInjectionModule;
import nl.knaw.huygens.timbuctoo.tools.importer.DefaultImporter;
import nl.knaw.huygens.timbuctoo.util.Files;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Maps;
import com.google.inject.Injector;

/**
 * Imports data of the "Dutch Caribbean" project.
 * 
 * Usage:
 *  java  -cp  [specs]  ${package-name}.DutchCaribbeanImporter  [importDirName]
 */
public class DutchCaribbeanImporter extends DefaultImporter {

    private static final Logger LOG = LoggerFactory.getLogger(DutchCaribbeanImporter.class);

    public static void main(String[] args) throws Exception {
        Stopwatch stopWatch = Stopwatch.createStarted();

        // Handle commandline arguments
        String importDirName = (args.length > 0) ? args[0] : "../../AtlantischeGids/work/";

        DutchCaribbeanImporter importer = null;
        try {
            Injector injector = ToolsInjectionModule.createInjector();
            Repository repository = injector.getInstance(Repository.class);
            IndexManager indexManager = injector.getInstance(IndexManager.class);

            importer = new DutchCaribbeanImporter(repository, indexManager, importDirName);
            importer.importAll();
        } finally {
            if (importer != null) {
                importer.close();
            }
            LOG.info("Time used: {}", stopWatch);
        }
    }

    // -------------------------------------------------------------------

    private static final String[] JSON_EXTENSION = { "json" };

    private final ObjectMapper objectMapper;
    private final File inputDir;

    private final Map<String, Reference> keywordRefMap = Maps.newHashMap();
    private final Map<String, Reference> personRefMap = Maps.newHashMap();
    private final Map<String, Reference> legislationRefMap = Maps.newHashMap();
    private final Map<String, Reference> archiveRefMap = Maps.newHashMap();
    private final Map<String, Reference> archiverRefMap = Maps.newHashMap();

    private Reference isCreatorRef;
    private Reference hasArchiveKeywordRef;
    private Reference hasArchiverKeywordRef;
    private Reference hasLegislationKeywordRef;
    private Reference hasArchivePersonRef;
    private Reference hasArchiverPersonRef;
    private Reference hasLegislationPersonRef;
    private Reference hasArchivePlaceRef;
    private Reference hasArchiverPlaceRef;
    private Reference hasLegislationPlaceRef;
    private Reference hasParentArchive;
    private Reference hasSiblingArchive;
    private Reference hasSiblingArchiver;

    public DutchCaribbeanImporter(Repository repository, IndexManager indexManager, String inputDirName) {
        super(repository, indexManager, "dcar");
        objectMapper = new ObjectMapper();
        inputDir = new File(inputDirName);
        System.out.printf("%n.. Importing from %s%n", inputDir.getAbsolutePath());
    }

    private <T> T readJsonValue(File file, Class<T> valueType) throws Exception {
        String text = Files.readTextFromFile(file);
        // For Dutch Caribbean it seems OK to map " " --> " "
        String converted = encodingFixer.convert2(text).replaceAll(" ", " ");
        if (!converted.equals(text)) {
            int n = text.length() - converted.length();
            handleError("Fixed %d character encoding error%s in '%s'", n, (n == 1) ? "" : "s", file.getName());
        }
        return objectMapper.readValue(converted, valueType);
    }

    public void importAll() throws Exception {

        printBoxedText("1. Initialization");

        removeNonPersistentEntities(DCARKeyword.class);
        removeNonPersistentEntities(DCARPerson.class);
        removeNonPersistentEntities(DCARArchive.class);
        removeNonPersistentEntities(DCARArchiver.class);
        removeNonPersistentEntities(DCARLegislation.class);

        System.out.printf("%n.. Setup relation types%n");
        importRelationTypes();
        setupRelationTypes();

        printBoxedText("2. Basic properties");

        System.out.println(".. Keywords");
        importKeywords(keywordRefMap);
        System.out.printf("Number of entries = %d%n", keywordRefMap.size());

        System.out.println(".. Persons");
        importPersons(personRefMap);
        System.out.printf("Number of entries = %d%n", personRefMap.size());

        System.out.println(".. Legislation");
        importLegislation(legislationRefMap);
        System.out.printf("Number of entries = %d%n", legislationRefMap.size());

        System.out.println(".. Archives");
        importArchives(archiveRefMap);
        System.out.printf("Number of entries = %d%n", archiveRefMap.size());

        System.out.println(".. Archivers");
        importArchivers(archiverRefMap);
        System.out.printf("Number of entries = %d%n", archiverRefMap.size());

        printBoxedText("3. Relations");

        System.out.println(".. Legislation");
        importLegislationRelations(legislationRefMap);

        System.out.println(".. Archives");
        importArchiveRelations(archiveRefMap);

        System.out.println(".. Archivers");
        importArchiverRelations(archiverRefMap);

        printBoxedText("4. Indexing");

        indexEntities(DCARKeyword.class);
        indexEntities(DCARPerson.class);
        indexEntities(DCARLegislation.class);
        indexEntities(DCARArchive.class);
        indexEntities(DCARArchiver.class);

        displayStatus();
        displayErrorSummary();
    }

    // --- relations -----------------------------------------------------

    private void setupRelationTypes() {
        isCreatorRef = retrieveRelationType(IS_CREATOR_OF.regular);
        hasArchiveKeywordRef = retrieveRelationType(HAS_ARCHIVE_KEYWORD.regular);
        hasArchiverKeywordRef = retrieveRelationType(HAS_ARCHIVER_KEYWORD.regular);
        hasLegislationKeywordRef = retrieveRelationType(HAS_LEGISLATION_KEYWORD.regular);
        hasArchivePersonRef = retrieveRelationType(HAS_ARCHIVE_PERSON.regular);
        hasArchiverPersonRef = retrieveRelationType(HAS_ARCHIVER_PERSON.regular);
        hasLegislationPersonRef = retrieveRelationType(HAS_LEGISLATION_PERSON.regular);
        hasArchivePlaceRef = retrieveRelationType(HAS_ARCHIVE_PLACE.regular);
        hasArchiverPlaceRef = retrieveRelationType(HAS_ARCHIVER_PLACE.regular);
        hasLegislationPlaceRef = retrieveRelationType(HAS_LEGISLATION_PLACE.regular);
        hasParentArchive = retrieveRelationType(HAS_PARENT_ARCHIVE.regular);
        hasSiblingArchive = retrieveRelationType(HAS_SIBLING_ARCHIVE.regular);
        hasSiblingArchiver = retrieveRelationType(HAS_SIBLING_ARCHIVER.regular);
    }

    private Reference retrieveRelationType(String name) {
        RelationType type = repository.getRelationTypeByName(name, true);
        return new Reference(RelationType.class, type.getId());
    }

    private void addRegularRelations(Reference sourceRef, Reference relTypeRef, Map<String, Reference> map,
            String[] keys) {
        if (keys != null) {
            for (String key : keys) {
                addRelation(DCARRelation.class, relTypeRef, sourceRef, map.get(key), change, "");
            }
        }
    }

    private void addInverseRelations(Reference targetRef, Reference relTypeRef, Map<String, Reference> map,
            String[] keys) {
        if (keys != null) {
            for (String key : keys) {
                addRelation(DCARRelation.class, relTypeRef, map.get(key), targetRef, change, "");
            }
        }
    }

    // --- Keywords ------------------------------------------------------

    private static final String KEYWORD_DIR = "keywords";
    private static final String KEYWORD_FILE = "keywords.json";

    private void importKeywords(Map<String, Reference> referenceMap) throws Exception {
        File file = new File(new File(inputDir, KEYWORD_DIR), KEYWORD_FILE);
        for (XKeyword xkeyword : readJsonValue(file, XKeyword[].class)) {
            String jsonId = xkeyword._id;
            if (referenceMap.containsKey(jsonId)) {
                handleError("[%s] Duplicate keyword id %s", KEYWORD_FILE, jsonId);
            } else {
                DCARKeyword keyword = convert(xkeyword);
                String storedId = addDomainEntity(DCARKeyword.class, keyword);
                referenceMap.put(jsonId, new Reference(Keyword.class, storedId));
            }
        }
    }

    private DCARKeyword convert(XKeyword xkeyword) {
        DCARKeyword keyword = new DCARKeyword();

        String type = xkeyword.type;
        keyword.setType(type);

        if (type == null) {
            handleError("[%s] Missing type for id %s", KEYWORD_FILE, xkeyword._id);
        } else if (type.equals("subject")) {
            keyword.setValue(xkeyword.onderwerp);
        } else if (type.equals("geography")) {
            keyword.setValue(xkeyword.regionaam);
        } else {
            handleError("[%s] Unknown type %s", KEYWORD_FILE, type);
            keyword.setValue("?");
        }

        if (xkeyword.label != null) {
            keyword.setLabel(xkeyword.label);
        }

        return keyword;
    }

    // --- Persons -------------------------------------------------------

    private static final String PERSON_DIR = "keywords";
    private static final String PERSON_FILE = "persons.json";

    private void importPersons(Map<String, Reference> referenceMap) throws Exception {
        File file = new File(new File(inputDir, PERSON_DIR), PERSON_FILE);
        for (XPerson xperson : readJsonValue(file, XPerson[].class)) {
            String jsonId = xperson._id;
            if (referenceMap.containsKey(jsonId)) {
                handleError("[%s] Duplicate person id %s", PERSON_FILE, jsonId);
            } else {
                DCARPerson person = convert(xperson);
                String storedId = addDomainEntity(DCARPerson.class, person);
                referenceMap.put(jsonId, new Reference(Person.class, storedId));
            }
        }
    }

    private DCARPerson convert(XPerson xperson) {
        DCARPerson person = new DCARPerson();

        PersonName name = new PersonName();
        if (xperson.voorl != null) {
            name.addNameComponent(Type.FORENAME, xperson.voorl);
        }
        if (xperson.tussenv != null) {
            name.addNameComponent(Type.NAME_LINK, xperson.tussenv);
        }
        if (xperson.achternaam != null) {
            name.addNameComponent(Type.SURNAME, xperson.achternaam);
        }
        // should be prefix and suffix
        if (xperson.toevoeging != null) {
            name.addNameComponent(Type.ADD_NAME, xperson.toevoeging);
        }
        person.addName(name);

        if (xperson.label != null) {
            String value = StringUtils.join(xperson.label, "; ");
            person.setLabel(value);
        }

        if (xperson.verwijzing != null) {
            String value = StringUtils.join(xperson.verwijzing, "; ");
            person.setReference(value);
        }

        return person;
    }

    // --- Legislation ---------------------------------------------------

    private static final String WETGEVING_DIR = "wetgeving";

    private void importLegislation(Map<String, Reference> referenceMap) throws Exception {
        File directory = new File(inputDir, WETGEVING_DIR);
        for (File file : FileUtils.listFiles(directory, JSON_EXTENSION, true)) {
            for (WetgevingEntry entry : readJsonValue(file, WetgevingEntry[].class)) {
                Wetgeving wetgeving = entry.wetgeving;
                String jsonId = wetgeving._id;
                if (referenceMap.containsKey(jsonId)) {
                    handleError("[%s] Duplicate 'wetgeving' id %s", file.getName(), jsonId);
                } else {
                    DCARLegislation legislation = convert(wetgeving);
                    String storedId = addDomainEntity(DCARLegislation.class, legislation);
                    referenceMap.put(jsonId, new Reference(Legislation.class, storedId));
                }
            }
        }
    }

    private DCARLegislation convert(Wetgeving wetgeving) {
        DCARLegislation legislation = new DCARLegislation();
        legislation.setOrigFilename(wetgeving.orig_filename);
        legislation.setReference(wetgeving.reference);
        legislation.setPages(wetgeving.pages);
        legislation.setTitleNld(wetgeving.titel);
        legislation.setTitleEng(wetgeving.titel_eng);
        if (wetgeving.dates != null) {
            legislation.setDate1(wetgeving.dates.date1);
            legislation.setDate2(wetgeving.dates.date2);
        }
        legislation.setContents(wetgeving.contents);
        if (wetgeving.see_also != null) {
            for (XSeeAlso item : wetgeving.see_also) {
                legislation.addSeeAlso(item.toString());
            }
        }
        if (wetgeving.other_publication != null) {
            for (String publication : wetgeving.other_publication) {
                legislation.addOtherPublication(publication);
            }
        }
        legislation.setOriginalArchivalSource(wetgeving.original_archival_source);
        legislation.setLinkArchivalDBase(wetgeving.link_archival_dbase);
        legislation.setRemarks(wetgeving.remarks);
        legislation.setScan(wetgeving.scan);
        legislation.setPartsToScan(wetgeving.partstoscan);
        legislation.setMadeBy(wetgeving.made_by);
        legislation.setReminders(wetgeving.Aantekeningen);
        return legislation;
    }

    private void importLegislationRelations(Map<String, Reference> referenceMap) throws Exception {
        File directory = new File(inputDir, WETGEVING_DIR);
        for (File file : FileUtils.listFiles(directory, JSON_EXTENSION, true)) {
            for (WetgevingEntry entry : readJsonValue(file, WetgevingEntry[].class)) {
                Wetgeving wetgeving = entry.wetgeving;
                Reference reference = referenceMap.get(wetgeving._id);
                if (reference == null) {
                    throw new IllegalStateException("Failed to retrieve legislation " + wetgeving._id);
                }
                addLegislationRelations(reference, wetgeving);
            }
        }
    }

    private void addLegislationRelations(Reference reference, Wetgeving wetgeving) {
        checkNotNull(reference, "Missing legislation reference");
        addRegularRelations(reference, hasLegislationPlaceRef, keywordRefMap, wetgeving.geography);
        addRegularRelations(reference, hasLegislationKeywordRef, keywordRefMap, wetgeving.keywords);
        addRegularRelations(reference, hasLegislationKeywordRef, keywordRefMap, wetgeving.keywords_extra);
        addRegularRelations(reference, hasLegislationPersonRef, personRefMap, wetgeving.persons);
    }

    // --- Archives ------------------------------------------------------

    private static final String ARCHIEFMAT_DIR = "archiefmat";

    private void importArchives(Map<String, Reference> referenceMap) throws Exception {
        File directory = new File(inputDir, ARCHIEFMAT_DIR);
        for (File file : FileUtils.listFiles(directory, JSON_EXTENSION, true)) {
            for (ArchiefMatEntry entry : readJsonValue(file, ArchiefMatEntry[].class)) {
                ArchiefMat archiefmat = entry.archiefmat;
                String jsonId = archiefmat._id;
                if (referenceMap.containsKey(jsonId)) {
                    handleError("[%s] Duplicate 'archiefmat' id %s", file.getName(), jsonId);
                } else {
                    DCARArchive archive = convert(archiefmat);
                    String storedId = addDomainEntity(DCARArchive.class, archive);
                    referenceMap.put(jsonId, new Reference(Archive.class, storedId));
                }
            }
        }
    }

    private DCARArchive convert(ArchiefMat archiefmat) {
        DCARArchive archive = new DCARArchive();
        archive.setOrigFilename(archiefmat.orig_filename);
        if (archiefmat.countries != null) {
            for (String country : archiefmat.countries) {
                archive.addCountry(country);
            }
        }
        archive.setRefCodeArchive(archiefmat.rf_archive);
        archive.setRefCode(archiefmat.ref_code);
        archive.setSubCode(archiefmat.code_subfonds);
        archive.setSeries(archiefmat.series);
        archive.setItemNo(archiefmat.itemno);
        archive.setTitleNld(archiefmat.titel);
        archive.setTitleEng(archiefmat.titel_eng);
        if (archiefmat.dates != null) {
            archive.setBeginDate(archiefmat.dates.begin_date);
            archive.setEndDate(archiefmat.dates.end_date);
        }
        archive.setPeriodDescription(archiefmat.period_description);
        archive.setExtent(archiefmat.extent);
        archive.setFindingAid(archiefmat.finding_aid);
        archive.setScope(archiefmat.scope);
        archive.setNotes(archiefmat.notes);
        archive.setMadeBy(archiefmat.made_by);
        archive.setReminders(archiefmat.Aantekeningen);
        // Ignored fields
        if (archiefmat.relation != null) {
            handleError("[%s] Ignoring field 'relation': '%s'", archiefmat.orig_filename, archiefmat.relation);
        }
        if (archiefmat.em != null) {
            handleError("[%s] Ignoring field 'em': '%s'", archiefmat.orig_filename, archiefmat.em);
        }
        return archive;
    }

    private void importArchiveRelations(Map<String, Reference> referenceMap) throws Exception {
        File directory = new File(inputDir, ARCHIEFMAT_DIR);
        for (File file : FileUtils.listFiles(directory, JSON_EXTENSION, true)) {
            for (ArchiefMatEntry entry : readJsonValue(file, ArchiefMatEntry[].class)) {
                ArchiefMat archiefmat = entry.archiefmat;
                Reference reference = referenceMap.get(archiefmat._id);
                if (reference == null) {
                    throw new IllegalStateException("Failed to retrieve archive " + archiefmat._id);
                }
                addArchiveRelations(reference, archiefmat);
            }
        }
    }

    private void addArchiveRelations(Reference reference, ArchiefMat archiefmat) {
        checkNotNull(reference, "Missing archive reference");
        addRegularRelations(reference, hasArchivePlaceRef, keywordRefMap, archiefmat.geography);
        addRegularRelations(reference, hasArchiveKeywordRef, keywordRefMap, archiefmat.keywords);
        addRegularRelations(reference, hasArchivePersonRef, personRefMap, archiefmat.persons);

        if (archiefmat.related != null) {
            for (XRelated item : archiefmat.related) {
                if ("overhead_title".equals(item.type)) {
                    // hasChildArchive relation is implied
                    addRegularRelations(reference, hasParentArchive, archiveRefMap, item.ids);
                } else if ("underlying_levels_titels".equals(item.type)) {
                    // hasChildArchive relation is implied
                    addInverseRelations(reference, hasParentArchive, archiveRefMap, item.ids);
                } else if ("unit".equals(item.type)) {
                    // symmetric, stored in canonical form
                    addRegularRelations(reference, hasSiblingArchive, archiveRefMap, item.ids);
                } else {
                    handleError("[%s] Ignoring field 'archiefmat.related' with type '%s'", archiefmat.orig_filename,
                            item.type);
                }
            }
        }
        addInverseRelations(reference, isCreatorRef, archiverRefMap, archiefmat.creators);
    }

    // --- Archivers -----------------------------------------------------

    private static final String CREATORS_DIR = "creators";

    private void importArchivers(Map<String, Reference> referenceMap) throws Exception {
        File directory = new File(inputDir, CREATORS_DIR);
        for (File file : FileUtils.listFiles(directory, JSON_EXTENSION, true)) {
            for (CreatorEntry entry : readJsonValue(file, CreatorEntry[].class)) {
                Creator creator = entry.creator;
                String jsonId = creator._id;
                if (referenceMap.containsKey(jsonId)) {
                    handleError("[%s] Duplicate 'creator' id %s", file.getName(), jsonId);
                } else {
                    DCARArchiver archiver = convert(creator);
                    String storedId = addDomainEntity(DCARArchiver.class, archiver);
                    referenceMap.put(jsonId, new Reference(Archiver.class, storedId));
                }
            }
        }
    }

    private DCARArchiver convert(Creator creator) {
        DCARArchiver archiver = new DCARArchiver();
        archiver.setOrigFilename(creator.orig_filename);
        archiver.setNameNld(creator.name);
        archiver.setNameEng(creator.name_english);
        if (creator.dates != null) {
            archiver.setBeginDate(creator.dates.begin_date);
            archiver.setEndDate(creator.dates.end_date);
        }
        archiver.setPeriodDescription(creator.period_description);
        archiver.setHistory(creator.his_func);
        archiver.setNotes(creator.notes);
        archiver.setLiterature(creator.literatuur);
        archiver.setMadeBy(creator.made_by);
        archiver.setReminders(creator.Aantekeningen);
        if (creator.types != null) {
            for (String type : creator.types) {
                archiver.addType(type);
            }
        }
        // Ignored fields; should have been preprocessed
        if (creator.related_archives != null) {
            handleError("[%s] Ignoring field 'creator.related_archives'", creator.orig_filename);
        }
        return archiver;
    }

    private void importArchiverRelations(Map<String, Reference> referenceMap) throws Exception {
        File directory = new File(inputDir, CREATORS_DIR);
        for (File file : FileUtils.listFiles(directory, JSON_EXTENSION, true)) {
            for (CreatorEntry entry : readJsonValue(file, CreatorEntry[].class)) {
                Creator creator = entry.creator;
                Reference reference = referenceMap.get(creator._id);
                if (reference == null) {
                    throw new IllegalStateException("Failed to retrieve archive " + creator._id);
                }
                addArchiverRelations(reference, creator);
            }
        }
    }

    private void addArchiverRelations(Reference reference, Creator creator) {
        checkNotNull(reference, "Missing archiver reference");
        addRegularRelations(reference, hasArchiverPlaceRef, keywordRefMap, creator.geography);
        addRegularRelations(reference, hasArchiverKeywordRef, keywordRefMap, creator.keywords);
        addRegularRelations(reference, hasArchiverPersonRef, personRefMap, creator.persons);

        if (creator.related != null) {
            for (XRelated item : creator.related) {
                if ("archive".equals(item.type)) {
                    // isCreatedByRef relation is implied
                    addRegularRelations(reference, isCreatorRef, archiveRefMap, item.ids);
                } else if ("creator".equals(item.type)) {
                    // symmetric, stored in canonical form
                    addRegularRelations(reference, hasSiblingArchiver, archiverRefMap, item.ids);
                } else {
                    handleError("[%s] Ignoring field 'creator.related' with type '%s'", creator.orig_filename,
                            item.type);
                }
            }
        }
        // symmetric, stored in canonical form
        addRegularRelations(reference, hasSiblingArchiver, archiverRefMap, creator.related_creators);
    }

    // -------------------------------------------------------------------
    // --- Data model defined in ING Forms -------------------------------
    // -------------------------------------------------------------------

    public static class XKeyword {
        /** ### Assigned id (admin) */
        public String _id;

        public String type;

        public String label;

        public String onderwerp;

        public String regionaam;

        @Override
        public String toString() {
            return String.format("%-5s %-10s %-40s %-30s %s", _id, type, onderwerp, regionaam, label);
        }
    }

    // -------------------------------------------------------------------

    public static class XPerson {
        /** ### Assigned id (admin) */
        public String _id;

        public String type;

        public String voorl;

        public String tussenv;

        public String achternaam;

        public String toevoeging;

        public String[] verwijzing;

        public String[] label;
    }

    // -------------------------------------------------------------------

    // TODO remove markup - various items contain paragraph tags
    public static class Wetgeving {
        /** ### Assigned id (admin) */
        public String _id;

        /** ### Name of source file (admin) */
        public String orig_filename;

        /** "Reference" */
        public String reference;

        /** "Pages" */
        public String pages;

        /** "Short title" */
        public String titel;

        /** "English title" */
        public String titel_eng;

        // TODO Provide more meaningful names
        /** "Date" and "Date 2" */
        public XDates dates;

        /** "Keyword(s) geography" */
        public String[] geography;

        /** "Keyword(s) Group classification" */
        public String[] keywords;

        /** "Keyword(s) other subject" */
        public String[] keywords_extra;

        /** "Keyword(s) person" */
        public String[] persons;

        /** "Summary of contents" */
        public String contents;

        /** "See also" */
        public XSeeAlso[] see_also;

        /** "Earlier/later publications" */
        public String[] other_publication;

        /** "Original archival source" */
        public String original_archival_source;

        /** "Link archival database" */
        public String link_archival_dbase;

        /** "Remarks" */
        public String remarks;

        /** "Scan" */
        public String scan;

        /** "Parts to scan" */
        public String partstoscan;

        /** "Record made by-" */
        public String made_by;

        /** "Reminders" */
        public String Aantekeningen;

        // TODO Check: never used
        /** "Binnenkomende relaties" */
        public XRelated[] related;
    }

    public static class WetgevingEntry {
        public Wetgeving wetgeving;
    }

    // -------------------------------------------------------------------

    public static class ArchiefMat {
        /** ### Assigned id (admin) */
        public String _id;

        /** ### Name of source file (admin) */
        public String orig_filename;

        /** "Ref. code country" ??? */
        public String[] countries;

        /** "Ref. code repository" ??? */
        public String rf_archive;

        /** "Reference code" */
        public String ref_code;

        /** "Code or indication of sub-fonds" */
        public String code_subfonds;

        /** "Indication of series, Nos." */
        public String series;

        /** "Item, No." */
        public String itemno;

        /** "Title" */
        public String titel;

        /** "English title" */
        public String titel_eng;

        /** "Begin date" and "End date" */
        public XPeriod dates;

        /** "Period description" */
        public String period_description;

        /** "Extent" */
        public String extent;

        /** "Additional finding aid" */
        public String finding_aid;

        /** "Name(s) of Creator(s)" */
        public String[] creators;

        /** "Scope and content" */
        public String scope;

        /** "Title(s) related underlying level(s) of description" ??? */
        public String relation;

        /** "Other related units of description" ??? */
        public String em;

        /** "Keyword(s) geography" */
        public String[] geography;

        /** "Keyword(s) subject" */
        public String[] keywords;

        /** "Keyword(s) person" */
        public String[] persons;

        /** "Remarks" ??? */
        public String notes;

        /** "Record made by-" */
        public String made_by;

        /** "Reminders" ??? */
        public String Aantekeningen;

        /** "Binnenkomende relaties" ??? */
        public XRelated[] related;

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append(_id);
            if (related != null) {
                for (XRelated entry : related) {
                    builder.append(" ").append(entry.type);
                }
            }
            return builder.toString();
        }
    }

    public static class ArchiefMatEntry {
        public ArchiefMat archiefmat;
    }

    // -------------------------------------------------------------------

    public static class Creator {
        /** ### Assigned id (admin) */
        public String _id;

        /** ### Name of source file (admin) */
        public String orig_filename;

        /** "Name" */
        public String name;

        /** "English name" */
        public String name_english;

        /** ### "Begin date" and "End date" */
        public XPeriod dates;

        /** "Period description" */
        public String period_description;

        /** "History/functions/occupations/activities" */
        public String his_func;

        /** "Title(s) related archive(s)" */
        public String[] related_archives;

        /** "Title(s) related creator(s)" */
        public String[] related_creators;

        /** "Keyword(s) geography" */
        public String[] geography;

        /** "Keyword(s) subject" */
        public String[] keywords;

        /** "Keyword(s) person" */
        public String[] persons;

        /** "Remarks" */
        public String notes;

        /** "Literature" */
        public String literatuur;

        /** "Record made by-" */
        public String made_by;

        /** "Reminders" */
        public String Aantekeningen;

        /** "Binnenkomende relaties" */
        public XRelated[] related;

        /** ??? ("person", "family") */
        public String[] types;
    }

    public static class CreatorEntry {
        public Creator creator;
    }

    // -------------------------------------------------------------------

    private static class XDates {
        public String date1;
        public String date2;
    }

    private static class XPeriod {
        public String begin_date;
        public String end_date;
    }

    private static class XSeeAlso {
        public String ref_id;
        public String text_line;

        @Override
        public String toString() {
            return (ref_id == null) ? text_line : String.format("%s: %s", ref_id, text_line);
        }
    }

}