diff --git a/pom.xml b/pom.xml
index ad0aa93bab..3aeff21c45 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
io.goobi.workflow
workflow-base
- 24.06
+ 24.07
workflow-core
@@ -15,6 +15,13 @@
https://nexus.intranda.com/repository/maven-public
+
+
+ io.goobi.vocabulary
+ vocabulary-server-exchange
+ 1.0.0
+
+
@@ -79,8 +86,7 @@
${project.build.directory}/classes/template.properties
- ${project.build.directory}/classes/goobi_config.properties
-
+ ${project.build.directory}/classes/goobi_config.properties
@@ -100,4 +106,3 @@
-
diff --git a/src/main/java/de/sub/goobi/config/ConfigurationHelper.java b/src/main/java/de/sub/goobi/config/ConfigurationHelper.java
index 344b6b8cde..4c80de923a 100644
--- a/src/main/java/de/sub/goobi/config/ConfigurationHelper.java
+++ b/src/main/java/de/sub/goobi/config/ConfigurationHelper.java
@@ -1111,6 +1111,10 @@ public int getNumberOfMetaBackups() {
return getLocalInt("numberOfMetaBackups", 9);
}
+ public int getNumberOfBackups() {
+ return getLocalInt("numberOfBackups", 9);
+ }
+
/*
* subcategory in goobi_config.properties/METS EDITOR: user interface
*/
@@ -1291,6 +1295,14 @@ public boolean isPdfAsDownload() {
return getLocalBoolean("pdfAsDownload", true);
}
+ public String getVocabularyServerHost() {
+ return getLocalString("vocabularyServerHost", "localhost");
+ }
+
+ public int getVocabularyServerPort() {
+ return getLocalInt("vocabularyServerPort", 8081);
+ }
+
/**
* This setter is only used by unit tests and makes manipulation of the configuration possible.
*
diff --git a/src/main/java/de/sub/goobi/export/download/ExportMets.java b/src/main/java/de/sub/goobi/export/download/ExportMets.java
index 8e85b073e9..7fe29fb0e8 100755
--- a/src/main/java/de/sub/goobi/export/download/ExportMets.java
+++ b/src/main/java/de/sub/goobi/export/download/ExportMets.java
@@ -123,6 +123,7 @@
import ugh.dl.ExportFileformat;
import ugh.dl.Fileformat;
import ugh.dl.Md;
+import ugh.dl.Md.MdType;
import ugh.dl.MetadataType;
import ugh.dl.Prefs;
import ugh.dl.VirtualFileGroup;
@@ -407,8 +408,7 @@ private ExportFileformat collectMetadataToSave(Process myProzess, Fileformat gdz
}
Element techMd = createTechMd(path);
if (techMd != null) {
- Md md = new Md(techMd);
- md.setType("techMD");
+ Md md = new Md(techMd, MdType.TECH_MD);
md.setId(String.format("AMD_%04d", counter++));
dd.addTechMd(md);
page.setAdmId(md.getId());
diff --git a/src/main/java/de/sub/goobi/forms/AdditionalField.java b/src/main/java/de/sub/goobi/forms/AdditionalField.java
index 4fab656f2f..38671c9b83 100644
--- a/src/main/java/de/sub/goobi/forms/AdditionalField.java
+++ b/src/main/java/de/sub/goobi/forms/AdditionalField.java
@@ -99,7 +99,8 @@ public void setInitEnd(String newValue) {
}
public void setWert(String newValue) {
- if (newValue == null || newValue.equals(this.initStart)) {
+ // TODO: Testing for "null" is not nice, but JSF seems to store empty selection of dropdowns as "null" instead of null
+ if (newValue == null || "null".equals(newValue) || newValue.equals(this.initStart)) {
newValue = "";
}
if (newValue.startsWith(this.initStart)) {
@@ -204,6 +205,10 @@ public LocalDate getValueAsDate() {
}
public void setValueAsDate(LocalDate date) {
+ if (date == null) {
+ return;
+ }
+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
wert = formatter.format(date);
}
diff --git a/src/main/java/de/sub/goobi/forms/ProzesskopieForm.java b/src/main/java/de/sub/goobi/forms/ProzesskopieForm.java
index 51de8bc5cd..b3ed427e65 100644
--- a/src/main/java/de/sub/goobi/forms/ProzesskopieForm.java
+++ b/src/main/java/de/sub/goobi/forms/ProzesskopieForm.java
@@ -2,20 +2,20 @@
/**
* This file is part of the Goobi Application - a Workflow tool for the support of mass digitization.
- *
+ *
* Visit the websites for more information.
- * - https://goobi.io
- * - https://www.intranda.com
- *
+ * - https://goobi.io
+ * - https://www.intranda.com
+ *
* 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 2 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, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
- *
+ *
* Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions
* of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to
* link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and
@@ -44,6 +44,7 @@
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
import javax.enterprise.inject.Default;
import javax.faces.model.SelectItem;
@@ -74,11 +75,9 @@
import org.goobi.production.flow.jobs.HistoryAnalyserJob;
import org.goobi.production.plugin.interfaces.IOpacPlugin;
import org.goobi.production.plugin.interfaces.IOpacPluginVersion2;
+import org.goobi.production.properties.AccessCondition;
import org.goobi.production.properties.ProcessProperty;
import org.goobi.production.properties.PropertyParser;
-import org.goobi.vocabulary.Field;
-import org.goobi.vocabulary.VocabRecord;
-import org.goobi.vocabulary.Vocabulary;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
@@ -108,10 +107,12 @@
import de.sub.goobi.persistence.managers.ProjectManager;
import de.sub.goobi.persistence.managers.RulesetManager;
import de.sub.goobi.persistence.managers.StepManager;
-import de.sub.goobi.persistence.managers.VocabularyManager;
import de.unigoettingen.sub.search.opac.ConfigOpac;
import de.unigoettingen.sub.search.opac.ConfigOpacCatalogue;
import de.unigoettingen.sub.search.opac.ConfigOpacDoctype;
+import io.goobi.vocabulary.exchange.Vocabulary;
+import io.goobi.workflow.api.vocabulary.VocabularyAPIManager;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabularyRecord;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -141,7 +142,7 @@
@Log4j2
public class ProzesskopieForm implements Serializable {
/**
- *
+ *
*/
private static final long serialVersionUID = 2579641488883675182L;
private Helper help = new Helper();
@@ -475,23 +476,21 @@ private AdditionalField readAdditionalFieldConfiguration(HierarchicalConfigurati
String vocabularyTitle = item.getString("@vocabulary");
if (StringUtils.isNotBlank(vocabularyTitle)) {
- Vocabulary currentVocabulary = VocabularyManager.getVocabularyByTitle(vocabularyTitle);
- if (currentVocabulary != null && currentVocabulary.getId() != null) {
- VocabularyManager.getAllRecords(currentVocabulary);
- List recordList = currentVocabulary.getRecords();
- Collections.sort(recordList);
- List selectItems = new ArrayList<>(recordList.size());
- for (VocabRecord vr : recordList) {
- for (Field f : vr.getFields()) {
- if (f.getDefinition().isMainEntry()) {
- selectItems.add(new SelectItem(f.getValue(), f.getValue()));
- break;
- }
- }
- }
- fa.setSelectList(selectItems);
- }
- }
+ Vocabulary vocabulary = VocabularyAPIManager.getInstance().vocabularies().findByName(vocabularyTitle);
+ List records = VocabularyAPIManager.getInstance()
+ .vocabularyRecords()
+ .list(vocabulary.getId())
+ .all()
+ .request()
+ .getContent();
+ fa.setSelectList(
+ records.stream()
+ .map(ExtendedVocabularyRecord::getMainValue)
+ .sorted()
+ .map(v -> new SelectItem(v, v))
+ .collect(Collectors.toList()));
+ }
+ // TODO: FIX
return fa;
}
@@ -567,7 +566,7 @@ public String opacAuswerten() {
/**
* die Eingabefelder für die Eigenschaften mit Inhalten aus der RDF-Datei füllen
- *
+ *
* @throws PreferencesException
*/
private void fillFieldsFromMetadataFile() throws PreferencesException {
@@ -666,7 +665,7 @@ private void clearValues() {
/**
* Auswahl des Processes auswerten
- *
+ *
* @throws DAOException
* @throws NamingException
* @throws SQLException ============================================================== ==
@@ -691,27 +690,34 @@ public String readMetadataFromTemplate() throws DAOException {
if (tempProcess.getEigenschaftenSize() > 0) {
fillTemplateFromProperties(tempProcess);
}
+
try {
this.myRdf = tempProcess.readMetadataAsTemplateFile();
+
+ /* falls ein erstes Kind vorhanden ist, sind die Collectionen dafür */
+ try {
+ DocStruct colStruct = this.myRdf.getDigitalDocument().getLogicalDocStruct();
+
+ List firstChildMetadata =
+ colStruct.getAllChildren().isEmpty() ? Collections.emptyList() : colStruct.getAllChildren().get(0).getAllMetadata();
+ fillTemplateFromMetadata(colStruct.getAllMetadata(), firstChildMetadata);
+
+ removeCollections(colStruct);
+ colStruct = colStruct.getAllChildren().get(0);
+ removeCollections(colStruct);
+ } catch (PreferencesException e) {
+ Helper.setFehlerMeldung("Error on creating process", e);
+ log.error("Error on creating process", e);
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ /*
+ * das Firstchild unterhalb des Topstructs konnte nicht ermittelt werden
+ */
+ }
} catch (Exception e) {
Helper.setFehlerMeldung("Error on reading template-metadata ", e);
}
- /* falls ein erstes Kind vorhanden ist, sind die Collectionen dafür */
- try {
- DocStruct colStruct = this.myRdf.getDigitalDocument().getLogicalDocStruct();
- removeCollections(colStruct);
- colStruct = colStruct.getAllChildren().get(0);
- removeCollections(colStruct);
- } catch (PreferencesException e) {
- Helper.setFehlerMeldung("Error on creating process", e);
- log.error("Error on creating process", e);
- } catch (RuntimeException e) {
- /*
- * das Firstchild unterhalb des Topstructs konnte nicht ermittelt werden
- */
- }
-
return "";
}
@@ -719,14 +725,7 @@ private void fillTemplateFromProperties(Process tempProcess) {
for (Processproperty pe : tempProcess.getEigenschaften()) {
for (AdditionalField field : this.additionalFields) {
if (field.getTitel().equals(pe.getTitel())) {
- // multiselect
- if (field.isMultiselect()) {
- List existingValues = field.getValues();
- existingValues.add(pe.getWert());
- field.setValues(existingValues);
- } else {
- field.setWert(pe.getWert());
- }
+ setFieldValue(field, pe.getWert());
}
}
if ("digitalCollection".equals(pe.getTitel())) {
@@ -741,14 +740,7 @@ private void fillTemplateFromTemplate(Process tempProcess) {
for (Templateproperty eig : vor.getEigenschaften()) {
for (AdditionalField field : this.additionalFields) {
if (field.getTitel().equals(eig.getTitel())) {
- // multiselect
- if (field.isMultiselect()) {
- List existingValues = field.getValues();
- existingValues.add(eig.getWert());
- field.setValues(existingValues);
- } else {
- field.setWert(eig.getWert());
- }
+ setFieldValue(field, eig.getWert());
}
}
}
@@ -759,14 +751,7 @@ private void fillTemplateFromMasterpiece(Process tempProcess) {
for (Masterpieceproperty eig : werk.getEigenschaften()) {
for (AdditionalField field : this.additionalFields) {
if (field.getTitel().equals(eig.getTitel())) {
- // multiselect
- if (field.isMultiselect()) {
- List existingValues = new ArrayList<>(field.getValues());
- existingValues.add(eig.getWert());
- field.setValues(existingValues);
- } else {
- field.setWert(eig.getWert());
- }
+ setFieldValue(field, eig.getWert());
}
if ("DocType".equals(eig.getTitel())) {
docType = eig.getWert();
@@ -775,9 +760,37 @@ private void fillTemplateFromMasterpiece(Process tempProcess) {
}
}
+ private void fillTemplateFromMetadata(List topstruct, List firstChild) {
+ for (Metadata m : topstruct) {
+ this.additionalFields.stream()
+ .filter(f -> "topstruct".equals(f.getDocstruct()))
+ .filter(f -> f.getMetadata() != null && f.getMetadata().equals(m.getType().getName()))
+ .findFirst()
+ .ifPresent(f -> setFieldValue(f, m.getValue()));
+ }
+
+ for (Metadata m : firstChild) {
+ this.additionalFields.stream()
+ .filter(f -> "firstchild".equals(f.getDocstruct()))
+ .filter(f -> f.getMetadata() != null && f.getMetadata().equals(m.getType().getName()))
+ .findFirst()
+ .ifPresent(f -> setFieldValue(f, m.getValue()));
+ }
+ }
+
+ private void setFieldValue(AdditionalField field, String value) {
+ if (field.isMultiselect()) {
+ List existingValues = field.getValues();
+ existingValues.add(value);
+ field.setValues(existingValues);
+ } else {
+ field.setWert(value);
+ }
+ }
+
/**
* Validierung der Eingaben
- *
+ *
* @return sind Fehler bei den Eingaben vorhanden? ================================================================
*/
private boolean isContentValid() {
@@ -822,14 +835,26 @@ private boolean isContentValid() {
* -------------------------------- Prüfung der additional-Eingaben, die angegeben werden müssen --------------------------------
*/
for (AdditionalField field : this.additionalFields) {
- if ((field.getWert() == null || "".equals(field.getWert())) && field.isRequired() && field.getShowDependingOnDoctype(getDocType())
- && (StringUtils.isBlank(field.getWert()))) {
+ if ((field.getWert() == null || field.getWert().isBlank()) && field.isRequired() && field.getShowDependingOnDoctype(getDocType())) {
valide = false;
Helper.setFehlerMeldung(Helper.getTranslation("UnvollstaendigeDaten") + " " + field.getTitel() + " "
+ Helper.getTranslation("ProcessCreationErrorFieldIsEmpty"));
}
}
+
+ // property validation
+
+ for (ProcessProperty pt : configuredProperties) {
+ if (!pt.isValid()
+ || (AccessCondition.WRITEREQUIRED.equals(pt.getShowProcessGroupAccessCondition())
+ && StringUtils.isBlank(pt.getValue()))) {
+ Helper.setFehlerMeldung(Helper.getTranslation("UnvollstaendigeDaten") + " " + pt.getName() + " "
+ + Helper.getTranslation("ProcessCreationErrorFieldIsEmpty"));
+ }
+
+ }
+
return valide;
}
@@ -841,7 +866,7 @@ public void showTablesOfExistingProcess() {
/**
* print the infos of the existing process
- *
+ *
* @param processName title that has already been used by some process
*/
@SuppressWarnings("unused")
@@ -912,7 +937,7 @@ public String openPage2() {
/**
* Anlegen des Processes und Speichern der Metadaten ================================================================
- *
+ *
* @throws DAOException
* @throws SwapException
* @throws WriteException
@@ -1452,7 +1477,7 @@ private void copyMetadata(DocStruct oldDocStruct, DocStruct newDocStruct) {
/*
* this is needed for GUI, render multiple select only if this is false if this is true use the only choice
- *
+ *
* @author Wulf
*/
public boolean isSingleChoiceCollection() {
@@ -1462,7 +1487,7 @@ public boolean isSingleChoiceCollection() {
/*
* this is needed for GUI, render multiple select only if this is false if isSingleChoiceCollection is true use this choice
- *
+ *
* @author Wulf
*/
public String getDigitalCollectionIfSingleChoice() {
@@ -1695,8 +1720,17 @@ public void calculateProcessTitle() {
}
/* den Inhalt zum Titel hinzufügen */
- if (myField.getTitel().equals(myString) && myField.getShowDependingOnDoctype(getDocType()) && myField.getWert() != null) {
- gen.addToken(calcProcesstitelCheck(myField.getTitel(), myField.getWert()), ManipulationType.NORMAL);
+ if (myField.getTitel().equals(myString) && myField.getShowDependingOnDoctype(getDocType())) {
+ String value = myField.getWert();
+ if (value == null) {
+ value = "";
+ }
+
+ // Skip process title generation if a required field is not present
+ if (myField.isRequired() && value.isBlank()) {
+ return;
+ }
+ gen.addToken(calcProcesstitelCheck(myField.getTitel(), value), ManipulationType.NORMAL);
}
}
}
@@ -1976,7 +2010,7 @@ private int getIndexOfFolder(String folder) {
/**
* Get get temporary folder to upload to
- *
+ *
* @return path to temporary folder
*/
private Path getTemporaryFolder() {
@@ -1992,7 +2026,7 @@ private Path getTemporaryFolder() {
/**
* Handle the upload of a file
- *
+ *
* @param event
*/
public void uploadFile(FileUploadEvent event) {
@@ -2006,7 +2040,7 @@ public void uploadFile(FileUploadEvent event) {
/**
* Save the uploaded file temporary in the tmp-folder inside of goobi in a subfolder for the user
- *
+ *
* @param fileName
* @param in
* @throws IOException
diff --git a/src/main/java/de/sub/goobi/helper/DateTimeHelper.java b/src/main/java/de/sub/goobi/helper/DateTimeHelper.java
new file mode 100644
index 0000000000..16c540101d
--- /dev/null
+++ b/src/main/java/de/sub/goobi/helper/DateTimeHelper.java
@@ -0,0 +1,14 @@
+package de.sub.goobi.helper;
+
+import java.time.LocalDateTime;
+
+/**
+ * Due to PowerMock mocking issues of LocalDateTime.now(), this class is used to retrieve local dates and times and mocked in testing.
+ *
+ * Feel free to extend this class with any DateTime related methods you might find useful.
+ */
+public class DateTimeHelper {
+ public static LocalDateTime localDateTimeNow() {
+ return LocalDateTime.now();
+ }
+}
diff --git a/src/main/java/de/sub/goobi/helper/VariableReplacer.java b/src/main/java/de/sub/goobi/helper/VariableReplacer.java
index 43cfe40cc1..7c53627138 100644
--- a/src/main/java/de/sub/goobi/helper/VariableReplacer.java
+++ b/src/main/java/de/sub/goobi/helper/VariableReplacer.java
@@ -34,6 +34,8 @@
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.MatchResult;
@@ -144,6 +146,7 @@ private enum MetadataLevel {
private static final String REGEX_TEMPLATE = PREFIX + "template\\.([^)}]+?)" + SUFFIX;
private static final String REGEX_PROCESS = PREFIX + "process\\.([^)}]+?)" + SUFFIX;
private static final String REGEX_DB_META = PREFIX + "db_meta\\.([^)}]+?)" + SUFFIX;
+ private static final String REGEX_DATETIME = PREFIX + "datetime\\.([^)}]+?)" + SUFFIX;
@Getter
@Setter
@@ -511,6 +514,17 @@ public String replace(String inString) {
}
}
+ for (MatchResult r : findRegexMatches(REGEX_DATETIME, inString)) {
+ try {
+ LocalDateTime now = DateTimeHelper.localDateTimeNow();
+ String pattern = r.group(1);
+ DateTimeFormatter format = DateTimeFormatter.ofPattern(pattern);
+ inString = inString.replace(r.group(), now.format(format));
+ } catch (IllegalArgumentException e) {
+ log.error(e);
+ }
+ }
+
return inString;
}
diff --git a/src/main/java/de/sub/goobi/metadaten/MetaCorporate.java b/src/main/java/de/sub/goobi/metadaten/MetaCorporate.java
index b093dbd25b..001d6daa15 100644
--- a/src/main/java/de/sub/goobi/metadaten/MetaCorporate.java
+++ b/src/main/java/de/sub/goobi/metadaten/MetaCorporate.java
@@ -36,8 +36,6 @@
import org.goobi.api.display.enums.DisplayType;
import org.goobi.api.display.helper.NormDatabase;
import org.goobi.beans.Process;
-import org.goobi.production.cli.helper.StringPair;
-import org.goobi.vocabulary.VocabRecord;
import de.intranda.digiverso.normdataimporter.NormDataImporter;
import de.intranda.digiverso.normdataimporter.model.NormData;
@@ -46,6 +44,7 @@
import de.sub.goobi.config.ConfigurationHelper;
import de.sub.goobi.metadaten.search.EasyDBSearch;
import de.sub.goobi.metadaten.search.ViafSearch;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabularyRecord;
import lombok.Data;
import ugh.dl.Corporate;
import ugh.dl.HoldingElement;
@@ -81,11 +80,13 @@ public class MetaCorporate implements SearchableMetadata {
private List normdataList;
private int totalResults;
private EasyDBSearch easydbSearch = new EasyDBSearch();
- private List vocabularySearchFields;
+ private List vocabularySearchFields;
private String vocabularyName;
- private List records;
+ private List records;
private String vocabularyUrl;
- private VocabRecord selectedVocabularyRecord;
+ private ExtendedVocabularyRecord selectedVocabularyRecord;
+ private long currentVocabularySearchField;
+ private String vocabularySearchQuery;
/**
* constructor
@@ -184,13 +185,13 @@ public String getData() {
String x156 = "\\x156";
for (NormData normdata : currentData) {
- if (normdata.getKey().equals("NORM_IDENTIFIER")) {
+ if ("NORM_IDENTIFIER".equals(normdata.getKey())) {
corporate.setAutorityFile("gnd", "http://d-nb.info/gnd/", normdata.getValues().get(0).getText());
- } else if (normdata.getKey().equals("NORM_ORGANIZATION")) {
+ } else if ("NORM_ORGANIZATION".equals(normdata.getKey())) {
mainValue = normdata.getValues().get(0).getText().replace(x152, "").replace(x156, "");
- } else if (normdata.getKey().equals("NORM_SUB_ORGANIZATION")) {
+ } else if ("NORM_SUB_ORGANIZATION".equals(normdata.getKey())) {
subNames.add(new NamePart(SUBNAME, normdata.getValues().get(0).getText().replace(x152, "").replace(x156, "")));
- } else if (normdata.getKey().equals("NORM_PART_ORGANIZATION")) {
+ } else if ("NORM_PART_ORGANIZATION".equals(normdata.getKey())) {
if (partName.length() > 0) {
partName.append("; ");
}
diff --git a/src/main/java/de/sub/goobi/metadaten/MetaPerson.java b/src/main/java/de/sub/goobi/metadaten/MetaPerson.java
index 4f4a3fcfaf..8b47f36487 100644
--- a/src/main/java/de/sub/goobi/metadaten/MetaPerson.java
+++ b/src/main/java/de/sub/goobi/metadaten/MetaPerson.java
@@ -39,8 +39,6 @@
import org.goobi.api.display.enums.DisplayType;
import org.goobi.api.display.helper.NormDatabase;
import org.goobi.beans.Process;
-import org.goobi.production.cli.helper.StringPair;
-import org.goobi.vocabulary.VocabRecord;
import de.intranda.digiverso.normdataimporter.NormDataImporter;
import de.intranda.digiverso.normdataimporter.model.NormData;
@@ -50,6 +48,7 @@
import de.sub.goobi.metadaten.search.EasyDBSearch;
import de.sub.goobi.metadaten.search.KulturNavImporter;
import de.sub.goobi.metadaten.search.ViafSearch;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabularyRecord;
import lombok.Data;
import ugh.dl.DocStruct;
import ugh.dl.HoldingElement;
@@ -95,11 +94,13 @@ public class MetaPerson implements SearchableMetadata {
private List normdataList;
private int totalResults;
private EasyDBSearch easydbSearch = new EasyDBSearch();
- private List vocabularySearchFields;
+ private List vocabularySearchFields;
private String vocabularyName;
- private List records;
+ private List records;
private String vocabularyUrl;
- private VocabRecord selectedVocabularyRecord;
+ private ExtendedVocabularyRecord selectedVocabularyRecord;
+ private long currentVocabularySearchField;
+ private String vocabularySearchQuery;
/**
* Allgemeiner Konstruktor ()
diff --git a/src/main/java/de/sub/goobi/metadaten/Metadaten.java b/src/main/java/de/sub/goobi/metadaten/Metadaten.java
index f201577f0e..4a3f24ba31 100644
--- a/src/main/java/de/sub/goobi/metadaten/Metadaten.java
+++ b/src/main/java/de/sub/goobi/metadaten/Metadaten.java
@@ -5207,18 +5207,25 @@ public String getAuthorityMetadataJSON() {
List authorityList = new ArrayList<>();
for (Person person : persons) {
- AuthorityData data = new AuthorityData(person.getDisplayname(), person.getAuthorityURI() + person.getAuthorityValue());
+ AuthorityData data =
+ new AuthorityData(person.getDisplayname(),
+ URI.create(Optional.ofNullable(person.getAuthorityURI()).orElse("")).resolve(person.getAuthorityValue()).toString());
authorityList.add(data);
}
for (Corporate corporate : corporates) {
- AuthorityData data = new AuthorityData(corporate.getMainName(), corporate.getAuthorityURI() + corporate.getAuthorityValue());
+ AuthorityData data = new AuthorityData(corporate.getMainName(),
+ URI.create(Optional.ofNullable(corporate.getAuthorityURI()).orElse("")).resolve(corporate.getAuthorityValue()).toString());
authorityList.add(data);
}
for (Metadata md : metadata) {
- AuthorityData data = new AuthorityData(md.getValue(), md.getAuthorityURI() + md.getAuthorityValue());
+ AuthorityData data = new AuthorityData(md.getValue(),
+ URI.create(Optional.ofNullable(md.getAuthorityURI()).orElse("")).resolve(md.getAuthorityValue()).toString());
authorityList.add(data);
}
+ Collections.sort(authorityList,
+ (a, b) -> Optional.ofNullable(a.getValue()).orElse("").compareTo(Optional.ofNullable(b.getValue()).orElse("")));
+
return new Gson().toJson(authorityList);
}
}
diff --git a/src/main/java/de/sub/goobi/metadaten/MetadatenVerifizierung.java b/src/main/java/de/sub/goobi/metadaten/MetadatenVerifizierung.java
index ee8c21e8af..390c36dec5 100644
--- a/src/main/java/de/sub/goobi/metadaten/MetadatenVerifizierung.java
+++ b/src/main/java/de/sub/goobi/metadaten/MetadatenVerifizierung.java
@@ -32,15 +32,15 @@
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
+import io.goobi.workflow.api.vocabulary.VocabularyAPIManager;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabulary;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabularyRecord;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.goobi.api.display.Item;
import org.goobi.api.display.enums.DisplayType;
import org.goobi.api.display.helper.ConfigDisplayRules;
import org.goobi.beans.Process;
-import org.goobi.vocabulary.Field;
-import org.goobi.vocabulary.VocabRecord;
-import org.goobi.vocabulary.Vocabulary;
import de.sub.goobi.config.ConfigProjects;
import de.sub.goobi.config.ConfigurationHelper;
@@ -49,7 +49,6 @@
import de.sub.goobi.helper.exceptions.InvalidImagesException;
import de.sub.goobi.helper.exceptions.SwapException;
import de.sub.goobi.helper.exceptions.UghHelperException;
-import de.sub.goobi.persistence.managers.VocabularyManager;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import ugh.dl.Corporate;
@@ -492,15 +491,16 @@ private List checkSelectFromVocabularyList(Process inProcess, DocStruct
if (allowedItems.get(0) == null) {
break;
}
- Vocabulary vocabulary = VocabularyManager.getVocabularyByTitle(allowedItems.get(0).getSource());
- VocabularyManager.getAllRecords(vocabulary);
- List records = vocabulary.getRecords();
- List allowedValues = new ArrayList<>();
- for (VocabRecord vocabularyRecord : records) {
- for (Field field : vocabularyRecord.getFields()) {
- allowedValues.add(field.getValue());
- }
- }
+ ExtendedVocabulary vocabulary = VocabularyAPIManager.getInstance().vocabularies().findByName(allowedItems.get(0).getSource());
+ List records = VocabularyAPIManager.getInstance().vocabularyRecords()
+ .list(vocabulary.getId())
+ .all()
+ .request()
+ .getContent();
+ List allowedValues = records.stream()
+ .map(ExtendedVocabularyRecord::getMainValue)
+ .collect(Collectors.toList());
+
List extends Metadata> ll = null;
ll = inStruct.getAllMetadataByType(mdt);
for (Metadata md : ll) {
diff --git a/src/main/java/de/sub/goobi/metadaten/MetadatumImpl.java b/src/main/java/de/sub/goobi/metadaten/MetadatumImpl.java
index 8f832b5897..f3f1e67ec7 100644
--- a/src/main/java/de/sub/goobi/metadaten/MetadatumImpl.java
+++ b/src/main/java/de/sub/goobi/metadaten/MetadatumImpl.java
@@ -1,23 +1,25 @@
package de.sub.goobi.metadaten;
-import java.net.URL;
-import java.nio.file.Paths;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import javax.faces.context.FacesContext;
-import javax.faces.model.SelectItem;
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.core.UriBuilder;
-
+import de.intranda.digiverso.normdataimporter.NormDataImporter;
+import de.intranda.digiverso.normdataimporter.dante.DanteImport;
+import de.intranda.digiverso.normdataimporter.model.NormData;
+import de.intranda.digiverso.normdataimporter.model.NormDataRecord;
+import de.sub.goobi.config.ConfigPlugins;
+import de.sub.goobi.config.ConfigurationHelper;
+import de.sub.goobi.helper.Helper;
+import de.sub.goobi.helper.StorageProvider;
+import de.sub.goobi.metadaten.search.EasyDBSearch;
+import de.sub.goobi.metadaten.search.KulturNavImporter;
+import de.sub.goobi.metadaten.search.ViafSearch;
+import de.sub.goobi.persistence.managers.MetadataManager;
+import io.goobi.vocabulary.exchange.FieldDefinition;
+import io.goobi.vocabulary.exchange.Vocabulary;
+import io.goobi.vocabulary.exchange.VocabularySchema;
+import io.goobi.workflow.api.vocabulary.APIException;
+import io.goobi.workflow.api.vocabulary.VocabularyAPIManager;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabularyRecord;
+import lombok.Data;
+import lombok.extern.log4j.Log4j2;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.configuration.XMLConfiguration;
@@ -37,32 +39,11 @@
import org.goobi.api.rest.request.SearchRequest;
import org.goobi.beans.Process;
import org.goobi.beans.Project;
-import org.goobi.production.cli.helper.StringPair;
-import org.goobi.vocabulary.Field;
-import org.goobi.vocabulary.VocabRecord;
-import org.goobi.vocabulary.Vocabulary;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.filter.Filters;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
-
-import de.intranda.digiverso.normdataimporter.NormDataImporter;
-import de.intranda.digiverso.normdataimporter.dante.DanteImport;
-import de.intranda.digiverso.normdataimporter.model.NormData;
-import de.intranda.digiverso.normdataimporter.model.NormDataRecord;
-import de.sub.goobi.config.ConfigPlugins;
-import de.sub.goobi.config.ConfigurationHelper;
-import de.sub.goobi.helper.FacesContextHelper;
-import de.sub.goobi.helper.Helper;
-import de.sub.goobi.helper.StorageProvider;
-import de.sub.goobi.metadaten.search.EasyDBSearch;
-import de.sub.goobi.metadaten.search.KulturNavImporter;
-import de.sub.goobi.metadaten.search.ViafSearch;
-import de.sub.goobi.persistence.managers.MetadataManager;
-import de.sub.goobi.persistence.managers.VocabularyManager;
-import lombok.Data;
-import lombok.extern.log4j.Log4j2;
import ugh.dl.DocStruct;
import ugh.dl.Metadata;
import ugh.dl.MetadataGroup;
@@ -72,6 +53,22 @@
import ugh.exceptions.MetadataTypeNotAllowedException;
import ugh.fileformats.mets.ModsHelper;
+import javax.faces.model.SelectItem;
+import java.net.URL;
+import java.nio.file.Paths;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
/**
* This file is part of the Goobi Application - a Workflow tool for the support of mass digitization.
*
@@ -134,13 +131,11 @@ public class MetadatumImpl implements Metadatum, SearchableMetadata {
/**
* The table of available normdata objects
- *
*/
private List> dataList;
/**
* The list of current normdata objects
- *
*/
private List currentData;
@@ -174,11 +169,14 @@ public class MetadatumImpl implements Metadatum, SearchableMetadata {
private EasyDBSearch easydbSearch = new EasyDBSearch();
// search in vocabulary
- private List vocabularySearchFields;
+ private VocabularyAPIManager vocabularyAPI = VocabularyAPIManager.getInstance();
+ private List vocabularySearchFields;
+ private long currentVocabularySearchField;
+ private String vocabularySearchQuery;
private String vocabularyName;
- private List records;
- private String vocabularyUrl;
- private VocabRecord selectedVocabularyRecord;
+ private List records;
+ private List definitions;
+ private ExtendedVocabularyRecord selectedVocabularyRecord;
private boolean validationErrorPresent;
private String validationMessage;
@@ -202,30 +200,17 @@ public MetadatumImpl(Metadata m, int inID, Prefs inPrefs, Process inProcess, Met
}
public void searchVocabulary() {
-
- FacesContext context = FacesContextHelper.getCurrentFacesContext();
- HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
- String contextPath = request.getContextPath();
- String scheme = request.getScheme(); // http
- String serverName = request.getServerName(); // hostname.com
- int serverPort = request.getServerPort(); // 80
- String reqUrl = scheme + "://" + serverName + contextPath;
- // if there is a port lower than the typical ones (443) don't show it in the url (http://mygoobi.io/xyz instead of http://mygoobi.io:80/xyz)
- if (serverPort > 443) {
- reqUrl = scheme + "://" + serverName + ":" + serverPort + contextPath;
- }
- UriBuilder ub = UriBuilder.fromUri(reqUrl);
- vocabularyUrl = ub.path("api").path("vocabulary").path("records").build().toString();
-
- records = VocabularyManager.findRecords(vocabulary, vocabularySearchFields);
-
- if (records == null || records.isEmpty()) {
- showNotHits = true;
- } else {
- showNotHits = false;
- }
- Collections.sort(records);
-
+ Vocabulary vocabulary = vocabularyAPI.vocabularies().findByName(this.vocabulary);
+ VocabularySchema schema = vocabularyAPI.vocabularySchemas().get(vocabulary.getSchemaId());
+ definitions = schema.getDefinitions().stream()
+ .filter(d -> Boolean.TRUE.equals(d.getTitleField()))
+ .sorted(Comparator.comparingLong(FieldDefinition::getId))
+ .collect(Collectors.toList());
+ records = vocabularyAPI.vocabularyRecords().list(vocabulary.getId())
+ .search(currentVocabularySearchField + ":" + vocabularySearchQuery)
+ .request()
+ .getContent();
+ showNotHits = records.isEmpty();
}
private void initializeValues() {
@@ -262,28 +247,111 @@ private void initializeValues() {
searchRequest.newGroup();
} else if (metadataDisplaytype == DisplayType.vocabularySearch) {
vocabularyName = myValues.getItemList().get(0).getSource();
- String fields = myValues.getItemList().get(0).getField();
+ Set fieldsNames = Stream.of(myValues.getItemList().get(0).getField().split(";"))
+ .map(String::trim)
+ .collect(Collectors.toSet());
- String[] fieldNames = fields.split(";");
- vocabularySearchFields = new ArrayList<>();
- for (String fieldname : fieldNames) {
- StringPair sp = new StringPair(fieldname.trim(), "");
- vocabularySearchFields.add(sp);
- }
+ Vocabulary currentVocabulary = vocabularyAPI.vocabularies().findByName(vocabularyName);
+ VocabularySchema vocabularySchema = vocabularyAPI.vocabularySchemas().get(currentVocabulary.getSchemaId());
+ vocabularySearchFields = vocabularySchema.getDefinitions().stream()
+ .filter(d -> fieldsNames.contains(d.getName()))
+ .map(d -> new SelectItem(d.getId(), d.getName()))
+ .collect(Collectors.toList());
} else if (metadataDisplaytype == DisplayType.vocabularyList) {
+ try {
+ String vocabularyTitle = myValues.getItemList().get(0).getSource();
+ String fields = myValues.getItemList().get(0).getField();
+ Vocabulary currentVocabulary = vocabularyAPI.vocabularies().findByName(vocabularyTitle);
+ VocabularySchema schema = vocabularyAPI.vocabularySchemas().get(currentVocabulary.getSchemaId());
+
+ if (Boolean.TRUE.equals(schema.getHierarchicalRecords())) {
+ Helper.setFehlerMeldung(Helper.getTranslation("mets_error_configuredVocabularyListHierarchical", md.getType().getName(), vocabularyTitle));
+ return;
+ }
+
+ if (StringUtils.isBlank(fields)) {
+ try {
+ List recordList = vocabularyAPI.vocabularyRecords()
+ .list(currentVocabulary.getId())
+ .all()
+ .request()
+ .getContent();
+ ArrayList- itemList = new ArrayList<>(recordList.size() + 1);
+ List
selectItems = new ArrayList<>(recordList.size() + 1);
+
+ String defaultLabel = myValues.getItemList().get(0).getLabel();
+ if (StringUtils.isNotBlank(defaultLabel)) {
+ List defaultitems = new ArrayList<>();
+ defaultitems.add(defaultLabel);
+ setDefaultItems(defaultitems);
+ }
+ itemList.add(new Item(Helper.getTranslation("bitteAuswaehlen"), "", false, "", ""));
+ selectItems.add(new SelectItem("", Helper.getTranslation("bitteAuswaehlen")));
+
+ for (ExtendedVocabularyRecord vr : recordList) {
+ selectItems.add(new SelectItem(vr.getMainValue(), vr.getMainValue()));
+ Item item = new Item(vr.getMainValue(), vr.getMainValue(), false, "", "");
+ if (StringUtils.isNotBlank(defaultLabel) && defaultLabel.equals(vr.getMainValue())) {
+ item.setSelected(true);
+ }
+ itemList.add(item);
+ }
+ setPossibleItems(selectItems);
+ myValues.setItemList(itemList);
+ } catch (APIException e) {
+ Helper.setFehlerMeldung(Helper.getTranslation("mets_error_configuredVocabularyInvalid", md.getType().getName(), vocabularyTitle));
+ metadataDisplaytype = DisplayType.input;
+ myValues.overwriteConfiguredElement(myProcess, md.getType());
+ }
+ } else {
+ if (fields.contains(";")) {
+ Helper.setFehlerMeldung("vocabularyList with multiple fields is not supported right now");
+ return;
+ }
+
+ String searchFilter = fields.trim();
+ Optional sorting = Optional.empty();
+ if (searchFilter.contains("@")) {
+ String[] parts = searchFilter.split("@");
+ searchFilter = parts[0];
+ sorting = Optional.of(parts[1]);
+ }
- String vocabularyTitle = myValues.getItemList().get(0).getSource();
+ String fieldName = searchFilter;
+ Optional fieldValueFilter = Optional.empty();
- String fields = myValues.getItemList().get(0).getField();
+ if (fieldName.contains("=")) {
+ String[] parts = searchFilter.split("=");
+ fieldName = parts[0];
+ fieldValueFilter = Optional.of(parts[1]);
+ }
- if (StringUtils.isBlank(fields)) {
- Vocabulary currentVocabulary = VocabularyManager.getVocabularyByTitle(vocabularyTitle);
- VocabularyManager.getAllRecords(currentVocabulary);
+ String finalFieldName = fieldName;
+ Optional searchField = schema.getDefinitions().stream()
+ .filter(d -> d.getName().equals(finalFieldName))
+ .findFirst();
+
+ if (searchField.isEmpty()) {
+ Helper.setFehlerMeldung("Field " + fieldName + " not found in vocabulary " + currentVocabulary.getName());
+ return;
+ }
+
+ Optional sortingQuery = Optional.empty();
+ if (sorting.isPresent()) {
+ sortingQuery = Optional.of(searchField.get().getId() + "," + sorting.get());
+ }
+ Optional searchQuery = Optional.empty();
+ if (fieldValueFilter.isPresent()) {
+ searchQuery = Optional.of(searchField.get().getId() + ":" + fieldValueFilter.get());
+ }
+ List recordList = vocabularyAPI.vocabularyRecords()
+ .list(currentVocabulary.getId())
+ .search(searchQuery)
+ .sorting(sortingQuery)
+ .request()
+ .getContent();
- if (currentVocabulary != null && currentVocabulary.getId() != null) {
- List recordList = currentVocabulary.getRecords();
- Collections.sort(recordList);
ArrayList- itemList = new ArrayList<>(recordList.size() + 1);
List
selectItems = new ArrayList<>(recordList.size() + 1);
@@ -296,60 +364,19 @@ private void initializeValues() {
itemList.add(new Item(Helper.getTranslation("bitteAuswaehlen"), "", false, "", ""));
selectItems.add(new SelectItem("", Helper.getTranslation("bitteAuswaehlen")));
- for (VocabRecord vr : recordList) {
- for (Field f : vr.getFields()) {
- if (f.getDefinition().isMainEntry()) {
- selectItems.add(new SelectItem(f.getValue(), f.getValue()));
- Item item = new Item(f.getValue(), f.getValue(), false, "", "");
- if (StringUtils.isNotBlank(defaultLabel) && defaultLabel.equals(f.getValue())) {
- item.setSelected(true);
- }
- itemList.add(item);
- break;
- }
+ for (ExtendedVocabularyRecord vr : recordList) {
+ selectItems.add(new SelectItem(vr.getMainValue(), vr.getMainValue()));
+ Item item = new Item(vr.getMainValue(), vr.getMainValue(), false, "", "");
+ if (StringUtils.isNotBlank(defaultLabel) && defaultLabel.equals(vr.getMainValue())) {
+ item.setSelected(true);
}
+ itemList.add(item);
}
setPossibleItems(selectItems);
myValues.setItemList(itemList);
- } else {
- Helper.setFehlerMeldung(Helper.getTranslation("mets_error_configuredVocabularyInvalid", md.getType().getName(), vocabularyTitle));
- metadataDisplaytype = DisplayType.input;
- myValues.overwriteConfiguredElement(myProcess, md.getType());
- }
- } else {
- String[] fieldNames = fields.split(";");
- vocabularySearchFields = new ArrayList<>();
- for (String fieldname : fieldNames) {
- String[] parts = fieldname.trim().split("=");
- if (parts.length > 1) {
- String name = parts[0];
- String value = parts[1];
- StringPair sp = new StringPair(name, value);
- vocabularySearchFields.add(sp);
- }
- }
- List recordList = VocabularyManager.findRecords(vocabularyTitle, vocabularySearchFields);
- Collections.sort(recordList);
-
- if (recordList != null && !recordList.isEmpty()) {
- ArrayList- itemList = new ArrayList<>(recordList.size());
- List
selectItems = new ArrayList<>(recordList.size());
- for (VocabRecord vr : recordList) {
- for (Field f : vr.getFields()) {
- if (f.getDefinition().isMainEntry()) {
- selectItems.add(new SelectItem(f.getValue(), f.getValue()));
- itemList.add(new Item(f.getValue(), f.getValue(), false, "", ""));
- break;
- }
- }
- }
- setPossibleItems(selectItems);
- myValues.setItemList(itemList);
- } else {
- Helper.setFehlerMeldung(Helper.getTranslation("mets_error_configuredVocabularyInvalid", md.getType().getName(), vocabularyTitle));
- metadataDisplaytype = DisplayType.input;
- myValues.overwriteConfiguredElement(myProcess, md.getType());
}
+ } catch (APIException e) {
+ Helper.setFehlerMeldung(e);
}
} else if (metadataDisplaytype == DisplayType.generate) {
for (Item item : myValues.getItemList()) {
@@ -504,7 +531,7 @@ public String getSelectedItem() {
String value = this.md.getValue();
if (value != null && value.length() != 0) {
for (Item i : this.myValues.getItemList()) {
- if (i.getValue().equals(value)) {
+ if (value.equals(i.getValue())) {
return i.getLabel();
}
}
@@ -770,20 +797,7 @@ public String getData() {
easydbSearch.getMetadata(md);
break;
case vocabularySearch:
- for (Field currentField : selectedVocabularyRecord.getFields()) {
- if (currentField.getDefinition().isMainEntry()) {
- md.setValue(currentField.getValue());
- }
- }
- String url = ConfigurationHelper.getInstance().getGoobiAuthorityServerUrl();
- String user = ConfigurationHelper.getInstance().getGoobiAuthorityServerUser();
- Integer vocabularyId = selectedVocabularyRecord.getVocabularyId();
- Integer recordId = selectedVocabularyRecord.getId();
- if (StringUtils.isNotBlank(user) && StringUtils.isNotBlank(url)) {
- md.setAutorityFile(vocabulary, url, url + user + "/vocabularies/" + vocabularyId + "/records/" + recordId);
- } else {
- md.setAutorityFile(vocabulary, vocabularyUrl, vocabularyUrl + "/jskos/" + vocabularyId + "/" + recordId);
- }
+ selectedVocabularyRecord.writeReferenceMetadata(md);
break;
default:
break;
diff --git a/src/main/java/de/sub/goobi/metadaten/SearchableMetadata.java b/src/main/java/de/sub/goobi/metadaten/SearchableMetadata.java
index dac8c8e89a..00a55cf108 100644
--- a/src/main/java/de/sub/goobi/metadaten/SearchableMetadata.java
+++ b/src/main/java/de/sub/goobi/metadaten/SearchableMetadata.java
@@ -110,4 +110,12 @@ default String filter(String str) {
}
return filtered.toString();
}
+
+ public long getCurrentVocabularySearchField();
+
+ public void setCurrentVocabularySearchField(long field);
+
+ public String getVocabularySearchQuery();
+
+ public void setVocabularySearchQuery(String query);
}
diff --git a/src/main/java/de/sub/goobi/persistence/managers/DatabaseVersion.java b/src/main/java/de/sub/goobi/persistence/managers/DatabaseVersion.java
index 4a0eb4cd90..9e2b69e54c 100644
--- a/src/main/java/de/sub/goobi/persistence/managers/DatabaseVersion.java
+++ b/src/main/java/de/sub/goobi/persistence/managers/DatabaseVersion.java
@@ -42,9 +42,6 @@
import org.goobi.beans.User;
import org.goobi.beans.Usergroup;
import org.goobi.production.enums.LogType;
-import org.goobi.vocabulary.Definition;
-import org.goobi.vocabulary.VocabRecord;
-import org.goobi.vocabulary.Vocabulary;
import com.google.gson.Gson;
@@ -934,48 +931,7 @@ private static void updateToVersion39() throws SQLException {
}
if (DatabaseVersion.checkIfTableExists("vocabularies")) {
- String allVocabularies = "SELECT * FROM vocabularies";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner runner = new QueryRunner();
- List vocabularyList = runner.query(connection, allVocabularies, VocabularyManager.resultSetToVocabularyListHandler);
- String insert = "INSERT INTO vocabulary_record (id, vocabulary_id) VALUES (?,?)";
- String insertField =
- "INSERT INTO vocabulary_record_data (record_id,vocabulary_id, definition_id, label, language, value) VALUES (?,?,?,?,?,?)";
-
- for (Vocabulary vocabulary : vocabularyList) {
- DatabaseVersion.runSql("INSERT INTO vocabulary(id, title, description) VALUES (" + vocabulary.getId() + ",'"
- + vocabulary.getTitle() + "', '" + vocabulary.getDescription() + "')");
- for (Definition def : vocabulary.getStruct()) {
- VocabularyManager.saveDefinition(vocabulary.getId(), def);
- }
- VocabularyManager.loadRecordsForVocabulary(vocabulary);
-
- for (VocabRecord rec : vocabulary.getRecords()) {
- runner.insert(connection, insert, MySQLHelper.resultSetToIntegerHandler, rec.getId(), vocabulary.getId());
-
- for (org.goobi.vocabulary.Field field : rec.getFields()) {
- int fieldId = runner.insert(connection, insertField, MySQLHelper.resultSetToIntegerHandler, rec.getId(),
- vocabulary.getId(), field.getDefinition().getId(), field.getLabel(), field.getLanguage(), field.getValue());
- field.setId(fieldId);
- }
- }
- }
- DatabaseVersion.runSql("drop table vocabularyRecords");
- DatabaseVersion.runSql("drop table vocabularies");
-
- } catch (SQLException e) {
- log.error(e);
- } finally {
- if (connection != null) {
- try {
- MySQLHelper.closeConnection(connection);
- } catch (SQLException exception) {
- log.warn(exception);
- }
- }
- }
+ log.error("The vocabulary migration logic for this version has been removed. Please try to manually re-create the vocabularies or contact the support.");
}
}
diff --git a/src/main/java/de/sub/goobi/persistence/managers/VocabularyManager.java b/src/main/java/de/sub/goobi/persistence/managers/VocabularyManager.java
deleted file mode 100644
index 8faada094c..0000000000
--- a/src/main/java/de/sub/goobi/persistence/managers/VocabularyManager.java
+++ /dev/null
@@ -1,669 +0,0 @@
-/**
- * This file is part of the Goobi Application - a Workflow tool for the support of mass digitization.
- *
- * Visit the websites for more information.
- * - https://goobi.io
- * - https://www.intranda.com
- * - https://github.com/intranda/goobi-workflow
- *
- * 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 2 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, write to the Free Software Foundation, Inc., 59
- * Temple Place, Suite 330, Boston, MA 02111-1307 USA
- *
- * Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions
- * of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to
- * link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and
- * distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and
- * conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this
- * library, you may extend this exception to your version of the library, but you are not obliged to do so. If you do not wish to do so, delete this
- * exception statement from your version.
- */
-package de.sub.goobi.persistence.managers;
-
-import java.io.Serializable;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.commons.dbutils.ResultSetHandler;
-import org.goobi.beans.DatabaseObject;
-import org.goobi.beans.Institution;
-import org.goobi.production.cli.helper.StringPair;
-import org.goobi.vocabulary.Definition;
-import org.goobi.vocabulary.Field;
-import org.goobi.vocabulary.VocabRecord;
-import org.goobi.vocabulary.Vocabulary;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
-import de.sub.goobi.helper.exceptions.DAOException;
-import lombok.extern.log4j.Log4j2;
-
-@Log4j2
-public class VocabularyManager implements IManager, Serializable {
-
- private static final long serialVersionUID = 3577063138324090483L;
-
- @SuppressWarnings("deprecation")
- private static JsonParser jsonParser = new JsonParser();
-
- private static Gson gson = new GsonBuilder().create();
-
- @Override
- public int getHitSize(String order, String filter, Institution institution) throws DAOException {
- try {
- return VocabularyMysqlHelper.getVocabularyCount(filter);
- } catch (SQLException e) {
- log.error(e);
- }
- return 0;
- }
-
- @Override
- public List extends DatabaseObject> getList(String order, String filter, Integer start, Integer count, Institution institution)
- throws DAOException {
- try {
- return VocabularyMysqlHelper.getVocabularies(order, filter, start, count);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- public int getHitSize(String order, String filter) throws DAOException {
- try {
- return VocabularyMysqlHelper.getVocabularyCount(filter);
- } catch (SQLException e) {
- log.error(e);
- }
- return 0;
- }
-
- public List extends DatabaseObject> getList(String order, String filter, Integer start, Integer count) throws DAOException {
- try {
- return VocabularyMysqlHelper.getVocabularies(order, filter, start, count);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- @Override
- public List getIdList(String order, String filter, Institution institution) {
- return null;
- }
-
- public static Vocabulary getVocabularyByTitle(String title) {
- try {
- return VocabularyMysqlHelper.getVocabularyByTitle(title);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- public static Vocabulary getVocabularyById(Integer id) {
- try {
- return VocabularyMysqlHelper.getVocabularyById(id);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- public static boolean isTitleUnique(Vocabulary vocabulary) {
- try {
- return VocabularyMysqlHelper.isTitleUnique(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- return false;
- }
-
- /**
- * @deprecated This handler is not used anymore
- */
- @Deprecated(since = "23.05", forRemoval = false)
- public static ResultSetHandler> resultSetToVocabularyListHandler = new ResultSetHandler>() {
-
- @Override
- public List handle(ResultSet rs) throws SQLException {
- List answer = new ArrayList<>();
- while (rs.next()) {
- Vocabulary vocabulary = convert(rs);
- answer.add(vocabulary);
- }
- return answer;
- }
- };
-
- /**
- * @deprecated This handler is not used anymore
- */
- @Deprecated(since = "23.05", forRemoval = false)
- public static ResultSetHandler resultSetToVocabularyHandler = new ResultSetHandler() {
-
- @Override
- public Vocabulary handle(ResultSet rs) throws SQLException {
- if (rs.next()) {
- return convert(rs);
- }
- return null;
- }
- };
-
- /**
- * @deprecated This method is not used anymore
- *
- * @param rs
- * @return
- */
- @Deprecated(since = "23.05", forRemoval = false)
- @SuppressWarnings("unchecked")
- private static Vocabulary convert(ResultSet rs) throws SQLException {
- int vocabId = rs.getInt("vocabId");
- String strVocabTitle = rs.getString("title");
- String strDescription = rs.getString("description");
- String jsonStruct = rs.getString("structure");
- ArrayList lstDefs = new ArrayList<>();
-
- JsonArray struct = jsonParser.parse(jsonStruct).getAsJsonArray();
-
- if (struct != null) {
- for (JsonElement jsonElt : struct) {
-
- JsonObject jsonObj = jsonElt.getAsJsonObject();
-
- String strLabel = jsonObj.get("label").getAsString();
- String strLanguage = jsonObj.get("language").getAsString();
- String strType = jsonObj.get("type").getAsString();
- String strValidation = jsonObj.get("validation").getAsString();
-
- boolean bRequired = jsonObj.get("required").getAsBoolean();
- boolean bMainEntry = jsonObj.get("mainEntry").getAsBoolean();
- boolean bUnique = jsonObj.get("unique").getAsBoolean();
-
- ArrayList selecteableValues = new ArrayList<>();
- JsonElement obj = jsonObj.get("selecteableValues");
- if (obj != null) {
- JsonArray array = obj.getAsJsonArray();
- if (array != null) {
- selecteableValues = gson.fromJson(array, ArrayList.class);
- }
- }
- if (strLabel != null) {
- Definition def = new Definition(strLabel, strLanguage, strType, strValidation, bRequired, bMainEntry, bUnique, bMainEntry);
- def.setSelecteableValues(selecteableValues);
- lstDefs.add(def);
-
- }
- }
- }
-
- Vocabulary vocab = new Vocabulary();
- vocab.setId(vocabId);
- vocab.setTitle(strVocabTitle);
- vocab.setDescription(strDescription);
- vocab.setStruct(lstDefs);
- vocab.setRecords(new ArrayList<>());
- return vocab;
- }
-
- public static ResultSetHandler> vocabularyRecordListHandler = new ResultSetHandler>() {
-
- @Override
- public List handle(ResultSet rs) throws SQLException {
-
- Map recordMap = new HashMap<>();
- List records = new LinkedList<>();
-
- while (rs.next()) {
- Integer fieldId = rs.getInt("id");
- Integer recordId = rs.getInt("record_id");
- Integer vocabularyId = rs.getInt("vocabulary_id");
- Integer definitionId = rs.getInt("definition_id");
- String label = rs.getString("label");
- String language = rs.getString("language");
- String value = rs.getString("value");
-
- Field field = new Field();
- field.setId(fieldId);
- field.setLabel(label);
- field.setLanguage(language);
- field.setValue(value);
- field.setDefinitionId(definitionId);
- VocabRecord rec = null;
- if (recordMap.containsKey(recordId)) {
- rec = recordMap.get(recordId);
- } else {
- rec = new VocabRecord();
- rec.setId(recordId);
- rec.setVocabularyId(vocabularyId);
- recordMap.put(recordId, rec);
- records.add(rec);
- }
- rec.getFields().add(field);
- }
-
- return records;
- }
-
- };
-
- public final static ResultSetHandler> vocabularyRecordMapHandler = new ResultSetHandler>() {
-
- @Override
- public Map handle(ResultSet rs) throws SQLException {
-
- Map recordMap = new HashMap<>();
- Map records = new HashMap<>();
-
- while (rs.next()) {
- Integer fieldId = rs.getInt("id");
- Integer recordId = rs.getInt("record_id");
- Integer vocabularyId = rs.getInt("vocabulary_id");
- Integer definitionId = rs.getInt("definition_id");
- String label = rs.getString("label");
- String language = rs.getString("language");
- String value = rs.getString("value");
-
- String idField = rs.getString("identifier");
-
- Field field = new Field();
- field.setId(fieldId);
- field.setLabel(label);
- field.setLanguage(language);
- field.setValue(value);
- field.setDefinitionId(definitionId);
- VocabRecord rec = null;
- if (recordMap.containsKey(recordId)) {
- rec = recordMap.get(recordId);
- } else {
- rec = new VocabRecord();
- rec.setId(recordId);
- rec.setVocabularyId(vocabularyId);
- recordMap.put(recordId, rec);
- records.put(idField, rec);
- }
- rec.getFields().add(field);
- }
-
- return records;
- }
-
- };
-
- /**
- * @deprecated This handler is not used anymore
- */
- @Deprecated(since = "23.05", forRemoval = false)
- public static ResultSetHandler> resultSetToVocabularyRecordListHandler = new ResultSetHandler>() {
-
- @Override
- public List handle(ResultSet rs) throws SQLException {
- List records = new LinkedList<>();
- while (rs.next()) {
- VocabRecord rec = convertRecord(rs);
- if (rec != null) {
- records.add(rec);
- }
- }
- return records;
- }
-
- };
-
- /**
- * @deprecated This handler is not used anymore
- */
- @Deprecated(since = "23.05", forRemoval = false)
- public static ResultSetHandler resultSetToVocabularyRecordHandler = new ResultSetHandler() {
-
- @Override
- public VocabRecord handle(ResultSet rs) throws SQLException {
- if (rs.next()) {
- return convertRecord(rs);
- }
- return null;
- }
-
- };
-
- /**
- * @deprecated This method is not used anymore
- *
- * @param rs
- * @return
- */
- @Deprecated(since = "23.05", forRemoval = false)
- private static VocabRecord convertRecord(ResultSet rs) throws SQLException {
-
- int iRecordId = rs.getInt("recordId");
- List lstFields = new ArrayList<>();
-
- int iVocabId = rs.getInt("vocabId");
-
- JsonElement eltAttr = null;
- if (rs.getString("attr") != null) {
- eltAttr = jsonParser.parse(rs.getString("attr"));
- }
-
- if (eltAttr != null) {
- try {
- JsonArray attr = eltAttr.getAsJsonArray();
- if (attr != null) {
- for (JsonElement jsonElt : attr) {
-
- JsonObject jsonField = jsonElt.getAsJsonObject();
- lstFields.add(gson.fromJson(jsonField, Field.class));
- }
- }
- return new VocabRecord(iRecordId, iVocabId, lstFields);
- } catch (Exception e) {
- log.error(e);
- }
- }
- return null;
- }
-
- public static void saveVocabulary(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.saveVocabulary(vocabulary);
- setVocabularyLastAltered(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void deleteVocabulary(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.deleteVocabulary(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
-
- }
-
- /**
- * @deprecated This method is replaced by getAllRecords(Vocabulary)
- *
- * @param vocabulary
- */
- @Deprecated(since = "23.05", forRemoval = false)
- public static void loadRecordsForVocabulary(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.loadRecordsForVocabulary(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void getAllRecords(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.getAllRecords(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void deleteRecord(VocabRecord vocabRecord) {
- try {
- VocabularyMysqlHelper.deleteRecord(vocabRecord);
- setVocabularyLastAltered(vocabRecord.getVocabularyId());
- } catch (SQLException e) {
- log.error(e);
- }
-
- }
-
- public static void deleteAllRecords(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.deleteAllRecords(vocabulary);
- setVocabularyLastAltered(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void saveRecords(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.saveRecords(vocabulary);
- setVocabularyLastAltered(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void saveRecord(Integer vocabularyId, VocabRecord vocabRecord) {
- try {
- VocabularyMysqlHelper.saveRecord(vocabularyId, vocabRecord);
- setVocabularyLastAltered(vocabularyId);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- /**
- * Find the vocabulary records which contain a given string in given fields. This search does not search for exact string match. It does a
- * 'contains'-search
- *
- * @param vocabularyName the vocabulary to search for
- * @param searchValue the value to be searched as term that must be contained within the defined field
- * @param fieldNames the list of fields to search in
- * @return a list of vocabulary records
- *
- * @throws SQLException
- */
- public static List findRecords(String vocabularyName, String searchValue, String... fieldNames) {
- try {
- return VocabularyMysqlHelper.findRecords(vocabularyName, searchValue, false, fieldNames);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- /**
- * Find the vocabulary records which match exactly the given string in the defined fields. This search does search for exact string match.
- *
- * @param vocabularyName the vocabulary to search for
- * @param searchValue the value to be searched as term that must be identical in the defined field
- * @param fieldNames the list of fields to search in
- * @return a list of vocabulary records
- *
- * @throws SQLException
- */
- public static List findExactRecords(String vocabularyName, String searchValue, String... fieldNames) {
- try {
- return VocabularyMysqlHelper.findRecords(vocabularyName, searchValue, true, fieldNames);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- public static VocabRecord getRecord(Integer vocabularyId, Integer recordId) {
- try {
- return VocabularyMysqlHelper.getRecord(vocabularyId, recordId);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- /**
- * Search in the vocabulary for String Pairs which contain the searched terms. This search does not contain exact matches as it does a
- * contains-search
- *
- * @param vocabularyName the vocabulary to search within
- * @param data the StringPair to use for searching
- * @return Vocabulary records
- *
- * @throws SQLException
- */
- public static List findRecords(String vocabularyName, List data) {
- try {
- return VocabularyMysqlHelper.findRecords(vocabularyName, data, false);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- /**
- * Search in the vocabulary for String Pairs which match exactly the searched terms. This search lists only exact matches.
- *
- * @param vocabularyName the vocabulary to search within
- * @param data the StringPair to use for searching
- * @return Vocabulary records
- *
- * @throws SQLException
- */
- public static List findExactRecords(String vocabularyName, List data) {
- try {
- return VocabularyMysqlHelper.findRecords(vocabularyName, data, true);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- public static Map getRecordMap(int vacabularyId, int definitionId) {
- try {
- return VocabularyMysqlHelper.getRecordMap(vacabularyId, definitionId);
- } catch (SQLException e) {
- log.error(e);
- }
- return Collections.emptyMap();
- }
-
- /**
- * @deprecated This method is not used anymore
- *
- * @param vocabulary
- */
- @Deprecated(since = "23.05", forRemoval = false)
- public static void getPaginatedRecords(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.getRecords(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void saveDefinition(Integer vocabularyId, Definition definition) {
- try {
- VocabularyMysqlHelper.saveDefinition(vocabularyId, definition);
- setVocabularyLastAltered(vocabularyId);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void deleteDefinition(Definition definition) {
- try {
- VocabularyMysqlHelper.deleteDefinition(definition);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void insertNewRecords(List records, Integer vocabularyID) {
- try {
- VocabularyMysqlHelper.insertNewRecords(records, vocabularyID);
- setVocabularyLastAltered(vocabularyID);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void batchUpdateRecords(List records, Integer vocabularyID) {
- try {
- VocabularyMysqlHelper.batchUpdateRecords(records, vocabularyID);
- setVocabularyLastAltered(vocabularyID);
- } catch (SQLException e) {
- log.error(e);
- }
-
- }
-
- private static void setVocabularyLastAltered(int vocabularyId) {
- Vocabulary vocab = getVocabularyById(vocabularyId);
- setVocabularyLastAltered(vocab);
- }
-
- public static void setVocabularyLastAltered(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.setVocabularyLastAltered(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static void setVocabularyLastUploaded(Vocabulary vocabulary) {
- try {
- VocabularyMysqlHelper.setVocabularyLastUploaded(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- }
-
- public static Timestamp getVocabularyLastAltered(Vocabulary vocabulary) {
- try {
- return VocabularyMysqlHelper.getVocabularyLastAltered(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
-
- return null;
- }
-
- public static Timestamp getVocabularyLastUploaded(Vocabulary vocabulary) {
- try {
- return VocabularyMysqlHelper.getVocabularyLastUploaded(vocabulary);
- } catch (SQLException e) {
- log.error(e);
- }
- return null;
- }
-
- public static ResultSetHandler resultSetGetLastUploadedHandler = new ResultSetHandler() {
-
- @Override
- public Timestamp handle(ResultSet rs) throws SQLException {
- if (rs.next()) {
- return rs.getTimestamp("lastUploaded");
- }
- return null;
- }
-
- };
-
- public static ResultSetHandler resultSetGetLastAlteredHandler = new ResultSetHandler() {
-
- @Override
- public Timestamp handle(ResultSet rs) throws SQLException {
- if (rs.next()) {
- return rs.getTimestamp("lastAltered");
- }
- return null;
- }
-
- };
-}
diff --git a/src/main/java/de/sub/goobi/persistence/managers/VocabularyMysqlHelper.java b/src/main/java/de/sub/goobi/persistence/managers/VocabularyMysqlHelper.java
deleted file mode 100644
index 91ae10ffdc..0000000000
--- a/src/main/java/de/sub/goobi/persistence/managers/VocabularyMysqlHelper.java
+++ /dev/null
@@ -1,1031 +0,0 @@
-/**
- * This file is part of the Goobi Application - a Workflow tool for the support of mass digitization.
- *
- * Visit the websites for more information.
- * - https://goobi.io
- * - https://www.intranda.com
- * - https://github.com/intranda/goobi-workflow
- *
- * 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 2 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, write to the Free Software Foundation, Inc., 59
- * Temple Place, Suite 330, Boston, MA 02111-1307 USA
- *
- * Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions
- * of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to
- * link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and
- * distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and
- * conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this
- * library, you may extend this exception to your version of the library, but you are not obliged to do so. If you do not wish to do so, delete this
- * exception statement from your version.
- */
-package de.sub.goobi.persistence.managers;
-
-import java.io.Serializable;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.commons.dbutils.QueryRunner;
-import org.apache.commons.dbutils.StatementConfiguration;
-import org.apache.commons.dbutils.handlers.BeanHandler;
-import org.apache.commons.dbutils.handlers.BeanListHandler;
-import org.apache.commons.lang3.StringUtils;
-import org.goobi.production.cli.helper.StringPair;
-import org.goobi.vocabulary.Definition;
-import org.goobi.vocabulary.Field;
-import org.goobi.vocabulary.VocabRecord;
-import org.goobi.vocabulary.Vocabulary;
-
-import com.google.gson.Gson;
-
-import de.sub.goobi.persistence.managers.MySQLHelper.SQLTYPE;
-
-/**
- * @author steffen
- *
- */
-class VocabularyMysqlHelper implements Serializable {
-
- private static final long serialVersionUID = 5141386688477409583L;
-
- private static final Integer MAXIMAL_QUERY_RESULTS = 10_000;
-
- private static final Integer QUERY_TIMEOUT_IN_SECONDS = 20;
-
- private static String vocabTable = "vocabulary";
-
- static Vocabulary getVocabularyByTitle(String title) throws SQLException {
- StringBuilder sql = new StringBuilder();
- sql.append("SELECT * FROM vocabulary v WHERE v.title = ?");
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- Vocabulary ret = new QueryRunner().query(connection, sql.toString(), new BeanHandler<>(Vocabulary.class), title);
- if (ret != null) {
- ret.setStruct(getDefinitionsForVocabulary(ret.getId()));
- }
- return ret;
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static List getDefinitionsForVocabulary(Integer vocabularyId) throws SQLException {
- StringBuilder sql = new StringBuilder();
- sql.append("SELECT * FROM vocabulary_structure v WHERE v.vocabulary_id = ?");
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- return new QueryRunner().query(connection, sql.toString(), new BeanListHandler<>(Definition.class), vocabularyId);
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static Vocabulary getVocabularyById(Integer id) throws SQLException {
- StringBuilder sql = new StringBuilder();
- sql.append("SELECT * FROM vocabulary v WHERE v.id = ?");
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- Vocabulary ret = new QueryRunner().query(connection, sql.toString(), new BeanHandler<>(Vocabulary.class), id);
- ret.setStruct(getDefinitionsForVocabulary(ret.getId()));
- return ret;
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static List getVocabularies(String order, String filter, Integer start, Integer count) throws SQLException {
- Connection connection = null;
- StringBuilder sql = new StringBuilder();
- sql.append("SELECT * FROM vocabulary");
- if (filter != null && !filter.isEmpty()) {
- sql.append(" WHERE " + filter);
- }
- if (order != null && !order.isEmpty()) {
- sql.append(" ORDER BY " + order);
- }
- if (start != null && count != null) {
- sql.append(" LIMIT " + start + ", " + count);
- }
- try {
- connection = MySQLHelper.getInstance().getConnection();
-
- List ret = new QueryRunner().query(connection, sql.toString(), new BeanListHandler<>(Vocabulary.class));
- for (Vocabulary v : ret) {
- v.setStruct(getDefinitionsForVocabulary(v.getId()));
- }
- return ret;
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static int getVocabularyCount(String filter) throws SQLException {
- Connection connection = null;
- StringBuilder sql = new StringBuilder();
- sql.append("SELECT COUNT(1) FROM vocabulary");
- if (filter != null && !filter.isEmpty()) {
- sql.append(" WHERE " + filter);
- }
- try {
- connection = MySQLHelper.getInstance().getConnection();
-
- return new QueryRunner().query(connection, sql.toString(), MySQLHelper.resultSetToIntegerHandler);
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static boolean isTitleUnique(Vocabulary vocabulary) throws SQLException {
- String sql;
- if (vocabulary.getId() == null) {
- // new vocabulary, check against all vocabularies
- sql = "SELECT count(1) FROM vocabulary WHERE title = ?";
- } else {
- // existing vocabulary, exclude current vocabulary from check
- sql = "SELECT count(1) FROM vocabulary WHERE title = ? and id !=" + vocabulary.getId();
- }
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- int numberOfProcessesWithTitle = new QueryRunner().query(connection, sql, MySQLHelper.resultSetToIntegerHandler, vocabulary.getTitle());
- return (numberOfProcessesWithTitle == 0);
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static void saveVocabulary(Vocabulary vocabulary) throws SQLException {
- StringBuilder sql = new StringBuilder();
- if (vocabulary.getId() == null) {
- sql.append("INSERT INTO vocabulary (title, description) ");
- sql.append("VALUES (?,?)");
- } else {
- sql.append("UPDATE vocabulary ");
- sql.append("SET title = ?, description = ? ");
- sql.append("WHERE id = " + vocabulary.getId());
- }
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- if (vocabulary.getId() == null) {
- Integer id = run.insert(connection, sql.toString(), MySQLHelper.resultSetToIntegerHandler, vocabulary.getTitle(),
- vocabulary.getDescription());
- vocabulary.setId(id);
- } else {
- run.update(connection, sql.toString(), vocabulary.getTitle(), vocabulary.getDescription());
- }
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- for (Definition definition : vocabulary.getStruct()) {
- saveDefinition(vocabulary.getId(), definition);
- }
- }
-
- static void saveDefinition(Integer vocabularyId, Definition definition) throws SQLException {
- StringBuilder sql = new StringBuilder();
- if (definition.getId() == null) {
- sql.append("INSERT INTO vocabulary_structure (vocabulary_id, label,language, type,validation,");
- sql.append("required ,mainEntry,distinctive,selection, titleField) ");
- sql.append("VALUES (?,?,?,?,?,?,?,?,?,?)");
- } else {
- sql.append("UPDATE vocabulary_structure ");
- sql.append("SET vocabulary_id = ?, label = ?, ");
- sql.append("language = ?, type = ?, ");
- sql.append("validation = ?, required = ?, ");
- sql.append("mainEntry = ?, distinctive = ?, selection = ?, titleField = ? ");
- sql.append("WHERE id = " + definition.getId());
- }
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- if (definition.getId() == null) {
- Integer id = run.insert(connection, sql.toString(), MySQLHelper.resultSetToIntegerHandler, vocabularyId, definition.getLabel(),
- definition.getLanguage(), definition.getType(), definition.getValidation(), definition.isRequired(), definition.isMainEntry(),
- definition.isDistinctive(), definition.getSelection(), definition.isTitleField());
- definition.setId(id);
- } else {
- run.update(connection, sql.toString(), vocabularyId, definition.getLabel(), definition.getLanguage(), definition.getType(),
- definition.getValidation(), definition.isRequired(), definition.isMainEntry(), definition.isDistinctive(),
- definition.getSelection(), definition.isTitleField());
- }
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static void deleteDefinition(Definition definition) throws SQLException {
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- run.update(connection, "DELETE FROM vocabulary_structure WHERE id = ?", definition.getId());
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- /**
- * @deprecated This method is not used anymore
- *
- * @param vocabulary
- * @param gson
- */
- @Deprecated(since = "23.05", forRemoval = true)
- static void saveVocabulary(Vocabulary vocabulary, Gson gson) throws SQLException {
- StringBuilder sql = new StringBuilder();
-
- if (vocabulary.getId() == null) {
- sql.append("INSERT INTO " + vocabTable + "(title, description, structure) ");
- sql.append("VALUES (?,?,?)");
- } else {
- sql.append("UPDATE " + vocabTable + " ");
- sql.append("SET title = ?, description = ?, structure = ? ");
- sql.append("WHERE vocabId = " + vocabulary.getId());
- }
- Connection connection = null;
-
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- if (vocabulary.getId() == null) {
- Integer id = run.insert(connection, sql.toString(), MySQLHelper.resultSetToIntegerHandler, vocabulary.getTitle(),
- vocabulary.getDescription(), gson.toJson(vocabulary.getStruct()));
- vocabulary.setId(id);
- } else {
- run.update(connection, sql.toString(), vocabulary.getTitle(), vocabulary.getDescription(), gson.toJson(vocabulary.getStruct()));
- }
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
-
- }
-
- static void deleteVocabulary(Vocabulary vocabulary) throws SQLException {
- if (vocabulary.getId() != null) {
- String deleteRecords = "DELETE FROM vocabulary_record WHERE vocabulary_id = ?";
- String deleteRecordData = "DELETE FROM vocabulary_record_data WHERE vocabulary_id = ?";
- String deleteVocabulary = "DELETE from vocabulary WHERE id = ?";
- String deleteDefinitions = "DELETE from vocabulary_structure WHERE vocabulary_id = ?";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- run.update(connection, deleteRecordData, vocabulary.getId());
- run.update(connection, deleteRecords, vocabulary.getId());
- run.update(connection, deleteDefinitions, vocabulary.getId());
- run.update(connection, deleteVocabulary, vocabulary.getId());
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
- }
-
- static void getAllRecords(Vocabulary vocabulary) throws SQLException {
- if (vocabulary != null && vocabulary.getId() != null) {
- String sql = "SELECT * FROM vocabulary_record_data WHERE vocabulary_id = ?";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- List records =
- new QueryRunner().query(connection, sql, VocabularyManager.vocabularyRecordListHandler, vocabulary.getId());
- for (VocabRecord rec : records) {
- setDefinitionsToRecord(rec, vocabulary);
- }
- vocabulary.setRecords(records);
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
- }
-
- /**
- * @deprecated This method is not used anymore
- *
- * @param vocabulary
- */
- @Deprecated(since = "23.05", forRemoval = true)
- static void loadRecordsForVocabulary(Vocabulary vocabulary) throws SQLException {
- if (vocabulary != null && vocabulary.getId() != null) {
- String sql = "SELECT * FROM vocabularyRecords WHERE vocabId = ?";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- List records =
- new QueryRunner().query(connection, sql, VocabularyManager.resultSetToVocabularyRecordListHandler, vocabulary.getId());
- for (VocabRecord rec : records) {
- // merge expected definitions with existing definitions
- mergeRecordAndVocabulary(vocabulary, rec);
- }
- vocabulary.setRecords(records);
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
- }
-
- /**
- * @deprecated This method is not used anymore
- *
- * @param vocabulary
- * @param rec
- */
- @Deprecated(since = "23.05", forRemoval = true)
- private static void mergeRecordAndVocabulary(Vocabulary vocabulary, VocabRecord rec) {
- for (Definition definition : vocabulary.getStruct()) {
- boolean fieldFound = false;
- for (Field f : rec.getFields()) {
- if (f.getLabel().equals(definition.getLabel())
- && ((StringUtils.isBlank(f.getLanguage()) && StringUtils.isBlank(definition.getLanguage()))
- || definition.getLanguage().equals(f.getLanguage()))) {
- f.setDefinition(definition);
- fieldFound = true;
- break;
- }
- }
- if (!fieldFound) {
- Field field = new Field(definition.getLabel(), definition.getLanguage(), "", definition);
- rec.getFields().add(field);
- }
- }
- // check if field definition was deleted
- // if this is the case, remove the field as well
- List fieldsToDelete = new ArrayList<>();
- for (Field f : rec.getFields()) {
- if (f.getDefinition() == null) {
- fieldsToDelete.add(f);
- }
- }
- if (!fieldsToDelete.isEmpty()) {
- for (Field f : fieldsToDelete) {
- rec.getFields().remove(f);
- }
- }
- // set field order
- List orderedList = new ArrayList<>(rec.getFields().size());
- for (Definition definition : vocabulary.getStruct()) {
- for (Field f : rec.getFields()) {
- if (f.getDefinition().equals(definition)) {
- orderedList.add(f);
- break;
- }
- }
- }
- rec.setFields(orderedList);
- }
-
- static void deleteRecord(VocabRecord vocabRecord) throws SQLException {
- if (vocabRecord.getId() != null) {
- String deleteFields = "DELETE from vocabulary_record_data WHERE record_id = ?";
- String deleteRecord = "DELETE from vocabulary_record WHERE id = ?";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- run.update(connection, deleteFields, vocabRecord.getId());
- run.update(connection, deleteRecord, vocabRecord.getId());
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
- }
-
- static void deleteAllRecords(Vocabulary vocabulary) throws SQLException {
- if (vocabulary.getId() != null) {
- String deleteFields = "DELETE from vocabulary_record_data WHERE vocabulary_id = ? ";
- String deleteRecords = "DELETE from vocabulary_record WHERE vocabulary_id = ? ";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- run.update(connection, deleteFields, vocabulary.getId());
- run.update(connection, deleteRecords, vocabulary.getId());
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
- }
-
- static void saveRecords(Vocabulary vocabulary) throws SQLException {
- String insertRecord = "INSERT INTO vocabulary_record (vocabulary_id) VALUES (?)";
- String insertField =
- "INSERT INTO vocabulary_record_data (record_id,vocabulary_id, definition_id, label, language, value) VALUES (?,?,?,?,?,?)";
- String updateField =
- "UPDATE vocabulary_record_data set record_id=?,vocabulary_id=?, definition_id=?, label=?, language=?, value=? WHERE id = ?";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- for (VocabRecord vocabRecord : vocabulary.getRecords()) {
- if (vocabRecord.getId() == null) {
- Integer id = run.insert(connection, insertRecord, MySQLHelper.resultSetToIntegerHandler, vocabulary.getId());
- vocabRecord.setId(id);
- }
- for (Field field : vocabRecord.getFields()) {
- if (field.getId() == null) {
- Integer id = run.insert(connection, insertField, MySQLHelper.resultSetToIntegerHandler, vocabRecord.getId(),
- vocabulary.getId(), field.getDefinition().getId(), field.getLabel(), field.getLanguage(), field.getValue());
- field.setId(id);
- } else {
- run.update(connection, updateField, vocabRecord.getId(), vocabulary.getId(), field.getDefinition().getId(), field.getLabel(),
- field.getLanguage(), field.getValue(), field.getId());
- }
- }
- }
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static void saveRecord(Integer vocabularyId, VocabRecord vocabRecord) throws SQLException {
- String insertRecord = "INSERT INTO vocabulary_record (vocabulary_id) VALUES (?)";
- String insertField =
- "INSERT INTO vocabulary_record_data (record_id,vocabulary_id, definition_id, label, language, value) VALUES (?,?,?,?,?,?)";
- String updateField =
- "UPDATE vocabulary_record_data set record_id=?,vocabulary_id=?, definition_id=?, label=?, language=?, value=? WHERE id = ?";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner run = new QueryRunner();
- if (vocabRecord.getId() == null) {
- Integer id = run.insert(connection, insertRecord, MySQLHelper.resultSetToIntegerHandler, vocabularyId);
- vocabRecord.setId(id);
- }
- for (Field field : vocabRecord.getFields()) {
- if (field.getId() == null) {
- Integer id = run.insert(connection, insertField, MySQLHelper.resultSetToIntegerHandler, vocabRecord.getId(), vocabularyId,
- field.getDefinition().getId(), field.getLabel(), field.getLanguage(), field.getValue());
- field.setId(id);
- } else {
- run.update(connection, updateField, vocabRecord.getId(), vocabularyId, field.getDefinition().getId(), field.getLabel(),
- field.getLanguage(), field.getValue(), field.getId());
- }
- }
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static List findRecords(String vocabularyName, String searchValue, boolean exact, String... fieldNames) throws SQLException {
- String likeStr = "like";
- if (MySQLHelper.getInstance().getSqlType() == SQLTYPE.H2) {
- likeStr = "ilike";
- }
-
- searchValue = MySQLHelper.escapeSql(searchValue.replace("\"", "_"));
- StringBuilder sb = new StringBuilder();
- sb.append("SELECT distinct record_id FROM vocabulary_record_data r LEFT JOIN vocabulary v ON v.id = r.vocabulary_id WHERE v.title = ? ");
- sb.append("AND r.value ");
- sb.append(likeStr);
- sb.append(" '");
- if (!exact) {
- sb.append("%");
- }
- sb.append(searchValue);
- if (!exact) {
- sb.append("%");
- }
- sb.append("' ");
- if (fieldNames != null && fieldNames.length > 0) {
- sb.append(" AND (");
- StringBuilder subQuery = new StringBuilder();
- for (String fieldName : fieldNames) {
- if (subQuery.length() > 0) {
- subQuery.append(" OR ");
- }
- subQuery.append("r.label ='" + MySQLHelper.escapeSql(fieldName) + "'");
- }
- sb.append(subQuery.toString());
- sb.append(")");
-
- }
-
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner runner = new QueryRunner();
- List idList =
- runner.query(connection, sb.toString(), MySQLHelper.resultSetToIntegerListHandler, MySQLHelper.escapeSql(vocabularyName));
-
- if (idList.isEmpty()) {
- return Collections.emptyList();
- }
- StringBuilder query = new StringBuilder();
- query.append("SELECT * FROM vocabulary_record_data r LEFT JOIN vocabulary v ON v.id = r.vocabulary_id WHERE r.record_id in (");
- StringBuilder sub = new StringBuilder();
-
- for (Integer id : idList) {
- if (sub.length() > 0) {
- sub.append(", ");
- }
- sub.append(id);
- }
-
- query.append(sub.toString());
- query.append(")");
- List records = new QueryRunner().query(connection, query.toString(), VocabularyManager.vocabularyRecordListHandler);
-
- Vocabulary vocabulary = getVocabularyByTitle(vocabularyName);
- for (VocabRecord rec : records) {
- setDefinitionsToRecord(rec, vocabulary);
- }
-
- return records;
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- private static void setDefinitionsToRecord(VocabRecord rec, Vocabulary vocabulary) {
- for (Definition def : vocabulary.getStruct()) {
- boolean fieldFound = false;
- for (Field field : rec.getFields()) {
- if (def.getId().equals(field.getDefinitionId())) {
- field.setDefinition(def);
- fieldFound = true;
- break;
- }
- }
- if (!fieldFound) {
- Field field = new Field();
- field.setDefinition(def);
- field.setLabel(def.getLabel());
- field.setLanguage(def.getLanguage());
- rec.getFields().add(field);
- }
- }
- }
-
- static VocabRecord getRecord(Integer vocabularyId, Integer recordId) throws SQLException {
- if (vocabularyId == null || recordId == null) {
- return null;
- }
- String sql = "SELECT * FROM vocabulary_record_data WHERE vocabulary_id = ? AND record_id = ?";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
-
- List recordList =
- new QueryRunner().query(connection, sql, VocabularyManager.vocabularyRecordListHandler, vocabularyId, recordId);
- VocabRecord rec = null;
- if (!recordList.isEmpty()) {
- Vocabulary vocabulary = getVocabularyById(vocabularyId);
- rec = recordList.get(0);
- setDefinitionsToRecord(rec, vocabulary);
- }
- return rec;
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- /**
- * @deprecated This method is not used anymore
- *
- * @param vocabulary
- */
- @Deprecated(since = "23.05", forRemoval = true)
- static void getRecords(Vocabulary vocabulary) throws SQLException {
- String likeStr = "like";
- if (MySQLHelper.getInstance().getSqlType() == SQLTYPE.H2) {
- likeStr = "ilike";
- }
-
- StringBuilder sb = new StringBuilder();
- sb.append("SELECT vocabularyRecords.* FROM vocabularyRecords LEFT JOIN " + vocabTable + " ON vocabularyRecords.vocabId=" + vocabTable
- + ".vocabId ");
- sb.append("WHERE " + vocabTable + ".vocabId = ? ");
- StringBuilder subQuery = new StringBuilder();
-
- if (StringUtils.isNotBlank(vocabulary.getSearchField())) {
- if (subQuery.length() == 0) {
- subQuery.append(" AND ");
- subQuery.append("(");
- } else {
- subQuery.append(" OR ");
- }
- subQuery.append("attr ");
- subQuery.append(likeStr);
- subQuery.append(" '%label\":\"" + MySQLHelper.escapeSql(vocabulary.getMainFieldName()) + "\",\"language\":\"%\",\"value\":\"%"
- + MySQLHelper.escapeSql(vocabulary.getSearchField().replace("\"", "_")) + "%' ");
- }
-
- if (subQuery.length() > 0) {
- subQuery.append(")");
- sb.append(subQuery.toString());
- }
-
- Connection connection = null;
-
- try {
- connection = MySQLHelper.getInstance().getConnection();
- QueryRunner runner = new QueryRunner();
-
- // total number of records
-
- int numberOfRecords = runner.query(connection, "SELECT COUNT(1) FROM (" + sb.toString() + ") a", MySQLHelper.resultSetToIntegerHandler,
- vocabulary.getId());
- vocabulary.setTotalNumberOfRecords(numberOfRecords);
-
- // order
- if (MySQLHelper.isJsonCapable()) {
- String sqlPathToField = "SELECT REPLACE(JSON_SEARCH(attr, 'one', '" + vocabulary.getMainFieldName()
- + "'), 'label','value') from vocabularyRecords WHERE vocabId= ? limit 1";
- String field = runner.query(connection, sqlPathToField, MySQLHelper.resultSetToStringHandler, vocabulary.getId());
- sb.append(" ORDER BY " + "JSON_EXTRACT(attr, " + field + ") ");
- if (StringUtils.isNotBlank(vocabulary.getOrder())) {
- sb.append(vocabulary.getOrder());
- }
- }
- // limit
- sb.append(" LIMIT " + (vocabulary.getPageNo() * vocabulary.getNumberOfRecordsPerPage()) + ", " + vocabulary.getNumberOfRecordsPerPage());
-
- List records =
- runner.query(connection, sb.toString(), VocabularyManager.resultSetToVocabularyRecordListHandler, vocabulary.getId());
- for (VocabRecord rec : records) {
- // merge expected definitions with existing definitions
- mergeRecordAndVocabulary(vocabulary, rec);
- }
- vocabulary.setRecords(records);
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- static List findRecords(String vocabularyName, List data, boolean exactSearch) throws SQLException {
- String likeStr = "like";
- if (MySQLHelper.getInstance().getSqlType() == SQLTYPE.H2) {
- likeStr = "ilike";
- }
-
- StringBuilder sb = new StringBuilder();
- sb.append("SELECT r.* FROM vocabulary_record_data r LEFT JOIN vocabulary v ON v.id = r.vocabulary_id WHERE v.title = ? ");
-
- StringBuilder subQuery = new StringBuilder();
- for (StringPair sp : data) {
- if (StringUtils.isNotBlank(sp.getTwo())) {
- if (subQuery.length() > 0) {
- subQuery.append(" OR ");
- }
- subQuery.append("(value ");
- subQuery.append(likeStr);
- subQuery.append(" '");
- if (!exactSearch) {
- subQuery.append("%");
- }
- subQuery.append(MySQLHelper.escapeSql(sp.getTwo().replace("\"", "_")));
- if (!exactSearch) {
- subQuery.append("%");
- }
- subQuery.append("' AND ");
- subQuery.append("label ='" + MySQLHelper.escapeSql(sp.getOne()) + "') ");
- }
- }
- if (subQuery.length() > 0) {
- sb.append(" AND r.record_id in (select distinct record_id from vocabulary_record_data where ");
- sb.append(subQuery.toString());
- sb.append(")");
- }
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- StatementConfiguration stmtConfig =
- new StatementConfiguration.Builder().maxRows(MAXIMAL_QUERY_RESULTS).queryTimeout(QUERY_TIMEOUT_IN_SECONDS).build();
- QueryRunner queryRunner = new QueryRunner(stmtConfig);
- List records = queryRunner.query(connection, sb.toString(), VocabularyManager.vocabularyRecordListHandler, vocabularyName);
- Vocabulary vocabulary = getVocabularyByTitle(vocabularyName);
- for (VocabRecord rec : records) {
- setDefinitionsToRecord(rec, vocabulary);
- }
- return records;
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- public static void insertNewRecords(List records, Integer vocabularyID) throws SQLException {
- StringBuilder insertRecordQuery = new StringBuilder();
- insertRecordQuery.append("INSERT INTO vocabulary_record (id, vocabulary_id) VALUES ");
-
- for (int i = 0; i < records.size(); i++) {
-
- if (i == 0) {
- insertRecordQuery.append(" (?, ?)");
- } else {
- insertRecordQuery.append(", (?, ?)");
- }
- }
- Connection connection = null;
- QueryRunner runner = new QueryRunner();
- try {
- connection = MySQLHelper.getInstance().getConnection();
- try {
- if (MySQLHelper.getInstance().getSqlType() != SQLTYPE.H2) {
- runner.execute(connection, "Lock tables vocabulary_record write");
- }
- int id = runner.query(connection, "SELECT MAX(id) +1 FROM vocabulary_record", MySQLHelper.resultSetToIntegerHandler);
- Object[] parameter = new Object[records.size() * 2];
- for (int i = 0; i < records.size(); i++) {
- VocabRecord rec = records.get(i);
- rec.setId(id);
- parameter[i * 2] = id;
- parameter[i * 2 + 1] = vocabularyID;
- id = id + 1;
- }
- runner.execute(connection, insertRecordQuery.toString(), parameter);
- } finally {
- if (MySQLHelper.getInstance().getSqlType() != SQLTYPE.H2) {
- runner.execute(connection, "unlock tables");
- }
- }
- fieldsBatchInsertion(records, vocabularyID, connection, runner);
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
-
- }
-
- private static void fieldsBatchInsertion(List records, Integer vocabularyID, Connection connection, QueryRunner runner)
- throws SQLException {
- // create a single query for all fields
- String fieldQuery = "INSERT INTO vocabulary_record_data (record_id,vocabulary_id, definition_id, label, language, value) VALUES ";
- int totalNumberOfRecords = records.size();
- int currentBatchNo = 0;
- int numberOfRecordsPerBatch = 50;
- while (totalNumberOfRecords > (currentBatchNo * numberOfRecordsPerBatch)) {
- List subList;
- if (totalNumberOfRecords > (currentBatchNo * numberOfRecordsPerBatch) + numberOfRecordsPerBatch) {
- subList = records.subList(currentBatchNo * numberOfRecordsPerBatch,
- (currentBatchNo * numberOfRecordsPerBatch) + numberOfRecordsPerBatch);
- } else {
- subList = records.subList(currentBatchNo * numberOfRecordsPerBatch, totalNumberOfRecords);
- }
-
- List parameter = new ArrayList<>();
-
- StringBuilder insertFieldQuery = new StringBuilder();
- insertFieldQuery.append(fieldQuery);
- boolean isFirst = true;
- for (int i = 0; i < subList.size(); i++) {
- VocabRecord rec = subList.get(i);
- for (int j = 0; j < rec.getFields().size(); j++) {
- if (isFirst) {
- isFirst = false;
- insertFieldQuery.append("(?,?,?,?,?,?) ");
- } else {
- insertFieldQuery.append(", (?,?,?,?,?,?) ");
- }
- Field f = rec.getFields().get(j);
- parameter.add(rec.getId());
- parameter.add(vocabularyID);
- parameter.add(f.getDefinition().getId());
- parameter.add(f.getLabel());
- parameter.add(f.getLanguage());
- parameter.add(MySQLHelper.escapeSql(f.getValue()));
- }
- }
- runner.execute(connection, insertFieldQuery.toString(), parameter.toArray());
- currentBatchNo = currentBatchNo + 1;
- }
- }
-
- // private static void fieldsBatchInsertion(List records, Integer vocabularyID, Connection connection, QueryRunner runner)
- // throws SQLException {
- // // create a single query for all fields
- // int totalNumberOfRecords = records.size();
- // int currentBatchNo = 0;
- // int numberOfRecordsPerBatch = 200;
- // connection.setAutoCommit(false);
- // PreparedStatement pstmt = connection.prepareStatement(
- // "INSERT INTO vocabulary_record_data (record_id,vocabulary_id, definition_id, label, language, value) VALUES (?,?,?,?,?,?)");
- // while (totalNumberOfRecords > (currentBatchNo * numberOfRecordsPerBatch)) {
- // List subList;
- // if (totalNumberOfRecords > (currentBatchNo * numberOfRecordsPerBatch) + numberOfRecordsPerBatch) {
- // subList = records.subList(currentBatchNo * numberOfRecordsPerBatch,
- // (currentBatchNo * numberOfRecordsPerBatch) + numberOfRecordsPerBatch);
- // } else {
- // subList = records.subList(currentBatchNo * numberOfRecordsPerBatch, totalNumberOfRecords);
- // }
- //
- //
- // pstmt.clearParameters();
- // pstmt.clearBatch();
- // for (int i = 0; i < subList.size(); i++) {
- // VocabRecord rec = subList.get(i);
- // for (int j = 0; j < rec.getFields().size(); j++) {
- //
- // Field f = rec.getFields().get(j);
- // pstmt.setInt(1, rec.getId());
- // pstmt.setInt(2, vocabularyID);
- // pstmt.setInt(3, f.getDefinition().getId());
- // pstmt.setString(4, f.getLabel());
- // pstmt.setString(5, f.getLanguage());
- // pstmt.setString(6, MySQLHelper.escapeSql(f.getValue()));
- // // Add row to the batch.
- // pstmt.addBatch();
- // }
- // }
- // pstmt.executeBatch();
- // currentBatchNo = currentBatchNo + 1;
- // }
- // connection.commit();
- // }
-
- public static void batchUpdateRecords(List records, Integer vocabularyID) throws SQLException {
- // 1.) delete old fields
- StringBuilder sql = new StringBuilder();
- sql.append("DELETE from vocabulary_record_data WHERE record_id IN (");
-
- StringBuilder ids = new StringBuilder();
-
- for (VocabRecord rec : records) {
- if (ids.length() > 0) {
- ids.append(", ");
- }
- ids.append(rec.getId());
- }
- sql.append(ids.toString());
- sql.append(")");
-
- Connection connection = null;
- try {
- QueryRunner runner = new QueryRunner();
- connection = MySQLHelper.getInstance().getConnection();
- runner.execute(connection, sql.toString());
- // 2.) insert new fields
- fieldsBatchInsertion(records, vocabularyID, connection, runner);
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
-
- }
-
- public static Timestamp getVocabularyLastAltered(Vocabulary vocabulary) throws SQLException {
-
- if (vocabulary == null) {
- return null;
- }
- String sql = "SELECT * FROM vocabulary WHERE id = ? ";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
-
- return new QueryRunner().query(connection, sql, VocabularyManager.resultSetGetLastAlteredHandler, vocabulary.getId());
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- public static void setVocabularyLastAltered(Vocabulary vocabulary) throws SQLException {
-
- if (vocabulary == null) {
- return;
- }
-
- String updateSql = "UPDATE vocabulary SET lastAltered = ? WHERE id = ?";
- Connection connection = null;
-
- try {
- Calendar calendar = Calendar.getInstance();
- Timestamp timeNow = new Timestamp(calendar.getTime().getTime());
- connection = MySQLHelper.getInstance().getConnection();
-
- new QueryRunner().update(connection, updateSql, timeNow, vocabulary.getId());
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
-
- }
-
- public static Timestamp getVocabularyLastUploaded(Vocabulary vocabulary) throws SQLException {
- if (vocabulary == null) {
- return null;
- }
- String sql = "SELECT * FROM " + vocabTable + " WHERE id = ? ";
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
-
- return new QueryRunner().query(connection, sql, VocabularyManager.resultSetGetLastUploadedHandler, vocabulary.getId());
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- public static void setVocabularyLastUploaded(Vocabulary vocabulary) throws SQLException {
-
- if (vocabulary == null) {
- return;
- }
-
- String updateSql = "UPDATE vocabulary SET lastUploaded = ? WHERE id = ?";
- Connection connection = null;
-
- try {
- Calendar calendar = Calendar.getInstance();
- Timestamp timeNow = new Timestamp(calendar.getTime().getTime());
- connection = MySQLHelper.getInstance().getConnection();
-
- new QueryRunner().update(connection, updateSql, timeNow, vocabulary.getId());
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-
- public static Map getRecordMap(int vocabularyId, int definitionId) throws SQLException {
-
- StringBuilder sql = new StringBuilder();
- sql.append("SELECT r2.value as identifier, r1.* FROM vocabulary_record_data r1 ");
- sql.append("left join vocabulary_record_data r2 on r1.record_id = r2.record_id and r2.definition_id = ? ");
- sql.append("WHERE r1.vocabulary_id = ? ");
-
- Connection connection = null;
- try {
- connection = MySQLHelper.getInstance().getConnection();
- return new QueryRunner().query(connection, sql.toString(), VocabularyManager.vocabularyRecordMapHandler, definitionId, vocabularyId);
-
- } finally {
- if (connection != null) {
- MySQLHelper.closeConnection(connection);
- }
- }
- }
-}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/APIException.java b/src/main/java/io/goobi/workflow/api/vocabulary/APIException.java
new file mode 100644
index 0000000000..84d3b846ac
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/APIException.java
@@ -0,0 +1,24 @@
+package io.goobi.workflow.api.vocabulary;
+
+import io.goobi.vocabulary.exception.VocabularyException;
+import lombok.Getter;
+
+@Getter
+public class APIException extends RuntimeException {
+ private final String url;
+ private final String method;
+ private final int statusCode;
+ private final String reason;
+ private final VocabularyException vocabularyCause;
+ private final Exception cause;
+
+ public APIException(String url, String method, int statusCode, String reason, VocabularyException vocabularyCause, Exception cause) {
+ super("API call was not successful" + (statusCode >= 0 ? " with status code [" + statusCode + "]" : "") + ": " + method + " -> " + url + ", Reason:\n" + reason);
+ this.url = url;
+ this.method = method;
+ this.statusCode = statusCode;
+ this.reason = reason;
+ this.vocabularyCause = vocabularyCause;
+ this.cause = cause;
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/CRUDAPI.java b/src/main/java/io/goobi/workflow/api/vocabulary/CRUDAPI.java
new file mode 100644
index 0000000000..8b1649c344
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/CRUDAPI.java
@@ -0,0 +1,58 @@
+package io.goobi.workflow.api.vocabulary;
+
+import io.goobi.vocabulary.exchange.Identifiable;
+
+import java.util.Optional;
+
+public abstract class CRUDAPI {
+ protected final RESTAPI restApi;
+ private final Class instanceTypeClass;
+ private final Class pageResultTypeClass;
+ private final String commonEndpoint;
+ private final String instanceEndpoint;
+
+ protected CRUDAPI(String host, int port, Class instanceTypeClass, Class pageResultTypeClass, String commonEndpoint, String instanceEndpoint) {
+ this.restApi = new RESTAPI(host, port);
+ this.instanceTypeClass = instanceTypeClass;
+ this.pageResultTypeClass = pageResultTypeClass;
+ this.commonEndpoint = commonEndpoint;
+ this.instanceEndpoint = instanceEndpoint;
+ }
+
+ public PageResultType list() {
+ return restApi.get(commonEndpoint, pageResultTypeClass);
+ }
+
+ public PageResultType list(Optional size, Optional page) {
+ String params = "";
+ if (size.isPresent()) {
+ params += params.isEmpty() ? "?" : "&";
+ params += "size=" + size.get();
+ }
+ if (page.isPresent()) {
+ params += params.isEmpty() ? "?" : "&";
+ params += "page=" + page.get();
+ }
+ return restApi.get(commonEndpoint + params, pageResultTypeClass);
+ }
+
+ public InstanceType get(long id) {
+ return restApi.get(instanceEndpoint, instanceTypeClass, id);
+ }
+
+ public InstanceType create(InstanceType obj) {
+ return restApi.post(commonEndpoint, instanceTypeClass, obj);
+ }
+
+ public InstanceType change(InstanceType obj) {
+ long id = obj.getId();
+ obj.setId(null);
+ InstanceType newObj = restApi.put(instanceEndpoint, instanceTypeClass, obj, id);
+ obj.setId(id);
+ return newObj;
+ }
+
+ public void delete(InstanceType obj) {
+ restApi.delete(instanceEndpoint, instanceTypeClass, obj.getId());
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/FieldTypeAPI.java b/src/main/java/io/goobi/workflow/api/vocabulary/FieldTypeAPI.java
new file mode 100644
index 0000000000..bdddc058e3
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/FieldTypeAPI.java
@@ -0,0 +1,22 @@
+package io.goobi.workflow.api.vocabulary;
+
+import io.goobi.vocabulary.exchange.FieldType;
+import io.goobi.workflow.api.vocabulary.hateoas.FieldTypePageResult;
+import io.goobi.workflow.api.vocabulary.helper.CachedLookup;
+
+public class FieldTypeAPI extends CRUDAPI {
+ private static final String COMMON_ENDPOINT = "/api/v1/types";
+ private static final String INSTANCE_ENDPOINT = COMMON_ENDPOINT + "/{{0}}";
+
+ private final CachedLookup singleLookupCache;
+
+ public FieldTypeAPI(String host, int port) {
+ super(host, port, FieldType.class, FieldTypePageResult.class, COMMON_ENDPOINT, INSTANCE_ENDPOINT);
+ this.singleLookupCache = new CachedLookup<>(super::get);
+ }
+
+ @Override
+ public FieldType get(long id) {
+ return this.singleLookupCache.getCached(id);
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/LanguageAPI.java b/src/main/java/io/goobi/workflow/api/vocabulary/LanguageAPI.java
new file mode 100644
index 0000000000..3350a0e1a3
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/LanguageAPI.java
@@ -0,0 +1,22 @@
+package io.goobi.workflow.api.vocabulary;
+
+import io.goobi.vocabulary.exchange.Language;
+import io.goobi.workflow.api.vocabulary.hateoas.LanguagePageResult;
+import io.goobi.workflow.api.vocabulary.helper.CachedLookup;
+
+public class LanguageAPI extends CRUDAPI {
+ private static final String COMMON_ENDPOINT = "/api/v1/languages";
+ private static final String FIND_INSTANCE_ENDPOINT = "/api/v1/languages/by-abbreviation/{{0}}";
+ private static final String INSTANCE_ENDPOINT = COMMON_ENDPOINT + "/{{0}}";
+
+ private final CachedLookup singleLookupCache;
+
+ public LanguageAPI(String host, int port) {
+ super(host, port, Language.class, LanguagePageResult.class, COMMON_ENDPOINT, INSTANCE_ENDPOINT);
+ this.singleLookupCache = new CachedLookup<>(abbreviation -> restApi.get(FIND_INSTANCE_ENDPOINT, Language.class, abbreviation));
+ }
+
+ public Language findByAbbreviation(String abbreviation) {
+ return this.singleLookupCache.getCached(abbreviation);
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/RESTAPI.java b/src/main/java/io/goobi/workflow/api/vocabulary/RESTAPI.java
new file mode 100644
index 0000000000..2283c71bc6
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/RESTAPI.java
@@ -0,0 +1,167 @@
+package io.goobi.workflow.api.vocabulary;
+
+import io.goobi.vocabulary.exception.VocabularyException;
+import lombok.Setter;
+import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+import org.glassfish.jersey.media.multipart.MultiPart;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
+import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
+
+import javax.servlet.http.Part;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RESTAPI {
+ @Setter
+ private static Client client = ClientBuilder.newBuilder()
+ .register(MultiPartFeature.class)
+ .register(FileDataBodyPart.class)
+ .build();
+ private String baseUrl;
+
+ protected RESTAPI(String host, int port) {
+ baseUrl = "http://" + host + ":" + port;
+ }
+
+ private String generateUrl(String endpoint, Object... parameters) {
+ String url = endpoint;
+ if (!url.startsWith(baseUrl)) {
+ url = baseUrl + url;
+ }
+ List queryParams = new ArrayList<>();
+ for (int i = 0; i < parameters.length; i++) {
+ if (url.contains("{{" + i + "}}")) {
+ url = url.replace("{{" + i + "}}", parameters[i].toString());
+ } else {
+ queryParams.add(parameters[i].toString());
+ }
+ }
+ if (!queryParams.isEmpty()) {
+ url = url + "?" + String.join("&", queryParams);
+ }
+ return url;
+ }
+
+ public T get(String endpoint, Class clazz, Object... parameters) {
+ try {
+ try (Response response = client
+ .target(generateUrl(endpoint, parameters))
+ .request(MediaType.APPLICATION_JSON)
+ .get()) {
+ if (response.getStatus() / 100 != 2) {
+ throw new APIException(generateUrl(endpoint, parameters), "GET", response.getStatus(), "Vocabulary server error", response.readEntity(VocabularyException.class), null);
+ }
+ return response.readEntity(clazz);
+ }
+ } catch (APIException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new APIException(generateUrl(endpoint, parameters), "GET", -1, e.getMessage(), null, e);
+ }
+ }
+
+ public T post(String endpoint, Class clazz, T obj, Object... parameters) {
+ try {
+ try (Response response = client
+ .target(generateUrl(endpoint, parameters))
+ .request(MediaType.APPLICATION_JSON)
+ .post(Entity.json(obj))) {
+ if (response.getStatus() / 100 != 2) {
+ throw new APIException(generateUrl(endpoint, parameters), "POST", response.getStatus(), "Vocabulary server error", response.readEntity(VocabularyException.class), null);
+ }
+ return response.readEntity(clazz);
+ }
+ } catch (APIException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new APIException(generateUrl(endpoint, parameters), "POST", -1, e.getMessage(), null, e);
+ }
+ }
+
+ public T put(String endpoint, Class clazz, T obj, Object... parameters) {
+ try {
+ try (Response response = client
+ .target(generateUrl(endpoint, parameters))
+ .request(MediaType.APPLICATION_JSON)
+ .put(Entity.json(obj))) {
+ if (response.getStatus() / 100 != 2) {
+ throw new APIException(generateUrl(endpoint, parameters), "PUT", response.getStatus(), "Vocabulary server error", response.readEntity(VocabularyException.class), null);
+ }
+ return response.readEntity(clazz);
+ }
+ } catch (APIException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new APIException(generateUrl(endpoint, parameters), "PUT", -1, e.getMessage(), null, e);
+ }
+ }
+
+ public Response put(String endpoint, Part part, Object... parameters) {
+ try {
+ StreamDataBodyPart body = new StreamDataBodyPart("file", part.getInputStream());
+ try (MultiPart multiPart = new FormDataMultiPart()) {
+ multiPart.bodyPart(body);
+ try (Response response = client
+ .target(generateUrl(endpoint, parameters))
+ .request(MediaType.MULTIPART_FORM_DATA)
+ .put(Entity.entity(multiPart, multiPart.getMediaType()), Response.class)) {
+ if (response.getStatus() / 100 != 2) {
+ throw new APIException(generateUrl(endpoint, parameters), "PUT", response.getStatus(), "Vocabulary server error", response.readEntity(VocabularyException.class), null);
+ }
+ return response.readEntity(Response.class);
+ }
+ }
+ } catch (APIException e) {
+ throw e;
+ } catch (RuntimeException | IOException e) {
+ throw new APIException(generateUrl(endpoint, parameters), "PUT", -1, e.getMessage(), null, e);
+ }
+ }
+
+ public Response post(String endpoint, Part part, Object... parameters) {
+ try {
+ StreamDataBodyPart body = new StreamDataBodyPart("file", part.getInputStream());
+ try (MultiPart multiPart = new FormDataMultiPart()) {
+ multiPart.bodyPart(body);
+ try (Response response = client
+ .target(generateUrl(endpoint, parameters))
+ .request(MediaType.MULTIPART_FORM_DATA)
+ .post(Entity.entity(multiPart, multiPart.getMediaType()), Response.class)) {
+ if (response.getStatus() / 100 != 2) {
+ throw new APIException(generateUrl(endpoint, parameters), "POST", response.getStatus(), "Vocabulary server error", response.readEntity(VocabularyException.class), null);
+ }
+ return response.readEntity(Response.class);
+ }
+ }
+ } catch (APIException e) {
+ throw e;
+ } catch (RuntimeException | IOException e) {
+ throw new APIException(generateUrl(endpoint, parameters), "POST", -1, e.getMessage(), null, e);
+ }
+ }
+
+ public T delete(String endpoint, Class clazz, Object... parameters) {
+ try {
+ try (Response response = client
+ .target(generateUrl(endpoint, parameters))
+ .request(MediaType.APPLICATION_JSON)
+ .delete()) {
+ if (response.getStatus() / 100 != 2) {
+ throw new APIException(generateUrl(endpoint, parameters), "DELETE", response.getStatus(), "Vocabulary server error", response.readEntity(VocabularyException.class), null);
+ }
+ return response.readEntity(clazz);
+ }
+ } catch (APIException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new APIException(generateUrl(endpoint, parameters), "DELETE", -1, e.getMessage(), null, e);
+ }
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyAPI.java b/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyAPI.java
new file mode 100644
index 0000000000..7fdeee933f
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyAPI.java
@@ -0,0 +1,66 @@
+package io.goobi.workflow.api.vocabulary;
+
+import io.goobi.vocabulary.exchange.Vocabulary;
+import io.goobi.workflow.api.vocabulary.hateoas.VocabularyPageResult;
+import io.goobi.workflow.api.vocabulary.helper.CachedLookup;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabulary;
+
+import javax.servlet.http.Part;
+import java.util.List;
+
+public class VocabularyAPI extends CRUDAPI {
+ private static final String COMMON_ENDPOINT = "/api/v1/vocabularies";
+ private static final String FIND_INSTANCE_ENDPOINT = "/api/v1/vocabularies/by-name/{{0}}";
+ private static final String IMPORT_CSV_ENDPOINT = "/api/v1/vocabularies/{{0}}/import/csv";
+ private static final String IMPORT_EXCEL_ENDPOINT = "/api/v1/vocabularies/{{0}}/import/excel";
+ private static final String INSTANCE_ENDPOINT = COMMON_ENDPOINT + "/{{0}}";
+
+ private final CachedLookup singleLookupCache;
+
+ public VocabularyAPI(String host, int port) {
+ super(host, port, Vocabulary.class, VocabularyPageResult.class, COMMON_ENDPOINT, INSTANCE_ENDPOINT);
+ this.singleLookupCache = new CachedLookup<>(id -> new ExtendedVocabulary(super.get(id)));
+ }
+
+ @Override
+ public ExtendedVocabulary get(long id) {
+ return this.singleLookupCache.getCached(id);
+ }
+
+ public List all() {
+ return restApi.get(COMMON_ENDPOINT, VocabularyPageResult.class, "all=1").getContent();
+ }
+
+ @Override
+ public ExtendedVocabulary change(Vocabulary vocabulary) {
+ ExtendedVocabulary result = new ExtendedVocabulary(super.change(vocabulary));
+ this.singleLookupCache.update(vocabulary.getId(), result);
+ return result;
+ }
+
+ @Override
+ public void delete(Vocabulary vocabulary) {
+ super.delete(vocabulary);
+ this.singleLookupCache.invalidate(vocabulary.getId());
+ }
+
+ public ExtendedVocabulary findByName(String name) {
+ return new ExtendedVocabulary(restApi.get(FIND_INSTANCE_ENDPOINT, Vocabulary.class, name));
+ }
+
+ public void cleanImportCsv(long id, Part uploadedFile) {
+ restApi.put(IMPORT_CSV_ENDPOINT, uploadedFile, id);
+ }
+
+ public void importCsv(long id, Part uploadedFile) {
+ restApi.post(IMPORT_CSV_ENDPOINT, uploadedFile, id);
+ }
+
+ public void cleanImportExcel(long id, Part uploadedFile) {
+ restApi.put(IMPORT_EXCEL_ENDPOINT, uploadedFile, id);
+ }
+
+ public void importExcel(long id, Part uploadedFile) {
+ restApi.post(IMPORT_EXCEL_ENDPOINT, uploadedFile, id);
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyAPIManager.java b/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyAPIManager.java
new file mode 100644
index 0000000000..e738ab690c
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyAPIManager.java
@@ -0,0 +1,50 @@
+package io.goobi.workflow.api.vocabulary;
+
+import de.sub.goobi.config.ConfigurationHelper;
+
+public class VocabularyAPIManager {
+ private static VocabularyAPIManager instance;
+
+ private FieldTypeAPI fieldTypeAPI;
+ private LanguageAPI languageAPI;
+ private VocabularySchemaAPI vocabularySchemaAPI;
+ private VocabularyAPI vocabularyAPI;
+ private VocabularyRecordAPI vocabularyRecordAPI;
+
+ private VocabularyAPIManager() {
+ final String host = ConfigurationHelper.getInstance().getVocabularyServerHost();
+ final int port = ConfigurationHelper.getInstance().getVocabularyServerPort();
+ this.fieldTypeAPI = new FieldTypeAPI(host, port);
+ this.languageAPI = new LanguageAPI(host, port);
+ this.vocabularySchemaAPI = new VocabularySchemaAPI(host, port);
+ this.vocabularyAPI = new VocabularyAPI(host, port);
+ this.vocabularyRecordAPI = new VocabularyRecordAPI(host, port);
+ }
+
+ public synchronized static VocabularyAPIManager getInstance() {
+ if (instance == null) {
+ instance = new VocabularyAPIManager();
+ }
+ return instance;
+ }
+
+ public FieldTypeAPI fieldTypes() {
+ return fieldTypeAPI;
+ }
+
+ public LanguageAPI languages() {
+ return languageAPI;
+ }
+
+ public VocabularySchemaAPI vocabularySchemas() {
+ return vocabularySchemaAPI;
+ }
+
+ public VocabularyAPI vocabularies() {
+ return vocabularyAPI;
+ }
+
+ public VocabularyRecordAPI vocabularyRecords() {
+ return vocabularyRecordAPI;
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyRecordAPI.java b/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyRecordAPI.java
new file mode 100644
index 0000000000..d1c44fe7e8
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/VocabularyRecordAPI.java
@@ -0,0 +1,285 @@
+package io.goobi.workflow.api.vocabulary;
+
+import io.goobi.vocabulary.exception.VocabularyException;
+import io.goobi.vocabulary.exchange.FieldInstance;
+import io.goobi.vocabulary.exchange.FieldValue;
+import io.goobi.vocabulary.exchange.TranslationInstance;
+import io.goobi.vocabulary.exchange.VocabularyRecord;
+import io.goobi.workflow.api.vocabulary.hateoas.VocabularyRecordPageResult;
+import io.goobi.workflow.api.vocabulary.helper.CachedLookup;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabulary;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabularyRecord;
+import lombok.extern.log4j.Log4j2;
+
+import javax.faces.model.SelectItem;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Log4j2
+public class VocabularyRecordAPI {
+ private static final String IN_VOCABULARY_RECORDS_ENDPOINT = "/api/v1/vocabularies/{{0}}/records";
+ private static final String METADATA_ENDPOINT = "/api/v1/vocabularies/{{0}}/metadata";
+ private static final String INSTANCE_ENDPOINT = "/api/v1/records/{{0}}";
+
+ private final RESTAPI restApi;
+ private final CachedLookup singleLookupCache;
+
+ public class VocabularyRecordQueryBuilder {
+ private final long vocabularyId;
+ private Optional page = Optional.empty();
+ private Optional pageSize = Optional.empty();
+ private Optional sorting = Optional.empty();
+ private Optional search = Optional.empty();
+ private Optional all = Optional.empty();
+
+ private VocabularyRecordQueryBuilder(long vocabularyId) {
+ this.vocabularyId = vocabularyId;
+ }
+
+ public VocabularyRecordQueryBuilder page(int page) {
+ this.page = Optional.of(page);
+ return this;
+ }
+
+ public VocabularyRecordQueryBuilder page(Optional page) {
+ this.page = page;
+ return this;
+ }
+
+ public VocabularyRecordQueryBuilder pageSize(int pageSize) {
+ this.pageSize = Optional.of(pageSize);
+ return this;
+ }
+
+ public VocabularyRecordQueryBuilder pageSize(Optional pageSize) {
+ this.pageSize = pageSize;
+ return this;
+ }
+
+ public VocabularyRecordQueryBuilder sorting(String sorting) {
+ this.sorting = Optional.of(sorting);
+ return this;
+ }
+
+ public VocabularyRecordQueryBuilder sorting(Optional sorting) {
+ this.sorting = sorting;
+ return this;
+ }
+
+ public VocabularyRecordQueryBuilder search(String search) {
+ this.search = Optional.of(search);
+ return this;
+ }
+
+ public VocabularyRecordQueryBuilder search(Optional search) {
+ this.search = search;
+ return this;
+ }
+
+ public VocabularyRecordQueryBuilder all() {
+ this.all = Optional.of(true);
+ return this;
+ }
+
+ public VocabularyRecordPageResult request() {
+ return list(this.vocabularyId, this.pageSize, this.page, this.sorting, this.search, this.all);
+ }
+ }
+
+ public VocabularyRecordAPI(String host, int port) {
+ this.restApi = new RESTAPI(host, port);
+ this.singleLookupCache = new CachedLookup<>(id -> {
+ VocabularyRecord r = restApi.get(INSTANCE_ENDPOINT, VocabularyRecord.class, id);
+ VocabularyAPIManager.getInstance().vocabularySchemas().load(r.getVocabularyId());
+ return new ExtendedVocabularyRecord(r);
+ });
+ }
+
+ public VocabularyRecordQueryBuilder list(long vocabularyId) {
+ return new VocabularyRecordQueryBuilder(vocabularyId);
+ }
+
+ private VocabularyRecordPageResult list(long vocabularyId, Optional size, Optional page, Optional sorting, Optional search, Optional all) {
+ String params = "";
+ if (size.isPresent()) {
+ params += params.isEmpty() ? "?" : "&";
+ params += "size=" + size.get();
+ }
+ if (page.isPresent()) {
+ params += params.isEmpty() ? "?" : "&";
+ params += "page=" + page.get();
+ }
+ if (sorting.isPresent()) {
+ params += params.isEmpty() ? "?" : "&";
+ params += "sort=" + sorting.get();
+ }
+ if (search.isPresent()) {
+ params += params.isEmpty() ? "?" : "&";
+ params += "search=" + search.get();
+ }
+ if (all.isPresent() && Boolean.TRUE.equals(all.get())) {
+ params += params.isEmpty() ? "?" : "&";
+ params += "all=1";
+ }
+ // Load schema to populate definition resolver
+ VocabularyAPIManager.getInstance().vocabularySchemas().load(vocabularyId);
+
+ VocabularyRecordPageResult result = restApi.get(IN_VOCABULARY_RECORDS_ENDPOINT + params, VocabularyRecordPageResult.class, vocabularyId);
+ result.getContent().forEach(r -> this.singleLookupCache.update(r.getId(), r));
+ return result;
+ }
+
+ public ExtendedVocabularyRecord get(long id) {
+ return singleLookupCache.getCached(id);
+ }
+
+ public ExtendedVocabularyRecord get(String url) {
+ return new ExtendedVocabularyRecord(restApi.get(url, VocabularyRecord.class));
+ }
+
+ public ExtendedVocabularyRecord save(VocabularyRecord vocabularyRecord) {
+ cleanUpRecord(vocabularyRecord);
+ if (Boolean.TRUE.equals(vocabularyRecord.getMetadata())) {
+ return changeMetadata(vocabularyRecord);
+ }
+ if (vocabularyRecord.getId() != null) {
+ return change(vocabularyRecord);
+ } else {
+ return create(vocabularyRecord);
+ }
+ }
+
+ private ExtendedVocabularyRecord create(VocabularyRecord vocabularyRecord) {
+ long vocabularyId = vocabularyRecord.getVocabularyId();
+ vocabularyRecord.setVocabularyId(null);
+ ExtendedVocabularyRecord newRecord;
+ if (vocabularyRecord.getParentId() == null) {
+ newRecord = new ExtendedVocabularyRecord(restApi.post(IN_VOCABULARY_RECORDS_ENDPOINT, VocabularyRecord.class, vocabularyRecord, vocabularyId));
+ } else {
+ long parentId = vocabularyRecord.getParentId();
+ vocabularyRecord.setParentId(null);
+ newRecord = new ExtendedVocabularyRecord(restApi.post(INSTANCE_ENDPOINT, VocabularyRecord.class, vocabularyRecord, parentId));
+ vocabularyRecord.setParentId(parentId);
+ this.singleLookupCache.invalidate(vocabularyRecord.getParentId());
+ }
+ vocabularyRecord.setId(vocabularyId);
+ this.singleLookupCache.update(newRecord.getId(), newRecord);
+ return newRecord;
+ }
+
+ private ExtendedVocabularyRecord change(VocabularyRecord vocabularyRecord) {
+ long id = vocabularyRecord.getId();
+ vocabularyRecord.setId(null);
+ ExtendedVocabularyRecord newRecord = new ExtendedVocabularyRecord(restApi.put(INSTANCE_ENDPOINT, VocabularyRecord.class, vocabularyRecord, id));
+ vocabularyRecord.setId(id);
+ this.singleLookupCache.update(newRecord.getId(), newRecord);
+ return newRecord;
+ }
+
+ public void delete(VocabularyRecord vocabularyRecord) {
+ restApi.delete(INSTANCE_ENDPOINT, VocabularyRecord.class, vocabularyRecord.getId());
+ this.singleLookupCache.invalidate(vocabularyRecord.getId());
+ if (vocabularyRecord.getParentId() != null) {
+ this.singleLookupCache.invalidate(vocabularyRecord.getParentId());
+ }
+ }
+
+ public ExtendedVocabularyRecord getMetadata(Long id) {
+ // TODO: Make this more elegant
+ try {
+ return new ExtendedVocabularyRecord(restApi.get(METADATA_ENDPOINT, VocabularyRecord.class, id));
+ } catch (APIException e) {
+ // Throw all exceptions that are not missing metadata, ignore missing metadata and just continue with empty record creation
+ if (e.getVocabularyCause() == null || e.getVocabularyCause().getErrorType() != VocabularyException.ErrorCode.EntityNotFound) {
+ throw e;
+ }
+ }
+ ExtendedVocabulary vocabulary = VocabularyAPIManager.getInstance().vocabularies().get(id);
+ return createEmptyRecord(vocabulary.getId(), null, true);
+ }
+
+ private ExtendedVocabularyRecord changeMetadata(VocabularyRecord metadataRecord) {
+ if (!Boolean.TRUE.equals(metadataRecord.getMetadata())) {
+ throw new IllegalArgumentException("Cannot perform changeMetadata on normal record");
+ }
+ Long id = metadataRecord.getId();
+ long vocabularyId = metadataRecord.getVocabularyId();
+ metadataRecord.setId(null);
+ metadataRecord.setVocabularyId(null);
+ metadataRecord.setMetadata(null);
+ ExtendedVocabularyRecord newRecord;
+ if (id == null) {
+ newRecord = new ExtendedVocabularyRecord(restApi.post(METADATA_ENDPOINT, VocabularyRecord.class, metadataRecord, vocabularyId));
+ } else {
+ newRecord = new ExtendedVocabularyRecord(restApi.put(METADATA_ENDPOINT, VocabularyRecord.class, metadataRecord, vocabularyId));
+ }
+ metadataRecord.setId(id);
+ metadataRecord.setVocabularyId(vocabularyId);
+ metadataRecord.setMetadata(true);
+ return newRecord;
+ }
+
+ public ExtendedVocabularyRecord createEmptyRecord(long vocabularyId, Long parentId, boolean metadata) {
+ VocabularyRecord record = new VocabularyRecord();
+ record.setVocabularyId(vocabularyId);
+ record.setParentId(parentId);
+ record.setMetadata(metadata);
+ record.setFields(new HashSet<>());
+ return new ExtendedVocabularyRecord(record);
+ }
+
+ private void cleanUpRecord(VocabularyRecord currentRecord) {
+ for (FieldInstance field : currentRecord.getFields()) {
+ for (FieldValue value : field.getValues()) {
+ value.getTranslations().removeIf(this::translationIsEmpty);
+ }
+ field.getValues().removeIf(this::valueIsEmpty);
+ }
+ currentRecord.getFields().removeIf(this::fieldIsEmpty);
+ }
+
+ private boolean translationIsEmpty(TranslationInstance translationInstance) {
+ return translationInstance.getValue().isEmpty();
+ }
+
+ private boolean valueIsEmpty(FieldValue fieldValue) {
+ return fieldValue.getTranslations().isEmpty();
+ }
+
+ private boolean fieldIsEmpty(FieldInstance fieldInstance) {
+ return fieldInstance.getValues().isEmpty();
+ }
+
+ public List getRecordMainValues(long vocabularyId) {
+ return getRecordMainValues(list(vocabularyId));
+ }
+
+ public List getRecordMainValues(VocabularyRecordQueryBuilder query) {
+ return query
+ .all()
+ .request()
+ .getContent()
+ .stream()
+ .map(ExtendedVocabularyRecord::getMainValue)
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+ public List getRecordSelectItems(long vocabularyId) {
+ return getRecordSelectItems(list(vocabularyId));
+ }
+
+ public List getRecordSelectItems(VocabularyRecordQueryBuilder query) {
+ return query
+ .all()
+ .request()
+ .getContent()
+ .stream()
+ .map(r -> new SelectItem(String.valueOf(r.getId()), r.getMainValue()))
+ .sorted(Comparator.comparing(SelectItem::getLabel))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/VocabularySchemaAPI.java b/src/main/java/io/goobi/workflow/api/vocabulary/VocabularySchemaAPI.java
new file mode 100644
index 0000000000..a3693f9031
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/VocabularySchemaAPI.java
@@ -0,0 +1,64 @@
+package io.goobi.workflow.api.vocabulary;
+
+import io.goobi.vocabulary.exchange.FieldDefinition;
+import io.goobi.vocabulary.exchange.Vocabulary;
+import io.goobi.vocabulary.exchange.VocabularyRecord;
+import io.goobi.vocabulary.exchange.VocabularySchema;
+import io.goobi.workflow.api.vocabulary.hateoas.VocabularySchemaPageResult;
+import io.goobi.workflow.api.vocabulary.helper.CachedLookup;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class VocabularySchemaAPI extends CRUDAPI {
+ private static final String COMMON_ENDPOINT = "/api/v1/schemas";
+ private static final String INSTANCE_ENDPOINT = COMMON_ENDPOINT + "/{{0}}";
+
+ // TODO: Make this generic
+ private Map definitionMap = new HashMap<>();
+ private final CachedLookup singleLookupCache;
+
+ public VocabularySchemaAPI(String host, int port) {
+ super(host, port, VocabularySchema.class, VocabularySchemaPageResult.class, COMMON_ENDPOINT, INSTANCE_ENDPOINT);
+ this.singleLookupCache = new CachedLookup<>(this::request);
+ }
+
+ @Override
+ public VocabularySchema get(long id) {
+ return this.singleLookupCache.getCached(id);
+ }
+
+ private VocabularySchema request(long id) {
+ VocabularySchema schema = super.get(id);
+ schema.getDefinitions().forEach(d -> definitionMap.put(d.getId(), d));
+ return schema;
+ }
+
+ public FieldDefinition getDefinition(Long definitionId) {
+ return this.definitionMap.get(definitionId);
+ }
+
+ public VocabularySchema getSchema(VocabularyRecord vocabularyRecord) {
+ return getSchema(VocabularyAPIManager.getInstance().vocabularies().get(vocabularyRecord.getVocabularyId()));
+ }
+
+ public VocabularySchema getSchema(Vocabulary vocabulary) {
+ return get(vocabulary.getSchemaId());
+ }
+
+ public Optional getMetadataSchema(VocabularyRecord vocabularyRecord) {
+ return getMetadataSchema(VocabularyAPIManager.getInstance().vocabularies().get(vocabularyRecord.getVocabularyId()));
+ }
+
+ public Optional getMetadataSchema(Vocabulary vocabulary) {
+ if (vocabulary.getMetadataSchemaId() == null) {
+ return Optional.empty();
+ }
+ return Optional.of(get(vocabulary.getMetadataSchemaId()));
+ }
+
+ public void load(long vocabularyId) {
+ getSchema(VocabularyAPIManager.getInstance().vocabularies().get(vocabularyId));
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/BasePageResult.java b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/BasePageResult.java
new file mode 100644
index 0000000000..7b9dc4e413
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/BasePageResult.java
@@ -0,0 +1,14 @@
+package io.goobi.workflow.api.vocabulary.hateoas;
+
+import io.goobi.vocabulary.exchange.HateoasHref;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public abstract class BasePageResult {
+ private Map _links;
+ private PageInformation page;
+ public abstract List getContent();
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/FieldTypePageResult.java b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/FieldTypePageResult.java
new file mode 100644
index 0000000000..40ef8064cd
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/FieldTypePageResult.java
@@ -0,0 +1,26 @@
+package io.goobi.workflow.api.vocabulary.hateoas;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.goobi.vocabulary.exchange.FieldType;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class FieldTypePageResult extends BasePageResult {
+ @Data
+ private class EmbeddedWrapper {
+ private List fieldTypeList;
+ }
+
+ private EmbeddedWrapper _embedded;
+
+ public List getContent() {
+ if (_embedded == null) {
+ return Collections.emptyList();
+ }
+ return _embedded.getFieldTypeList();
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/HATEOASPaginator.java b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/HATEOASPaginator.java
new file mode 100644
index 0000000000..574ce83e30
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/HATEOASPaginator.java
@@ -0,0 +1,368 @@
+package io.goobi.workflow.api.vocabulary.hateoas;
+
+import io.goobi.vocabulary.exception.VocabularyException;
+import io.goobi.vocabulary.exchange.Identifiable;
+import io.goobi.workflow.api.vocabulary.APIException;
+import lombok.Data;
+import lombok.Setter;
+import org.apache.commons.lang3.NotImplementedException;
+import org.goobi.managedbeans.Paginator;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class HATEOASPaginator> implements Paginator {
+ public static final String NAVIGATE_PREVIOUS = "prev";
+ public static final String NAVIGATE_NEXT = "next";
+ public static final String NAVIGATE_FIRST = "first";
+ public static final String NAVIGATE_LAST = "last";
+
+ @Data
+ class Node {
+ private Long id;
+ private T data;
+ private List children;
+ private boolean visible = false;
+
+ public void load() {
+ if (data != null) {
+ return;
+ }
+
+ data = postLoader.apply(getId());
+ prepareChildren();
+ }
+
+ private void prepareChildren() {
+ if (getData() != null) {
+ childrenExtractor.ifPresent(e -> {
+ Collection extractedChildren = e.apply(getData());
+ if (extractedChildren != null) {
+ setChildren(new LinkedList<>(
+ extractedChildren.stream().map(c -> {
+ Node child = new Node();
+ child.setId(c);
+ return child;
+ }).collect(Collectors.toList())));
+ }
+ });
+ }
+ if (getChildren() == null) {
+ setChildren(new LinkedList<>());
+ }
+ }
+ }
+
+ @Setter
+ private Client client = ClientBuilder.newClient();
+ private final Optional>> childrenExtractor;
+ private final Optional> parentExtractor;
+ private final Function postLoader;
+ private Optional searchParameter = Optional.empty();
+ private Optional sortField = Optional.empty();
+
+ private Class pageClass;
+ private PageT currentPage;
+ private List tree = new LinkedList<>();
+ private Map treeMap = new HashMap<>();
+ private List items = new LinkedList<>();
+
+ public HATEOASPaginator(Class pageClass, PageT initialPage, Function> childrenExtractor, Function parentExtractor, Function postLoader) {
+ this.pageClass = pageClass;
+ this.childrenExtractor = Optional.ofNullable(childrenExtractor);
+ this.parentExtractor = Optional.ofNullable(parentExtractor);
+ this.postLoader = postLoader;
+ setCurrentPage(initialPage);
+ }
+
+ private void setCurrentPage(PageT page) {
+ this.currentPage = page;
+ this.tree.clear();
+ this.currentPage.getContent().forEach(this::insertElement);
+ this.tree.forEach(this::recursiveShow);
+ rebuildTree();
+ }
+
+ private void recursiveShow(Node node) {
+ if (node.getData() == null) {
+ return;
+ }
+ node.setVisible(true);
+ if (node.getChildren() != null) {
+ node.getChildren().forEach(this::recursiveShow);
+ }
+ }
+
+ private void rebuildTree() {
+ this.items.clear();
+ this.treeMap.clear();
+ this.tree.stream()
+ .filter(Node::isVisible)
+ .forEachOrdered(this::addToFlatList);
+ }
+
+ private void addToFlatList(Node node) {
+ if (node.isVisible() && node.getData() != null) {
+ this.items.add(node.getData());
+ }
+ this.treeMap.put(node.getId(), node);
+ if (node.getData() != null) {
+ node.getChildren().forEach(this::addToFlatList);
+ }
+ }
+
+ public void expand(T entry) {
+ Node node = this.treeMap.get(entry.getId());
+ node.getChildren().forEach(child -> {
+ child.load();
+ child.setVisible(true);
+ });
+ rebuildTree();
+ }
+
+ public void collapse(T entry) {
+ Node node = this.treeMap.get(entry.getId());
+ this.recursiveCollapseChildren(node);
+ rebuildTree();
+ }
+
+ private void recursiveCollapseChildren(Node node) {
+ if (node.getChildren() == null) {
+ return;
+ }
+ node.getChildren().forEach(child -> {
+ child.setVisible(false);
+ this.recursiveCollapseChildren(child);
+ });
+ }
+
+ private void recursiveParentExpanding(Node node) {
+ node.load();
+ node.setVisible(true);
+ this.parentExtractor.ifPresent(e -> {
+ Long parentId = e.apply(node.getData());
+ if (parentId == null) {
+ return;
+ }
+ Node parent = getOrCreateParent(parentId);
+ parent.setVisible(true);
+ recursiveParentExpanding(parent);
+ });
+ }
+
+ public boolean isExpanded(T entry) {
+ List children = this.treeMap.get(entry.getId()).getChildren();
+ if (children == null) {
+ return false;
+ }
+ return children.stream().anyMatch(Node::isVisible);
+ }
+
+ public void setSearchParameter(String searchParameter) {
+ if (searchParameter.isBlank()) {
+ searchParameter = null;
+ }
+ this.searchParameter = Optional.ofNullable(searchParameter);
+ }
+
+ public String getSearchParameter() {
+ return searchParameter.orElse(null);
+ }
+
+ public void setSortField(String sortField) {
+ if (sortField.isBlank()) {
+ sortField = null;
+ }
+ this.sortField = Optional.ofNullable(sortField);
+ }
+
+ public String getSortField() {
+ return sortField.orElse(null);
+ }
+
+ private void request(String url) {
+ request(url, Optional.empty(), Optional.empty());
+ }
+
+ private void request(String url, Optional pageSize, Optional pageNumber) {
+ url = updatePageAndSizeUrlParameters(url, pageSize, pageNumber, sortField, searchParameter);
+ try (Response response = client
+ .target(url)
+ .request(MediaType.APPLICATION_JSON)
+ .get()) {
+ if (response.getStatus() / 100 != 2) {
+ throw new APIException(url, "GET", response.getStatus(), "Vocabulary server error", response.readEntity(VocabularyException.class), null);
+ }
+ setCurrentPage(response.readEntity(pageClass));
+ }
+ }
+
+ private static String updatePageAndSizeUrlParameters(String url, Optional pageSize, Optional pageNumber, Optional sortField, Optional searchParameter) {
+ Map parameters = new HashMap<>();
+ int questionMarkIndex = url.indexOf('?');
+ if (questionMarkIndex > 0) {
+ String[] parts = url.substring(questionMarkIndex + 1).split("&");
+ for (String part : parts) {
+ String[] keyValue = part.split("=");
+ if (keyValue.length == 2) {
+ parameters.put(keyValue[0], keyValue[1]);
+ }
+ }
+ }
+ pageSize.ifPresent(value -> parameters.put("size", String.valueOf(value)));
+ pageNumber.ifPresent(value -> parameters.put("page", String.valueOf(value)));
+ sortField.ifPresent(s -> parameters.put("sort", s));
+ searchParameter.ifPresent(s -> parameters.put("search", s));
+ if (searchParameter.isEmpty()) {
+ parameters.remove("search");
+ }
+ if (questionMarkIndex < 0) {
+ url += "?";
+ questionMarkIndex = url.length() - 1;
+ }
+ url = url.substring(0, questionMarkIndex + 1) + parameters.entrySet().stream()
+ .map(e -> e.getKey() + "=" + e.getValue())
+ .collect(Collectors.joining("&"));
+ return url;
+ }
+
+ @Override
+ public void reload() {
+ request(currentPage.get_links().get("self").getHref(), Optional.empty(), Optional.of(0L));
+ }
+
+ @Override
+ public void postLoad(T item) {
+ this.innerPostLoad(item);
+ this.rebuildTree();
+ }
+
+ private void innerPostLoad(T item) {
+ if (!this.treeMap.containsKey(item.getId())) {
+ this.insertElement(item);
+ }
+
+ Node node = this.treeMap.get(item.getId());
+ node.load();
+ node.setVisible(true);
+ recursiveParentExpanding(node);
+ }
+
+ private void insertElement(T item) {
+ Long parentId = null;
+ if (this.parentExtractor.isPresent()) {
+ parentId = this.parentExtractor.get().apply(item);
+ }
+
+ Node node = new Node();
+ node.setId(item.getId());
+ node.setData(item);
+ node.prepareChildren();
+
+ if (parentId == null) {
+ this.tree.add(node);
+ } else {
+ getOrCreateParent(parentId).getChildren().add(node);
+ }
+ this.treeMap.putIfAbsent(item.getId(), node);
+ }
+
+ private Node getOrCreateParent(Long parentId) {
+ if (!this.treeMap.containsKey(parentId)) {
+ T parentData = postLoader.apply(parentId);
+ insertElement(parentData);
+ }
+ Node result = this.treeMap.get(parentId);
+ result.load();
+ return result;
+ }
+
+ @Override
+ public long getCurrentPage() {
+ return currentPage.getPage().getNumber() + 1;
+ }
+
+ @Override
+ public void setCurrentPage(long page) {
+ if (page != getCurrentPage()) {
+ if (page > getNumberOfPages()) {
+ page = getNumberOfPages();
+ }
+ if (page < 1) {
+ page = 1;
+ }
+ request(currentPage.get_links().get(NAVIGATE_FIRST).getHref(), Optional.of(currentPage.getPage().getSize()), Optional.of(page - 1));
+ }
+ }
+
+ @Override
+ public List getItems() {
+ return this.items;
+ }
+
+ @Override
+ public long getPageSize() {
+ return currentPage.getPage().getSize();
+ }
+
+ @Override
+ public void setPageSize(long pageSize) {
+ throw new NotImplementedException("to be done");
+ }
+
+ @Override
+ public long getTotalResults() {
+ return currentPage.getPage().getTotalElements();
+ }
+
+ @Override
+ public long getNumberOfPages() {
+ return currentPage.getPage().getTotalPages();
+ }
+
+ @Override
+ public boolean hasPreviousPage() {
+ return currentPage.get_links().containsKey(NAVIGATE_PREVIOUS);
+ }
+
+ @Override
+ public boolean hasNextPage() {
+ return currentPage.get_links().containsKey(NAVIGATE_NEXT);
+ }
+
+ @Override
+ public void cmdMoveFirst() {
+ request(currentPage.get_links().get(NAVIGATE_FIRST).getHref());
+ }
+
+ @Override
+ public void cmdMoveLast() {
+ request(currentPage.get_links().get(NAVIGATE_LAST).getHref());
+ }
+
+ @Override
+ public void cmdMoveNext() {
+ if (!hasNextPage()) {
+ return;
+ }
+ request(currentPage.get_links().get(NAVIGATE_NEXT).getHref());
+ }
+
+ @Override
+ public void cmdMovePrevious() {
+ if (!hasPreviousPage()) {
+ return;
+ }
+ request(currentPage.get_links().get(NAVIGATE_PREVIOUS).getHref());
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/LanguagePageResult.java b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/LanguagePageResult.java
new file mode 100644
index 0000000000..836eb25334
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/LanguagePageResult.java
@@ -0,0 +1,26 @@
+package io.goobi.workflow.api.vocabulary.hateoas;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.goobi.vocabulary.exchange.Language;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class LanguagePageResult extends BasePageResult {
+ @Data
+ private class EmbeddedWrapper {
+ private List languageList;
+ }
+
+ private EmbeddedWrapper _embedded;
+
+ public List getContent() {
+ if (_embedded == null) {
+ return Collections.emptyList();
+ }
+ return _embedded.getLanguageList();
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/PageInformation.java b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/PageInformation.java
new file mode 100644
index 0000000000..3446643295
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/PageInformation.java
@@ -0,0 +1,11 @@
+package io.goobi.workflow.api.vocabulary.hateoas;
+
+import lombok.Data;
+
+@Data
+public class PageInformation {
+ private long size;
+ private long totalElements;
+ private long totalPages;
+ private long number;
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularyPageResult.java b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularyPageResult.java
new file mode 100644
index 0000000000..a08736a57d
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularyPageResult.java
@@ -0,0 +1,36 @@
+package io.goobi.workflow.api.vocabulary.hateoas;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.goobi.vocabulary.exchange.Vocabulary;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabulary;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class VocabularyPageResult extends BasePageResult {
+ @Data
+ private class EmbeddedWrapper {
+ private List vocabularyList;
+ }
+
+ private List content;
+
+ private EmbeddedWrapper _embedded;
+
+ public List getContent() {
+ if (content == null) {
+ if (_embedded == null) {
+ this.content = Collections.emptyList();
+ } else {
+ this.content = _embedded.getVocabularyList().stream()
+ .map(ExtendedVocabulary::new)
+ .collect(Collectors.toList());
+ }
+ }
+ return this.content;
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularyRecordPageResult.java b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularyRecordPageResult.java
new file mode 100644
index 0000000000..0e3b1b04c8
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularyRecordPageResult.java
@@ -0,0 +1,36 @@
+package io.goobi.workflow.api.vocabulary.hateoas;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.goobi.vocabulary.exchange.VocabularyRecord;
+import io.goobi.workflow.api.vocabulary.helper.ExtendedVocabularyRecord;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class VocabularyRecordPageResult extends BasePageResult {
+ @Data
+ private class EmbeddedWrapper {
+ private List vocabularyRecordList;
+ }
+
+ private EmbeddedWrapper _embedded;
+
+ private List content;
+
+ public List getContent() {
+ if (content == null) {
+ if (_embedded == null) {
+ this.content = Collections.emptyList();
+ } else {
+ this.content = _embedded.getVocabularyRecordList().stream()
+ .map(ExtendedVocabularyRecord::new)
+ .collect(Collectors.toList());
+ }
+ }
+ return this.content;
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularySchemaPageResult.java b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularySchemaPageResult.java
new file mode 100644
index 0000000000..5a6eb4bc37
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/hateoas/VocabularySchemaPageResult.java
@@ -0,0 +1,26 @@
+package io.goobi.workflow.api.vocabulary.hateoas;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.goobi.vocabulary.exchange.VocabularySchema;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class VocabularySchemaPageResult extends BasePageResult {
+ @Data
+ private class EmbeddedWrapper {
+ private List vocabularySchemaList;
+ }
+
+ private EmbeddedWrapper _embedded;
+
+ public List getContent() {
+ if (_embedded == null) {
+ return Collections.emptyList();
+ }
+ return _embedded.getVocabularySchemaList();
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/helper/APIExceptionExtractor.java b/src/main/java/io/goobi/workflow/api/vocabulary/helper/APIExceptionExtractor.java
new file mode 100644
index 0000000000..d1d85baed2
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/helper/APIExceptionExtractor.java
@@ -0,0 +1,94 @@
+package io.goobi.workflow.api.vocabulary.helper;
+
+import de.sub.goobi.helper.Helper;
+import io.goobi.vocabulary.exception.VocabularyException;
+import io.goobi.workflow.api.vocabulary.APIException;
+import lombok.extern.log4j.Log4j2;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Log4j2
+public class APIExceptionExtractor {
+ private final static String MESSAGE_PREFIX = "vocabularyManager_exception_";
+ private final APIException exception;
+
+ public APIExceptionExtractor(APIException exception) {
+ this.exception = exception;
+ }
+
+ public String getLocalizedMessage(Locale locale) {
+ return Optional.ofNullable(exception.getVocabularyCause())
+ .map(ex -> extractLocalizedVocabularyMessage(ex, locale)).orElse(exception.getMessage());
+ }
+
+ private String extractLocalizedVocabularyMessage(VocabularyException ex, Locale locale) {
+ String currentLevelMessage = getLocalizedMessage(ex.getErrorType(), Optional.ofNullable(ex.getParams()).orElse(Collections.emptyMap()))
+ .orElse("");
+ List causeLevelMessages = Optional.ofNullable(ex.getCauses())
+ .map(Collection::stream)
+ .map(s -> s.map(e -> extractLocalizedVocabularyMessage(e, locale))
+ .filter(m -> !m.isBlank())
+ .collect(Collectors.toList()))
+ .orElse(Collections.emptyList());
+ String message = currentLevelMessage;
+ if (!causeLevelMessages.isEmpty()) {
+ message += (message.isBlank() ? "" : "\n") + String.join("\n", causeLevelMessages);
+ }
+ return message;
+ }
+
+ private Optional getLocalizedMessage(VocabularyException.ErrorCode errorType, Map params) {
+ String message = null;
+ String messageKey = MESSAGE_PREFIX + errorType.toString();
+ List orderedParameters = null;
+ switch (errorType) {
+ case DataIntegrityViolation:
+ orderedParameters = List.of("reason");
+ break;
+ case EntityNotFound:
+ orderedParameters = List.of("type", "id");
+ break;
+ case FieldValueIsNotUnique:
+ orderedParameters = List.of("definitionName", "duplicateValues");
+ break;
+ case FieldValuesDoNotMatchSpecifiedValidationRegex:
+ orderedParameters = List.of("definitionName", "values", "regex");
+ break;
+ case RecordValidationMissingRequiredFields:
+ orderedParameters = List.of("missingFieldNames");
+ break;
+ case RecordImportUnsupportedExcelCellType:
+ orderedParameters = List.of("cellType");
+ break;
+ case DeletionOfReferencedVocabulary:
+ orderedParameters = List.of("vocabularyId", "referencingVocabularyIds");
+ break;
+ case DeletionOfReferencedVocabularyRecord:
+ if (params.containsKey("referencingRecordIds")) {
+ orderedParameters = List.of("recordId", "referencingRecordIds");
+ }
+ break;
+ default:
+ log.warn("No translation for vocabulary exception type \"{}\" given, parameters: {}", errorType, params);
+ orderedParameters = List.of(errorType.toString());
+ break;
+ }
+ if (orderedParameters == null) {
+ return Optional.empty();
+ }
+
+ String[] rawParams = new String[orderedParameters.size()];
+ orderedParameters.stream()
+ .map(params::get)
+ .collect(Collectors.toList())
+ .toArray(rawParams);
+ message = Helper.getTranslation(messageKey, rawParams);
+ return Optional.ofNullable(message);
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/helper/CachedLookup.java b/src/main/java/io/goobi/workflow/api/vocabulary/helper/CachedLookup.java
new file mode 100644
index 0000000000..84e096d1a0
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/helper/CachedLookup.java
@@ -0,0 +1,58 @@
+package io.goobi.workflow.api.vocabulary.helper;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+public class CachedLookup {
+ private static final long MAX_DATA_AGE_IN_MS = 5000L;
+
+ @Data
+ @AllArgsConstructor
+ private class AgingData {
+ private final long timestamp;
+ private final T data;
+
+ public long getAge() {
+ return System.currentTimeMillis() - getTimestamp();
+ }
+ }
+
+ private Map cache = new HashMap<>();
+ private Function lookupFunction;
+
+ public CachedLookup(Function lookupFunction) {
+ this.lookupFunction = lookupFunction;
+ }
+
+ public synchronized T getCached(K key) {
+ if (!cache.containsKey(key)) {
+ return insert(key);
+ }
+ AgingData agingData = cache.get(key);
+ if (agingData.getAge() > MAX_DATA_AGE_IN_MS) {
+ return insert(key);
+ }
+ return agingData.getData();
+ }
+
+ public synchronized T update(K key, T item) {
+ return insert(key, item);
+ }
+
+ public synchronized void invalidate(K key) {
+ this.cache.remove(key);
+ }
+
+ private T insert(K key) {
+ return insert(key, this.lookupFunction.apply(key));
+ }
+
+ private T insert(K key, T data) {
+ cache.put(key, new AgingData(System.currentTimeMillis(), data));
+ return data;
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedFieldInstance.java b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedFieldInstance.java
new file mode 100644
index 0000000000..93774b5fdb
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedFieldInstance.java
@@ -0,0 +1,206 @@
+package io.goobi.workflow.api.vocabulary.helper;
+
+import de.sub.goobi.helper.Helper;
+import io.goobi.vocabulary.exchange.FieldDefinition;
+import io.goobi.vocabulary.exchange.FieldInstance;
+import io.goobi.vocabulary.exchange.FieldType;
+import io.goobi.vocabulary.exchange.FieldValue;
+import io.goobi.vocabulary.exchange.TranslationDefinition;
+import io.goobi.vocabulary.exchange.TranslationInstance;
+import io.goobi.workflow.api.vocabulary.VocabularyAPIManager;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j2;
+import org.goobi.managedbeans.FormInputMultiSelectBean;
+import org.goobi.managedbeans.FormInputMultiSelectHelper;
+
+import javax.faces.model.SelectItem;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import static io.goobi.workflow.api.vocabulary.helper.ExtendedTranslationInstance.transformToThreeCharacterAbbreviation;
+
+@Getter
+@Log4j2
+public class ExtendedFieldInstance extends FieldInstance {
+ private Function recordResolver = VocabularyAPIManager.getInstance().vocabularyRecords()::get;
+ private Function> recordsResolver = this::getAllRecords;
+ private Function definitionResolver = VocabularyAPIManager.getInstance().vocabularySchemas()::getDefinition;
+ private Function typeResolver = VocabularyAPIManager.getInstance().fieldTypes()::get;
+ private Supplier languageSupplier = Helper.getLanguageBean().getLocale()::getLanguage;
+
+ private FieldDefinition definition;
+ private FieldType type;
+
+ private FormInputMultiSelectBean selectionBean;
+ private List selectableItems;
+
+ private List getAllRecords(Long vocabularyId) {
+ return VocabularyAPIManager.getInstance().vocabularyRecords()
+ .list(vocabularyId)
+ .all()
+ .request()
+ .getContent();
+ }
+
+ public ExtendedFieldInstance(FieldInstance orig) {
+ setId(orig.getId());
+ setRecordId(orig.getRecordId());
+ setDefinitionId(orig.getDefinitionId());
+ setValues(orig.getValues());
+
+ postInit();
+ prepareEmpty();
+ sortTranslations();
+ }
+
+ private void postInit() {
+ this.definition = definitionResolver.apply(getDefinitionId());
+ if (this.definition.getTypeId() != null) {
+ this.type = typeResolver.apply(this.definition.getTypeId());
+ }
+ if (this.type != null) {
+ this.selectableItems = this.type.getSelectableValues().stream()
+ .map(v -> new SelectItem(v, v))
+ .collect(Collectors.toList());
+ } else if (this.definition.getReferenceVocabularyId() != null) {
+ this.selectableItems = recordsResolver.apply(this.definition.getReferenceVocabularyId()).stream()
+ .map(r -> new SelectItem(Long.toString(r.getId()), r.getMainValue()))
+ .collect(Collectors.toList());
+ }
+ this.selectionBean = new FormInputMultiSelectHelper(() -> this.selectableItems, this::getSelection, this::setSelection);
+ }
+
+ private void prepareEmpty() {
+ if (getValues().isEmpty()) {
+ getValues().add(new ExtendedFieldValue(new FieldValue(), definition.getTranslationDefinitions()));
+ }
+ }
+
+ public FormInputMultiSelectBean getSelectionBean() {
+ return this.selectionBean;
+ }
+
+ public List getSelection() {
+ if (this.selectableItems.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List selection = new LinkedList<>();
+ for (String selectedValue : getValues().stream()
+ .flatMap(v -> v.getTranslations().stream())
+ .map(TranslationInstance::getValue)
+ .filter(v -> !v.isBlank() && !v.equals("null"))
+ .collect(Collectors.toList())) {
+ // TODO: Single selects are directly bound to value translations and might set "null"
+ SelectItem item = this.selectableItems.stream()
+ .filter(i -> i.getValue().equals(selectedValue))
+ .findFirst()
+ .orElseThrow();
+ selection.add(item);
+ }
+ return selection;
+ }
+
+ public void setSelection(List selection) {
+ List selectedValues = selection.stream()
+ .map(s -> (String) s.getValue())
+ .collect(Collectors.toList());
+ getValues().clear();
+ selectedValues.forEach(v -> {
+ FieldValue fieldValue = addFieldValue();
+ fieldValue.getTranslations().get(0).setValue(v);
+ });
+ }
+
+ private void sortTranslations() {
+ getValues().forEach(this::sortTranslations);
+ }
+
+ private void sortTranslations(FieldValue value) {
+ Collections.sort(value.getTranslations(), Comparator.comparing(TranslationInstance::getLanguage));
+ }
+
+ public FieldValue addFieldValue() {
+ FieldValue value = new ExtendedFieldValue(new FieldValue(), definition.getTranslationDefinitions());
+ getValues().add(value);
+ prepareEmpty();
+ sortTranslations(value);
+ return value;
+ }
+
+ public void deleteFieldValue(FieldValue value) {
+ getValues().remove(value);
+ }
+
+ public String getFieldValue() {
+ return getFieldValue(transformToThreeCharacterAbbreviation(this.languageSupplier.get()));
+ }
+
+ public String getFieldValue(String language) {
+ String value = extractValue(this, language);
+ if (!value.isBlank() && definition.getReferenceVocabularyId() != null) {
+ // Process multi-valued referenced fields
+ List ids = Arrays.stream(value.strip().split("\\|"))
+ .map(Long::parseLong)
+ .collect(Collectors.toList());
+ List values = new LinkedList<>();
+ for (long id : ids) {
+ ExtendedVocabularyRecord result = new ExtendedVocabularyRecord(recordResolver.apply(id));
+ values.add(result.getMainValue());
+ }
+ value = String.join("|", values);
+ }
+ return value;
+ }
+
+ public void setFieldValue(String value) {
+ getValues().clear();
+ FieldValue fieldValue = addFieldValue();
+ fieldValue.getTranslations().forEach(t -> t.setValue(value));
+ }
+
+ private String extractValue(FieldInstance field, String language) {
+ return field.getValues().stream()
+ .map(
+ v -> {
+ Optional preferredLanguage = v.getTranslations().stream()
+ .filter(t -> language != null && language.equals(t.getLanguage()))
+ .map(TranslationInstance::getValue)
+ .findFirst();
+ if (preferredLanguage.isPresent() && !preferredLanguage.get().isBlank()) {
+ return preferredLanguage.get();
+ }
+ String fallbackLanguage = definition.getTranslationDefinitions().stream()
+ .filter(t -> Boolean.TRUE.equals(t.getFallback()))
+ .map(TranslationDefinition::getLanguage)
+ .findFirst()
+ .orElse(null);
+ if (fallbackLanguage == null) {
+ return v.getTranslations().stream()
+ .filter(t -> t.getLanguage() == null)
+ .map(TranslationInstance::getValue)
+ .findFirst()
+ .orElseThrow();
+ }
+ return v.getTranslations().stream()
+ .filter(t -> fallbackLanguage.equals(t.getLanguage()))
+ .map(TranslationInstance::getValue)
+ .findFirst()
+ .orElseThrow();
+ }
+ ).collect(Collectors.joining("|"));
+ }
+
+ public List getExtendedValues() {
+ return getValues().stream()
+ .map(v -> new ExtendedFieldValue(v, definition.getTranslationDefinitions()))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedFieldValue.java b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedFieldValue.java
new file mode 100644
index 0000000000..306be5c668
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedFieldValue.java
@@ -0,0 +1,97 @@
+package io.goobi.workflow.api.vocabulary.helper;
+
+import io.goobi.vocabulary.exchange.FieldValue;
+import io.goobi.vocabulary.exchange.HateoasHref;
+import io.goobi.vocabulary.exchange.TranslationDefinition;
+import io.goobi.vocabulary.exchange.TranslationInstance;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j2;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Getter
+@Log4j2
+public class ExtendedFieldValue extends FieldValue {
+ private final FieldValue wrapped;
+ private List extendedTranslations;
+
+ ExtendedFieldValue(FieldValue orig, Set definitions) {
+ this.wrapped = orig;
+
+ postInit(definitions);
+ }
+
+ @Override
+ public Long getId() {
+ return wrapped.getId();
+ }
+
+ @Override
+ public void setId(Long id) {
+ wrapped.setId(id);
+ }
+
+ @Override
+ public Long getFieldId() {
+ return wrapped.getFieldId();
+ }
+
+ @Override
+ public void setFieldId(Long fieldId) {
+ wrapped.setFieldId(fieldId);
+ }
+
+ @Override
+ public List getTranslations() {
+ return wrapped.getTranslations();
+ }
+
+ @Override
+ public void setTranslations(List translations) {
+ wrapped.setTranslations(translations);
+ }
+
+ @Override
+ public Map get_links() {
+ return wrapped.get_links();
+ }
+
+ @Override
+ public void set_links(Map _links) {
+ wrapped.set_links(_links);
+ }
+
+ private void postInit(Set definitions) {
+ prepareEmpty(definitions);
+
+ Map lookup = definitions.stream()
+ .filter(d -> d.getLanguage() != null)
+ .collect(Collectors.toMap(TranslationDefinition::getLanguage, Function.identity()));
+ this.extendedTranslations = getTranslations().stream()
+ .map(t -> new ExtendedTranslationInstance(t, lookup.getOrDefault(t.getLanguage(), null)))
+ .sorted(Comparator.comparing(TranslationInstance::getLanguage))
+ .collect(Collectors.toList());
+ }
+
+ private void prepareEmpty(Set definitions) {
+ if (!definitions.isEmpty()) {
+ definitions.stream()
+ .filter(t -> getTranslations().stream().noneMatch(t2 -> t2.getLanguage().equals(t.getLanguage())))
+ .forEach(t -> {
+ TranslationInstance translation = new TranslationInstance();
+ translation.setLanguage(t.getLanguage());
+ translation.setValue("");
+ getTranslations().add(translation);
+ });
+ } else if (getTranslations().isEmpty()) {
+ TranslationInstance translation = new TranslationInstance();
+ translation.setValue("");
+ getTranslations().add(translation);
+ }
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedTranslationInstance.java b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedTranslationInstance.java
new file mode 100644
index 0000000000..4bf6944db1
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedTranslationInstance.java
@@ -0,0 +1,67 @@
+package io.goobi.workflow.api.vocabulary.helper;
+
+import io.goobi.vocabulary.exchange.Language;
+import io.goobi.vocabulary.exchange.TranslationDefinition;
+import io.goobi.vocabulary.exchange.TranslationInstance;
+import io.goobi.workflow.api.vocabulary.VocabularyAPIManager;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j2;
+
+import java.util.function.Function;
+
+@Getter
+@Log4j2
+public class ExtendedTranslationInstance extends TranslationInstance {
+ private Function languageNameResolver = VocabularyAPIManager.getInstance().languages()::findByAbbreviation;
+
+ private final TranslationInstance wrapped;
+ private TranslationDefinition definition;
+ private String languageName;
+
+ ExtendedTranslationInstance(TranslationInstance orig, TranslationDefinition definition) {
+ this.wrapped = orig;
+ this.definition = definition;
+
+ postInit();
+ }
+
+ @Override
+ public String getLanguage() {
+ return wrapped.getLanguage();
+ }
+
+ @Override
+ public void setLanguage(String language) {
+ wrapped.setLanguage(language);
+ }
+
+ @Override
+ public String getValue() {
+ return wrapped.getValue();
+ }
+
+ @Override
+ public void setValue(String value) {
+ wrapped.setValue(value);
+ }
+
+ private void postInit() {
+ if (getLanguage() != null) {
+ this.languageName = languageNameResolver.apply(getLanguage()).getName();
+ }
+ }
+
+ static String transformToThreeCharacterAbbreviation(String language) {
+ switch (language) {
+ case "en":
+ return "eng";
+ case "de":
+ return "ger";
+ case "fr":
+ return "fre";
+ default:
+ log.warn("Unknown language \"{}\", falling back to \"eng\"", language);
+ return "eng";
+ }
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedVocabulary.java b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedVocabulary.java
new file mode 100644
index 0000000000..379eef4e45
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedVocabulary.java
@@ -0,0 +1,52 @@
+package io.goobi.workflow.api.vocabulary.helper;
+
+import io.goobi.vocabulary.exchange.Vocabulary;
+import lombok.Getter;
+
+import java.util.Optional;
+
+public class ExtendedVocabulary extends Vocabulary {
+ @Getter
+ private boolean skosExportPossible;
+
+ public ExtendedVocabulary(Vocabulary orig) {
+ setId(orig.getId());
+ setSchemaId(orig.getSchemaId());
+ setMetadataSchemaId(orig.getMetadataSchemaId());
+ setName(orig.getName());
+ setDescription(orig.getDescription());
+ set_links(orig.get_links());
+
+ postInit();
+ }
+
+ private void postInit() {
+ this.skosExportPossible = Optional.ofNullable(get_links())
+ .map(m -> m.keySet().stream().anyMatch(link -> link.startsWith("export_rdf")))
+ .orElse(false);
+ }
+
+ public String getURI() {
+ return get_links().get("self").getHref();
+ }
+
+ public String rdfXmlExport() {
+ return get_links().get("export_rdf_xml").getHref();
+ }
+
+ public String rdfTurtleExport() {
+ return get_links().get("export_rdf_turtle").getHref();
+ }
+
+ public String jsonExport() {
+ return get_links().get("export_json").getHref();
+ }
+
+ public String csvExport() {
+ return get_links().get("export_csv").getHref();
+ }
+
+ public String excelExport() {
+ return get_links().get("export_excel").getHref();
+ }
+}
diff --git a/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedVocabularyRecord.java b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedVocabularyRecord.java
new file mode 100644
index 0000000000..3061d11040
--- /dev/null
+++ b/src/main/java/io/goobi/workflow/api/vocabulary/helper/ExtendedVocabularyRecord.java
@@ -0,0 +1,141 @@
+package io.goobi.workflow.api.vocabulary.helper;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.goobi.vocabulary.exchange.FieldDefinition;
+import io.goobi.vocabulary.exchange.FieldInstance;
+import io.goobi.vocabulary.exchange.VocabularyRecord;
+import io.goobi.vocabulary.exchange.VocabularySchema;
+import io.goobi.workflow.api.vocabulary.VocabularyAPIManager;
+import lombok.Getter;
+import ugh.dl.Metadata;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Getter
+public class ExtendedVocabularyRecord extends VocabularyRecord {
+ private Function vocabularyResolver = VocabularyAPIManager.getInstance().vocabularies()::get;
+ private Function recordResolver = VocabularyAPIManager.getInstance().vocabularyRecords()::get;
+ private Function schemaResolver = VocabularyAPIManager.getInstance().vocabularySchemas()::getSchema;
+ private Function> metadataSchemaResolver = VocabularyAPIManager.getInstance().vocabularySchemas()::getMetadataSchema;
+
+ private int level;
+ private String mainValue;
+ private List titleValues;
+ private List extendedFields;
+ private List parents;
+
+ public ExtendedVocabularyRecord(VocabularyRecord orig) {
+ // TODO: Make generic solution for this
+ setId(orig.getId());
+ setParentId(orig.getParentId());
+ setVocabularyId(orig.getVocabularyId());
+ setMetadata(orig.getMetadata());
+ setFields(orig.getFields());
+ setChildren(orig.getChildren());
+ set_links(orig.get_links());
+
+ postInit();
+ prepareEmpty();
+ }
+
+ // TODO: Think about recreation / updating these values in case the record changes..
+ private void postInit() {
+ initParents();
+ this.level = this.parents.size();
+ this.extendedFields = getFields().stream()
+ .sorted(Comparator.comparingLong(FieldInstance::getDefinitionId))
+ .map(ExtendedFieldInstance::new)
+ .collect(Collectors.toList());
+ this.titleValues = this.extendedFields.stream()
+ .sorted(Comparator.comparingLong(FieldInstance::getDefinitionId))
+ .filter(f -> Boolean.TRUE.equals(f.getDefinition().getTitleField()))
+ .map(ExtendedFieldInstance::getFieldValue)
+ .collect(Collectors.toList());
+ this.mainValue = this.extendedFields.stream()
+ .filter(f -> Boolean.TRUE.equals(f.getDefinition().getMainEntry()))
+ .map(ExtendedFieldInstance::getFieldValue)
+ .findAny()
+ .orElse("");
+ prepareEmpty();
+ }
+
+ @JsonIgnore
+ public String getURI() {
+ return get_links().get("self").getHref();
+ }
+
+ public Optional getFieldValueForDefinition(FieldDefinition definition) {
+ return getFieldForDefinition(definition)
+ .map(ExtendedFieldInstance::getFieldValue);
+ }
+
+ public Optional getFieldValueForDefinition(FieldDefinition definition, String language) {
+ return getFieldForDefinition(definition)
+ .map(f -> f.getFieldValue(language));
+ }
+
+ public Optional getFieldValueForDefinitionName(String definitionName) {
+ return getFieldForDefinitionName(definitionName)
+ .map(ExtendedFieldInstance::getFieldValue);
+ }
+
+ public Optional getFieldValueForDefinitionName(String definitionName, String language) {
+ return getFieldForDefinitionName(definitionName)
+ .map(f -> f.getFieldValue(language));
+ }
+
+ public Optional getFieldForDefinition(FieldDefinition definition) {
+ return getExtendedFields().stream()
+ .filter(f -> f.getDefinitionId().equals(definition.getId()))
+ .findFirst();
+ }
+
+ public Optional