diff --git a/.gitignore b/.gitignore
index d0cc242a..007e314b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,4 @@ generated/
/.metadata/
/build/
cnf/release/
-cnf/local/
src-gen
\ No newline at end of file
diff --git a/cnf/central.mvn b/cnf/central.mvn
index 1c3b2b92..90dd22ba 100644
--- a/cnf/central.mvn
+++ b/cnf/central.mvn
@@ -150,7 +150,6 @@ org.geckoprojects.bnd:org.gecko.bnd.dimc.library:1.1.1
org.geckoprojects.bnd:org.gecko.bnd.osgitest.library:1.1.1
org.geckoprojects.bnd:org.gecko.bnd.jacoco.library:1.1.1
-#org.geckoprojects.emf:org.gecko.emf.osgi.api:4.1.1.202202162308
org.geckoprojects.emf:org.gecko.emf.osgi.bnd.library.workspace:4.1.1-SNAPSHOT
com.fasterxml.jackson.core:jackson-core:2.14.1
@@ -163,3 +162,9 @@ org.eclipse.emfcloud:emfjson-jackson:2.2.0
de.undercouch:bson4jackson:2.13.1
org.yaml:snakeyaml:1.33
org.geckoprojects.emf:org.gecko.emf.osgi.example.model.basic:4.1.1-SNAPSHOT
+
+org.apache.commons:commons-text:1.10.0
+org.apache.commons:commons-lang3:3.12.0
+
+com.google.guava:guava:31.1-jre
+com.google.guava:failureaccess:1.0.1
diff --git a/cnf/local/com.github.miachm.sods/com.github.miachm.sods-1.5.3.jar b/cnf/local/com.github.miachm.sods/com.github.miachm.sods-1.5.3.jar
new file mode 100644
index 00000000..f234d2d8
Binary files /dev/null and b/cnf/local/com.github.miachm.sods/com.github.miachm.sods-1.5.3.jar differ
diff --git a/cnf/local/index.xml b/cnf/local/index.xml
new file mode 100644
index 00000000..74466bc8
--- /dev/null
+++ b/cnf/local/index.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.gecko.emf.exporter.ods.tests/.classpath b/org.gecko.emf.exporter.ods.tests/.classpath
new file mode 100644
index 00000000..66e477cd
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/.classpath
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.gecko.emf.exporter.ods.tests/.gitignore b/org.gecko.emf.exporter.ods.tests/.gitignore
new file mode 100644
index 00000000..7fdbdef7
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/.gitignore
@@ -0,0 +1,2 @@
+/bin/
+/bin_test/
diff --git a/org.gecko.emf.exporter.ods.tests/.project b/org.gecko.emf.exporter.ods.tests/.project
new file mode 100644
index 00000000..711a40ed
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/.project
@@ -0,0 +1,23 @@
+
+
+ org.gecko.emf.exporter.ods.tests
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ bndtools.core.bndbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ bndtools.core.bndnature
+
+
diff --git a/org.gecko.emf.exporter.ods.tests/.settings/org.eclipse.core.resources.prefs b/org.gecko.emf.exporter.ods.tests/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000..b905f5e1
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//.settings/org.eclipse.core.resources.prefs=UTF-8
+encoding//.settings/org.eclipse.jdt.core.prefs=UTF-8
+encoding//.settings/org.eclipse.jdt.ui.prefs=UTF-8
+encoding/bnd.bnd=UTF-8
+encoding/test.bndrun=UTF-8
diff --git a/org.gecko.emf.exporter.ods.tests/.settings/org.eclipse.jdt.core.prefs b/org.gecko.emf.exporter.ods.tests/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000..f2525a8b
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,14 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=11
diff --git a/org.gecko.emf.exporter.ods.tests/bnd.bnd b/org.gecko.emf.exporter.ods.tests/bnd.bnd
new file mode 100644
index 00000000..12c04331
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/bnd.bnd
@@ -0,0 +1,17 @@
+-enable-junit5: true
+-library: enable-emf
+
+javac.source: 11
+javac.target: 11
+
+Bundle-Version: 1.0.0.SNAPSHOT
+
+-buildpath: \
+ org.gecko.emf.osgi.component,\
+ org.eclipse.emf.ecore.xmi,\
+ org.eclipse.emf.ecore,\
+ org.gecko.emf.osgi.example.model.basic,\
+ org.gecko.emf.exporter;version=latest,\
+ org.apache.commons.commons-text,\
+ org.gecko.emf.util.model,\
+ com.github.miachm.sods
diff --git a/org.gecko.emf.exporter.ods.tests/src/org/gecko/emf/ods/tests/EMFODSExporterTest.java b/org.gecko.emf.exporter.ods.tests/src/org/gecko/emf/ods/tests/EMFODSExporterTest.java
new file mode 100644
index 00000000..e443ebd3
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/src/org/gecko/emf/ods/tests/EMFODSExporterTest.java
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2012 - 2023 Data In Motion and others.
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *
+ * Contributors:
+ * Data In Motion - initial API and implementation
+ */
+package org.gecko.emf.ods.tests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.gecko.emf.ods.tests.helper.EMFODSExporterTestHelper.createBasicPackageResourceSet;
+import static org.gecko.emf.ods.tests.helper.EMFODSExporterTestHelper.createBusinessPerson;
+import static org.gecko.emf.ods.tests.helper.EMFODSExporterTestHelper.createFlintstonesFamily;
+import static org.gecko.emf.ods.tests.helper.EMFODSExporterTestHelper.createRequest;
+import static org.gecko.emf.ods.tests.helper.EMFODSExporterTestHelper.createSimpsonFamily;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.emf.common.util.URI;
+import org.eclipse.emf.ecore.resource.Resource;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.gecko.emf.exporter.EMFExportOptions;
+import org.gecko.emf.exporter.EMFExporter;
+import org.gecko.emf.osgi.example.model.basic.BasicFactory;
+import org.gecko.emf.osgi.example.model.basic.BasicPackage;
+import org.gecko.emf.osgi.example.model.basic.BusinessPerson;
+import org.gecko.emf.osgi.example.model.basic.Family;
+import org.gecko.emf.utilities.Request;
+import org.gecko.emf.utilities.UtilitiesFactory;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.platform.commons.annotation.Testable;
+import org.osgi.framework.ServiceReference;
+import org.osgi.test.common.annotation.InjectService;
+import org.osgi.test.common.service.ServiceAware;
+import org.osgi.test.junit5.context.BundleContextExtension;
+import org.osgi.test.junit5.service.ServiceExtension;
+
+/**
+ * EMF ODS exporter integration test.
+ *
+ * @author Michal H. Siemaszko
+ */
+@Testable
+@ExtendWith(BundleContextExtension.class)
+@ExtendWith(ServiceExtension.class)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class EMFODSExporterTest {
+
+ @Order(value = -1)
+ @Test
+ public void testServices(
+ @InjectService(cardinality = 1, timeout = 4000, filter = "(component.name=EMFODSExporter)") ServiceAware emfOdsExporterAware) {
+
+ assertThat(emfOdsExporterAware.getServices()).hasSize(1);
+ ServiceReference emfOdsExporterReference = emfOdsExporterAware.getServiceReference();
+ assertThat(emfOdsExporterReference).isNotNull();
+ }
+
+ @Test
+ public void testExportBasicPackageResourceToOds(
+ @InjectService(cardinality = 1, timeout = 4000, filter = "(component.name=EMFODSExporter)") ServiceAware emfOdsExporterAware,
+ @InjectService BasicFactory basicFactory, @InjectService BasicPackage basicPackage) throws Exception {
+
+ assertThat(emfOdsExporterAware.getServices()).hasSize(1);
+ EMFExporter emfOdsExporterService = emfOdsExporterAware.getService();
+ assertThat(emfOdsExporterService).isNotNull();
+
+ ResourceSet resourceSet = createBasicPackageResourceSet(basicPackage);
+ Resource xmiResource = resourceSet.createResource(URI.createURI("basicPackageExporter.test"));
+ assertNotNull(xmiResource);
+
+ Family simpsonFamily = createSimpsonFamily(basicFactory);
+ xmiResource.getContents().add(simpsonFamily);
+
+ Family flintstonesFamily = createFlintstonesFamily(basicFactory);
+ xmiResource.getContents().add(flintstonesFamily);
+
+ BusinessPerson businessPerson = createBusinessPerson(basicFactory);
+ xmiResource.getContents().add(businessPerson);
+
+ Path filePath = Files.createTempFile("testBasicPackageExport", ".ods");
+
+ OutputStream fileOutputStream = Files.newOutputStream(filePath);
+
+ // @formatter:off
+ emfOdsExporterService.exportResourceTo(xmiResource, fileOutputStream,
+ Map.of(
+ EMFExportOptions.OPTION_LOCALE, Locale.GERMANY,
+ EMFExportOptions.OPTION_EXPORT_NONCONTAINMENT, true,
+ EMFExportOptions.OPTION_EXPORT_METADATA, true,
+ EMFExportOptions.OPTION_ADJUST_COLUMN_WIDTH, true,
+ EMFExportOptions.OPTION_GENERATE_LINKS, true,
+ EMFExportOptions.OPTION_ADD_MAPPING_TABLE, true
+ )
+ );
+ // @formatter:on
+ }
+
+ @Test
+ public void testExportUtilitiesPackageResourceToOds(
+ @InjectService(cardinality = 1, timeout = 4000, filter = "(component.name=EMFODSExporter)") ServiceAware emfOdsExporterAware)
+ throws Exception {
+
+ assertThat(emfOdsExporterAware.getServices()).hasSize(1);
+ EMFExporter emfOdsExporterService = emfOdsExporterAware.getService();
+ assertThat(emfOdsExporterService).isNotNull();
+
+ Request request = createRequest(UtilitiesFactory.eINSTANCE);
+
+ Path filePath = Files.createTempFile("testUtilitiesPackageExport", ".ods");
+
+ OutputStream fileOutputStream = Files.newOutputStream(filePath);
+
+ // @formatter:off
+ emfOdsExporterService.exportEObjectsTo(List.of(request), fileOutputStream,
+ Map.of(
+ EMFExportOptions.OPTION_LOCALE, Locale.GERMANY,
+ EMFExportOptions.OPTION_EXPORT_NONCONTAINMENT, true,
+ EMFExportOptions.OPTION_EXPORT_METADATA, true,
+ EMFExportOptions.OPTION_ADJUST_COLUMN_WIDTH, true,
+ EMFExportOptions.OPTION_GENERATE_LINKS, true,
+ EMFExportOptions.OPTION_ADD_MAPPING_TABLE, true
+ )
+ );
+ // @formatter:on
+ }
+}
diff --git a/org.gecko.emf.exporter.ods.tests/src/org/gecko/emf/ods/tests/helper/EMFODSExporterTestHelper.java b/org.gecko.emf.exporter.ods.tests/src/org/gecko/emf/ods/tests/helper/EMFODSExporterTestHelper.java
new file mode 100644
index 00000000..a2b7194f
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/src/org/gecko/emf/ods/tests/helper/EMFODSExporterTestHelper.java
@@ -0,0 +1,412 @@
+/**
+ * Copyright (c) 2012 - 2023 Data In Motion and others.
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *
+ * Contributors:
+ * Data In Motion - initial API and implementation
+ */
+package org.gecko.emf.ods.tests.helper;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+
+import org.apache.commons.text.RandomStringGenerator;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.gecko.emf.osgi.example.model.basic.Address;
+import org.gecko.emf.osgi.example.model.basic.BasicFactory;
+import org.gecko.emf.osgi.example.model.basic.BasicPackage;
+import org.gecko.emf.osgi.example.model.basic.BusinessPerson;
+import org.gecko.emf.osgi.example.model.basic.ContactContextType;
+import org.gecko.emf.osgi.example.model.basic.ContactType;
+import org.gecko.emf.osgi.example.model.basic.EmployeeInfo;
+import org.gecko.emf.osgi.example.model.basic.Family;
+import org.gecko.emf.osgi.example.model.basic.GenderType;
+import org.gecko.emf.osgi.example.model.basic.Person;
+import org.gecko.emf.osgi.example.model.basic.PersonContact;
+import org.gecko.emf.osgi.example.model.basic.Tag;
+import org.gecko.emf.osgi.example.model.basic.util.BasicResourceFactoryImpl;
+import org.gecko.emf.utilities.Filter;
+import org.gecko.emf.utilities.Request;
+import org.gecko.emf.utilities.Sort;
+import org.gecko.emf.utilities.SortType;
+import org.gecko.emf.utilities.UtilitiesFactory;
+
+public class EMFODSExporterTestHelper {
+
+ public static Request createRequest(UtilitiesFactory uf) {
+ Request request = uf.createRequest();
+
+ Instant now = Instant.now();
+
+ request.setId(UUID.randomUUID().toString());
+ request.setFrom(Date.from(now));
+ request.setTo(Date.from(now.plus(7, ChronoUnit.DAYS)));
+
+ Sort sort = uf.createSort();
+ sort.setIndex(0);
+ sort.setField("cartoon");
+ sort.setType(SortType.ASCENDING);
+
+ request.getSorting().add(sort);
+
+ Filter filter = uf.createFilter();
+ filter.setIndex(0);
+ filter.setField("cartoon");
+ filter.getValue().add("Simpsons");
+
+ request.getFiltering().add(filter);
+
+ return request;
+ }
+
+ public static Family createSimpsonFamily(BasicFactory bf) {
+ Family simpsonFamily = bf.createFamily();
+ simpsonFamily.setId("Simpsons");
+
+ Address address = createSimpsonsAddress(bf);
+
+ Person homerSimpson = createHomerSimpson(bf, address);
+ simpsonFamily.setFather(homerSimpson);
+
+ Person margeSimpson = createMargeSimpson(bf, address);
+ simpsonFamily.setMother(margeSimpson);
+
+ Person bartSimpson = createBartSimpson(bf, address);
+ simpsonFamily.getChildren().add(bartSimpson);
+
+ Person lisaSimpson = createLisaSimpson(bf, address);
+ simpsonFamily.getChildren().add(lisaSimpson);
+
+ Person maggieSimpson = createMaggieSimpson(bf, address);
+ simpsonFamily.getChildren().add(maggieSimpson);
+
+ homerSimpson.getRelatives().add(margeSimpson);
+ homerSimpson.getRelatives().add(bartSimpson);
+ homerSimpson.getRelatives().add(lisaSimpson);
+ homerSimpson.getRelatives().add(maggieSimpson);
+
+ margeSimpson.getRelatives().add(homerSimpson);
+ margeSimpson.getRelatives().add(bartSimpson);
+ margeSimpson.getRelatives().add(lisaSimpson);
+ margeSimpson.getRelatives().add(maggieSimpson);
+
+ bartSimpson.getRelatives().add(homerSimpson);
+ bartSimpson.getRelatives().add(margeSimpson);
+ bartSimpson.getRelatives().add(lisaSimpson);
+ bartSimpson.getRelatives().add(maggieSimpson);
+
+ lisaSimpson.getRelatives().add(homerSimpson);
+ lisaSimpson.getRelatives().add(margeSimpson);
+ lisaSimpson.getRelatives().add(bartSimpson);
+ lisaSimpson.getRelatives().add(maggieSimpson);
+
+ maggieSimpson.getRelatives().add(homerSimpson);
+ maggieSimpson.getRelatives().add(margeSimpson);
+ maggieSimpson.getRelatives().add(lisaSimpson);
+ maggieSimpson.getRelatives().add(maggieSimpson);
+
+ homerSimpson.getTags().add(createMultiLevelTag(bf, createUniquePrefix(10)));
+
+ homerSimpson.setBigInt(BigInteger.TEN);
+
+ homerSimpson.getBigDec().add(BigDecimal.ZERO);
+ homerSimpson.getBigDec().add(BigDecimal.ONE);
+ homerSimpson.getBigDec().add(BigDecimal.TEN);
+
+ homerSimpson.setImage(createByteArr());
+
+ homerSimpson.getProperties().putAll(createProperties(createUniquePrefix(10)));
+
+ return simpsonFamily;
+ }
+
+ private static Address createSimpsonsAddress(BasicFactory bf) {
+ return createAddress(bf, "742 Evergreen Terrace", "Springfield", "97482");
+ }
+
+ private static Person createHomerSimpson(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Homer", "Simpson", GenderType.MALE, address);
+
+ p.getContact().add(createHomePhonePersonContact(bf, p));
+ p.getContact().add(createHomeMobilePersonContact(bf, p));
+ p.getContact().add(createHomeWhatsAppPersonContact(bf, p));
+ p.getContact().add(createHomeEmailPersonContact(bf, p));
+ p.getContact().add(createHomeSkypePersonContact(bf, p));
+ p.getContact().add(createHomeWebAddressPersonContact(bf, p));
+
+ return p;
+ }
+
+ private static Person createMargeSimpson(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Marge", "Simpson", GenderType.FEMALE, address);
+
+ p.getContact().add(createHomePhonePersonContact(bf, p));
+ p.getContact().add(createHomeMobilePersonContact(bf, p));
+ p.getContact().add(createHomeWhatsAppPersonContact(bf, p));
+ p.getContact().add(createHomeEmailPersonContact(bf, p));
+ p.getContact().add(createHomeSkypePersonContact(bf, p));
+ p.getContact().add(createHomeWebAddressPersonContact(bf, p));
+
+ return p;
+ }
+
+ private static Person createBartSimpson(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Bart", "Simpson", GenderType.MALE, address);
+
+ return p;
+ }
+
+ private static Person createLisaSimpson(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Lisa", "Simpson", GenderType.FEMALE, address);
+
+ return p;
+ }
+
+ private static Person createMaggieSimpson(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Maggie", "Simpson", GenderType.FEMALE, address);
+
+ return p;
+ }
+
+ public static Family createFlintstonesFamily(BasicFactory bf) {
+ Family flintstonesFamily = bf.createFamily();
+ flintstonesFamily.setId("Flintstones");
+
+ Address address = createFlintstonesAddress(bf);
+
+ Person fredFlintstone = createFredFlintstone(bf, address);
+ flintstonesFamily.setFather(fredFlintstone);
+
+ Person wilmaFlintstone = createWilmaFlintstone(bf, address);
+ flintstonesFamily.setMother(wilmaFlintstone);
+
+ Person pebblesFlintstone = createPebblesFlintstone(bf, address);
+ flintstonesFamily.getChildren().add(pebblesFlintstone);
+
+ Person stonyFlintstone = createStonyFlintstone(bf, address);
+ flintstonesFamily.getChildren().add(stonyFlintstone);
+
+ fredFlintstone.getRelatives().add(wilmaFlintstone);
+ fredFlintstone.getRelatives().add(pebblesFlintstone);
+ fredFlintstone.getRelatives().add(stonyFlintstone);
+
+ wilmaFlintstone.getRelatives().add(fredFlintstone);
+ wilmaFlintstone.getRelatives().add(pebblesFlintstone);
+ wilmaFlintstone.getRelatives().add(stonyFlintstone);
+
+ pebblesFlintstone.getRelatives().add(fredFlintstone);
+ pebblesFlintstone.getRelatives().add(wilmaFlintstone);
+ pebblesFlintstone.getRelatives().add(stonyFlintstone);
+
+ stonyFlintstone.getRelatives().add(fredFlintstone);
+ stonyFlintstone.getRelatives().add(wilmaFlintstone);
+ stonyFlintstone.getRelatives().add(pebblesFlintstone);
+
+ fredFlintstone.getTags().add(createMultiLevelTag(bf, createUniquePrefix(10)));
+
+ fredFlintstone.setBigInt(BigInteger.TEN);
+
+ fredFlintstone.getBigDec().add(BigDecimal.ZERO);
+ fredFlintstone.getBigDec().add(BigDecimal.ONE);
+ fredFlintstone.getBigDec().add(BigDecimal.TEN);
+
+ fredFlintstone.setImage(createByteArr());
+
+ fredFlintstone.getProperties().putAll(createProperties(createUniquePrefix(10)));
+
+ return flintstonesFamily;
+ }
+
+ private static Address createFlintstonesAddress(BasicFactory bf) {
+ return createAddress(bf, "301 Cobblestone Way", "Bedrock", "70777");
+ }
+
+ private static Person createFredFlintstone(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Fred", "Flintstone", GenderType.MALE, address);
+
+ p.getContact().add(createHomePhonePersonContact(bf, p));
+ p.getContact().add(createHomeMobilePersonContact(bf, p));
+ p.getContact().add(createHomeWhatsAppPersonContact(bf, p));
+ p.getContact().add(createHomeEmailPersonContact(bf, p));
+ p.getContact().add(createHomeSkypePersonContact(bf, p));
+ p.getContact().add(createHomeWebAddressPersonContact(bf, p));
+
+ return p;
+ }
+
+ private static Person createWilmaFlintstone(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Wilma", "Flintstone", GenderType.FEMALE, address);
+
+ p.getContact().add(createHomePhonePersonContact(bf, p));
+ p.getContact().add(createHomeMobilePersonContact(bf, p));
+ p.getContact().add(createHomeWhatsAppPersonContact(bf, p));
+ p.getContact().add(createHomeEmailPersonContact(bf, p));
+ p.getContact().add(createHomeSkypePersonContact(bf, p));
+ p.getContact().add(createHomeWebAddressPersonContact(bf, p));
+
+ return p;
+ }
+
+ private static Person createPebblesFlintstone(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Pebbles", "Flintstone", GenderType.FEMALE, address);
+
+ return p;
+ }
+
+ private static Person createStonyFlintstone(BasicFactory bf, Address address) {
+ Person p = createPerson(bf, "Stony", "Flintstone", GenderType.MALE, address);
+
+ return p;
+ }
+
+ public static BusinessPerson createBusinessPerson(BasicFactory bf) {
+ BusinessPerson bp = bf.createBusinessPerson();
+
+ bp.setId(UUID.randomUUID().toString());
+ bp.setFirstName("Thomas");
+ bp.setLastName("Edison");
+ bp.setGender(GenderType.MALE);
+
+ bp.setCompanyIdCardNumber(UUID.randomUUID().toString());
+
+ EmployeeInfo nikolaTesla = bf.createEmployeeInfo();
+ nikolaTesla.setPosition("one-time employee");
+ bp.getEmployeeInfo().add(nikolaTesla);
+
+ return bp;
+ }
+
+ private static Person createPerson(BasicFactory bf, String firstName, String lastName, GenderType gender,
+ Address address) {
+ Person p = bf.createPerson();
+
+ p.setId(UUID.randomUUID().toString());
+ p.setFirstName(firstName);
+ p.setLastName(lastName);
+ p.setGender(gender);
+
+ p.setAddress(address);
+
+ return p;
+ }
+
+ private static Address createAddress(BasicFactory bf, String street, String city, String zip) {
+ Address a = bf.createAddress();
+
+ a.setId(UUID.randomUUID().toString());
+ a.setStreet(street);
+ a.setCity(city);
+ a.setZip(zip);
+
+ return a;
+ }
+
+ private static PersonContact createHomePhonePersonContact(BasicFactory bf, Person p) {
+ return createPersonContact(bf, ContactType.PHONE, ContactContextType.HOME, UUID.randomUUID().toString(), p);
+ }
+
+ private static PersonContact createHomeMobilePersonContact(BasicFactory bf, Person p) {
+ return createPersonContact(bf, ContactType.MOBILE, ContactContextType.HOME, UUID.randomUUID().toString(), p);
+ }
+
+ private static PersonContact createHomeWhatsAppPersonContact(BasicFactory bf, Person p) {
+ return createPersonContact(bf, ContactType.WHATSAPP, ContactContextType.HOME, UUID.randomUUID().toString(), p);
+ }
+
+ private static PersonContact createHomeEmailPersonContact(BasicFactory bf, Person p) {
+ return createPersonContact(bf, ContactType.EMAIL, ContactContextType.HOME, UUID.randomUUID().toString(), p);
+ }
+
+ private static PersonContact createHomeSkypePersonContact(BasicFactory bf, Person p) {
+ return createPersonContact(bf, ContactType.SKYPE, ContactContextType.HOME, UUID.randomUUID().toString(), p);
+ }
+
+ private static PersonContact createHomeWebAddressPersonContact(BasicFactory bf, Person p) {
+ return createPersonContact(bf, ContactType.WEBADDRESS, ContactContextType.HOME, UUID.randomUUID().toString(),
+ p);
+ }
+
+ private static PersonContact createPersonContact(BasicFactory bf, ContactType type, ContactContextType context,
+ String value, Person p) {
+ PersonContact pc = bf.createPersonContact();
+
+ pc.setContext(context);
+ pc.setType(type);
+ pc.setValue(value);
+
+ pc.setContactPerson(p);
+
+ return pc;
+ }
+
+ private static Tag createMultiLevelTag(BasicFactory bf, String namePrefix) {
+ Tag t1 = createTag(bf, namePrefix, "tag_level_1", "tag_level_1_value", "tag_level_1_description");
+
+ t1.setTag(createTag(bf, namePrefix, "tag_level_2", "tag_level_2_value", "tag_level_2_description"));
+
+ t1.getTags().add(createTag(bf, namePrefix, "tag_level_3", "tag_level_3_value", "tag_level_3_description"));
+
+ return t1;
+ }
+
+ private static Tag createTag(BasicFactory bf, String namePrefix, String name, String value, String description) {
+ Tag t = bf.createTag();
+
+ t.setName(namePrefix + "_" + name);
+ t.setValue(value);
+ t.setDescription(description);
+
+ return t;
+ }
+
+ private static byte[] createByteArr() {
+ byte[] b = new byte[20];
+ new Random().nextBytes(b);
+ return b;
+ }
+
+ private static Map createProperties(String namePrefix) {
+ Map props = new HashMap();
+
+ props.put(createPropertyName(namePrefix, "prop_1"), "prop_1_value");
+ props.put(createPropertyName(namePrefix, "prop_2"), "prop_2_value");
+ props.put(createPropertyName(namePrefix, "prop_3"), "prop_3_value");
+ props.put(createPropertyName(namePrefix, "prop_4"), "prop_4_value");
+
+ return props;
+ }
+
+ private static String createPropertyName(String prefix, String name) {
+ return (prefix + "_" + name);
+ }
+
+ public static ResourceSet createBasicPackageResourceSet(BasicPackage bp) {
+ ResourceSet resourceSet = new ResourceSetImpl();
+ resourceSet.getPackageRegistry().put(BasicPackage.eNS_URI, bp);
+ resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put("test", new BasicResourceFactoryImpl());
+ resourceSet.getResourceFactoryRegistry().getContentTypeToFactoryMap().put(BasicPackage.eCONTENT_TYPE,
+ new BasicResourceFactoryImpl());
+ return resourceSet;
+ }
+
+ private static String createUniquePrefix(int maxChars) {
+ // @formatter:off
+ return new RandomStringGenerator.Builder()
+ .withinRange('a', 'z')
+ .build()
+ .generate(maxChars);
+ // @formatter:on
+ }
+}
diff --git a/org.gecko.emf.exporter.ods.tests/test.bndrun b/org.gecko.emf.exporter.ods.tests/test.bndrun
new file mode 100644
index 00000000..29ef1b41
--- /dev/null
+++ b/org.gecko.emf.exporter.ods.tests/test.bndrun
@@ -0,0 +1,58 @@
+
+-runfw: org.apache.felix.framework;version='[7.0.1,7.0.1]'
+-runprovidedcapabilities: ${native_capability}
+
+-resolve.effective: active
+
+-runbundles.junit5: ${test.runbundles}
+
+-runbundles: \
+ junit-jupiter-api;version='[5.8.2,5.8.3)',\
+ junit-platform-commons;version='[1.8.2,1.8.3)',\
+ org.apache.felix.configadmin;version='[1.9.22,1.9.23)',\
+ org.apache.felix.scr;version='[2.1.30,2.1.31)',\
+ org.eclipse.emf.common;version='[2.23.0,2.23.1)',\
+ org.eclipse.emf.ecore;version='[2.25.0,2.25.1)',\
+ org.eclipse.emf.ecore.xmi;version='[2.16.0,2.16.1)',\
+ org.gecko.emf.osgi.api;version='[4.1.1,4.1.2)',\
+ org.gecko.emf.osgi.component;version='[4.1.1,4.1.2)',\
+ org.gecko.emf.osgi.example.model.basic;version='[4.1.1,4.1.2)',\
+ org.opentest4j;version='[1.2.0,1.2.1)',\
+ org.osgi.util.function;version='[1.1.0,1.1.1)',\
+ org.osgi.util.pushstream;version='[1.0.1,1.0.2)',\
+ org.apache.felix.converter;version='[1.0.18,1.0.19)',\
+ org.osgi.util.promise;version='[1.2.0,1.2.1)',\
+ assertj-core;version='[3.22.0,3.22.1)',\
+ junit-jupiter-params;version='[5.8.2,5.8.3)',\
+ org.osgi.test.common;version='[1.1.0,1.1.1)',\
+ org.osgi.test.junit5;version='[1.1.0,1.1.1)',\
+ org.yaml.snakeyaml;version='[1.33.0,1.33.1)',\
+ org.gecko.emf.exporter;version=snapshot,\
+ org.gecko.emf.exporter.ods;version=snapshot,\
+ org.gecko.emf.exporter.ods.tests;version=snapshot,\
+ com.fasterxml.jackson.core.jackson-annotations;version='[2.14.1,2.14.2)',\
+ com.fasterxml.jackson.core.jackson-core;version='[2.14.1,2.14.2)',\
+ com.fasterxml.jackson.core.jackson-databind;version='[2.14.1,2.14.2)',\
+ com.fasterxml.jackson.dataformat.jackson-dataformat-properties;version='[2.14.1,2.14.2)',\
+ com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.14.1,2.14.2)',\
+ de.undercouch.bson4jackson;version='[2.13.1,2.13.2)',\
+ org.apache.commons.commons-text;version='[1.10.0,1.10.1)',\
+ org.apache.commons.lang3;version='[3.12.0,3.12.1)',\
+ org.eclipse.emfcloud.emfjson-jackson;version='[2.2.0,2.2.1)',\
+ org.gecko.emf.json;version=snapshot,\
+ org.gecko.emf.util.model;version=snapshot,\
+ org.osgi.service.cm;version='[1.6.0,1.6.1)',\
+ com.github.miachm.sods;version='[1.5.3,1.5.4)',\
+ org.gecko.emf.pushstreams;version=snapshot,\
+ com.fasterxml.jackson.datatype.jackson-datatype-jsr310;version='[2.14.1,2.14.2)'
+
+-runrequires: bnd.identity;id='org.gecko.emf.exporter.ods.tests'
+
+-runee: JavaSE-11
+
+-runtrace: true
+
+-runproperties.debug: \
+ felix.log.level=4,\
+ org.osgi.service.log.admin.loglevel=DEBUG
+
diff --git a/org.gecko.emf.exporter.ods/.classpath b/org.gecko.emf.exporter.ods/.classpath
new file mode 100644
index 00000000..66e477cd
--- /dev/null
+++ b/org.gecko.emf.exporter.ods/.classpath
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.gecko.emf.exporter.ods/.gitignore b/org.gecko.emf.exporter.ods/.gitignore
new file mode 100644
index 00000000..7fdbdef7
--- /dev/null
+++ b/org.gecko.emf.exporter.ods/.gitignore
@@ -0,0 +1,2 @@
+/bin/
+/bin_test/
diff --git a/org.gecko.emf.exporter.ods/.project b/org.gecko.emf.exporter.ods/.project
new file mode 100644
index 00000000..739cd2ab
--- /dev/null
+++ b/org.gecko.emf.exporter.ods/.project
@@ -0,0 +1,23 @@
+
+
+ org.gecko.emf.exporter.ods
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ bndtools.core.bndbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ bndtools.core.bndnature
+
+
diff --git a/org.gecko.emf.exporter.ods/.settings/org.eclipse.core.resources.prefs b/org.gecko.emf.exporter.ods/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000..e8cd65cb
--- /dev/null
+++ b/org.gecko.emf.exporter.ods/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,5 @@
+eclipse.preferences.version=1
+encoding//.settings/org.eclipse.core.resources.prefs=UTF-8
+encoding//.settings/org.eclipse.jdt.core.prefs=UTF-8
+encoding//.settings/org.eclipse.jdt.ui.prefs=UTF-8
+encoding/bnd.bnd=UTF-8
diff --git a/org.gecko.emf.exporter.ods/.settings/org.eclipse.jdt.core.prefs b/org.gecko.emf.exporter.ods/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000..f2525a8b
--- /dev/null
+++ b/org.gecko.emf.exporter.ods/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,14 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=11
diff --git a/org.gecko.emf.exporter.ods/bnd.bnd b/org.gecko.emf.exporter.ods/bnd.bnd
new file mode 100644
index 00000000..9fc5e408
--- /dev/null
+++ b/org.gecko.emf.exporter.ods/bnd.bnd
@@ -0,0 +1,14 @@
+Bundle-Version: 1.0.0.SNAPSHOT
+Bundle-Name: Gecko EMF ODS Exporter
+Bundle-Description: ODS (OpenDocument Spreadsheet) Exporter for EMF
+
+-library: enable-emf
+
+-buildpath: \
+ org.gecko.emf.exporter,\
+ org.apache.commons.commons-text;version='1.10',\
+ com.google.guava;version='31.1',\
+ com.github.miachm.sods
+
+Private-Package: \
+ org.gecko.emf.exporter.ods
diff --git a/org.gecko.emf.exporter.ods/src/org/gecko/emf/exporter/ods/EMFODSExporter.java b/org.gecko.emf.exporter.ods/src/org/gecko/emf/exporter/ods/EMFODSExporter.java
new file mode 100644
index 00000000..aa14b2b4
--- /dev/null
+++ b/org.gecko.emf.exporter.ods/src/org/gecko/emf/exporter/ods/EMFODSExporter.java
@@ -0,0 +1,1303 @@
+/**
+ * Copyright (c) 2012 - 2023 Data In Motion and others.
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *
+ * Contributors:
+ * Data In Motion - initial API and implementation
+ */
+package org.gecko.emf.exporter.ods;
+
+import static java.util.stream.Collectors.toList;
+
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.commons.text.WordUtils;
+import org.eclipse.emf.ecore.EAnnotation;
+import org.eclipse.emf.ecore.EAttribute;
+import org.eclipse.emf.ecore.EClass;
+import org.eclipse.emf.ecore.EDataType;
+import org.eclipse.emf.ecore.EEnum;
+import org.eclipse.emf.ecore.EEnumLiteral;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EReference;
+import org.eclipse.emf.ecore.EStructuralFeature;
+import org.eclipse.emf.ecore.resource.Resource;
+import org.eclipse.emf.ecore.util.EcoreUtil;
+import org.gecko.emf.exporter.EMFExportException;
+import org.gecko.emf.exporter.EMFExportOptions;
+import org.gecko.emf.exporter.EMFExporter;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ServiceScope;
+
+import com.github.miachm.sods.Color;
+import com.github.miachm.sods.LinkedValue;
+import com.github.miachm.sods.Range;
+import com.github.miachm.sods.Sheet;
+import com.github.miachm.sods.SpreadSheet;
+import com.github.miachm.sods.Style;
+
+/**
+ * Implementation of the {@link EMFExporter} to provide support for exporting EMF resources and lists of EMF objects to ODS format.
+ *
+ * @author Michal H. Siemaszko
+ */
+@Component(name = "EMFODSExporter", scope = ServiceScope.PROTOTYPE)
+public class EMFODSExporter implements EMFExporter {
+
+ private static final int MAX_CHAR_PER_LINE_DEFAULT = 30;
+
+ private static final Style HEADER_STYLE = new Style();
+ static {
+ HEADER_STYLE.setBackgroundColor(new Color("#a3a3a3"));
+ HEADER_STYLE.setFontColor(new Color("#000000"));
+ HEADER_STYLE.setBold(true);
+ HEADER_STYLE.setTextAligment(Style.TEXT_ALIGMENT.Center);
+ }
+
+ private static final Style WRAPPED_DATA_CELL_STYLE = new Style();
+ static {
+ WRAPPED_DATA_CELL_STYLE.setWrap(true);
+ }
+
+ private static final List METADATA_ECLASS_SHEET_HEADERS = List.of("Name", "Type", "isMany", "isRequired",
+ "Default value", "Documentation");
+ private static final List METADATA_EENUM_SHEET_HEADERS = List.of("Name", "Literal", "Value",
+ "Documentation");
+ private static final String METADATA_DOCUMENTATION_HEADER = "Documentation";
+
+ private static final String METADATA_SHEET_SUFFIX = "Metadata";
+ private static final String MAPPING_TABLE_SHEET_SUFFIX = "Mapping Table";
+
+ private static final String DOCUMENTATION_GENMODEL_SOURCE = "http://www.eclipse.org/emf/2002/GenModel";
+ private static final String DOCUMENTATION_GENMODEL_DETAILS = "documentation";
+
+ private static final String ECORE_PACKAGE_NAME = "ecore";
+
+ private static final String ID_COLUMN_NAME = "Id";
+ private static final int ID_COLUMN_WIDTH = 18;
+
+ private static final String REF_COLUMN_PREFIX = "ref_";
+
+ /*
+ * (non-Javadoc)
+ * @see org.gecko.emf.exporter.EMFExporter#exportResourceTo(org.eclipse.emf.ecore.resource.Resource, java.io.OutputStream, java.util.Map)
+ */
+ @Override
+ public void exportResourceTo(Resource resource, OutputStream outputStream, Map, ?> options)
+ throws EMFExportException {
+ Objects.requireNonNull(resource, "Resource is required for export!");
+
+ try {
+
+ exportEObjectsTo(resource.getContents(), outputStream, options);
+
+ } catch (Exception e) {
+ throw new EMFExportException(e);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.gecko.emf.exporter.EMFExporter#exportEObjectsTo(java.util.List, java.io.OutputStream, java.util.Map)
+ */
+ @Override
+ public void exportEObjectsTo(List eObjects, OutputStream outputStream, Map, ?> options)
+ throws EMFExportException {
+ Objects.requireNonNull(eObjects, "At least one EObject is required for export!");
+ Objects.requireNonNull(outputStream, "Output stream is required for export!");
+
+ if (!eObjects.isEmpty()) {
+
+ try {
+
+ final Map