+ * Interface describing the API of suppliers for different file formats. + * + * @since 1.6.0 + */ +public interface FileFormatSupplier { + + File from(String fileName, ResearchProject researchProject) throws FormatException; + + class FormatException extends RuntimeException { + + public FormatException(String message) { + } + + public FormatException(String message, Throwable cause) { + } + } + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/export/TempDirectory.java b/user-interface/src/main/java/life/qbic/datamanager/export/TempDirectory.java new file mode 100644 index 000000000..67bdf9413 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/export/TempDirectory.java @@ -0,0 +1,52 @@ +package life.qbic.datamanager.export; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Temporary Director + *
+ * Provides a temporary directory for the application to use when files need to be created for
+ * exporting them.
+ *
+ * @since
+ * The method minimises the probability for collisions, so the client does not need to take care
+ * about potential existing directories.
+ *
+ * @return the path to the newly created directory in the app's global temporary directory
+ * @throws IOException in case the directory could not be created
+ * @since 1.6.0
+ */
+ public Path createDirectory() throws IOException {
+ return Files.createDirectory(tempDir.resolve(UUID.randomUUID().toString()));
+ }
+
+}
diff --git a/user-interface/src/main/java/life/qbic/datamanager/export/docx/DocxSupplier.java b/user-interface/src/main/java/life/qbic/datamanager/export/docx/DocxSupplier.java
new file mode 100644
index 000000000..0cf01a34b
--- /dev/null
+++ b/user-interface/src/main/java/life/qbic/datamanager/export/docx/DocxSupplier.java
@@ -0,0 +1,67 @@
+package life.qbic.datamanager.export.docx;
+
+import java.io.File;
+import life.qbic.datamanager.export.FileFormatSupplier;
+import life.qbic.datamanager.export.model.ContactPoint;
+import life.qbic.datamanager.export.model.ResearchProject;
+import org.docx4j.openpackaging.exceptions.Docx4JException;
+import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
+import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
+
+/**
+ * DOCX formatter implementation
+ *
+ * Creates DOCX representations of various content types in a Data Manager's project.
+ *
+ * @since 1.0.0
+ */
+public class DocxSupplier implements FileFormatSupplier {
+
+ public static DocxSupplier create() {
+ return new DocxSupplier();
+ }
+
+ @Override
+ public File from(String fileName, ResearchProject researchProject) {
+ try {
+ var wordPackage = WordprocessingMLPackage.createPackage();
+ var mainDocument = wordPackage.getMainDocumentPart();
+ addTitle(mainDocument, researchProject);
+ addProjectId(mainDocument, researchProject);
+ addSection(mainDocument, "Description", researchProject.description());
+ addSectionTitle(mainDocument, "Contact Points");
+ researchProject.contactPoint()
+ .forEach(contactPoint -> addContactPoint(mainDocument, contactPoint));
+ File file = new File(fileName);
+ wordPackage.save(file);
+ return file;
+ } catch (Docx4JException e) {
+ throw new FormatException("Creating docx package failed. ", e);
+ }
+ }
+
+ private void addTitle(MainDocumentPart mainDocumentPart, ResearchProject researchProject) {
+ mainDocumentPart.addStyledParagraphOfText("Title", researchProject.name());
+ }
+
+ private void addProjectId(MainDocumentPart mainDocumentPart, ResearchProject researchProject) {
+ mainDocumentPart.addStyledParagraphOfText("Subtitle",
+ "Project ID: " + researchProject.identifier());
+ }
+
+ private void addSection(MainDocumentPart mainDocumentPart, String sectionTitle,
+ String sectionContent) {
+ mainDocumentPart.addStyledParagraphOfText("Heading1", sectionTitle);
+ mainDocumentPart.addParagraphOfText(sectionContent);
+ }
+
+ private void addSectionTitle(MainDocumentPart mainDocumentPart, String title) {
+ mainDocumentPart.addStyledParagraphOfText("Heading1", title);
+ }
+
+ private void addContactPoint(MainDocumentPart mainDocumentPart, ContactPoint contactPoint) {
+ var contactPointFormatted = "%s (%s) - %s".formatted(contactPoint.name(),
+ contactPoint.contactType(), contactPoint.email());
+ mainDocumentPart.addParagraphOfText(contactPointFormatted);
+ }
+}
diff --git a/user-interface/src/main/java/life/qbic/datamanager/export/model/ContactPoint.java b/user-interface/src/main/java/life/qbic/datamanager/export/model/ContactPoint.java
new file mode 100644
index 000000000..b9d6699ed
--- /dev/null
+++ b/user-interface/src/main/java/life/qbic/datamanager/export/model/ContactPoint.java
@@ -0,0 +1,44 @@
+package life.qbic.datamanager.export.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A schema.org ContactPoint representation in Java.
+ *
+ * @since 1.6.0.
+ */
+public class ContactPoint {
+
+ @JsonProperty(value = "@type")
+ private final String type = "ContactPoint";
+
+ @JsonProperty(value = "name")
+ private String name;
+
+ @JsonProperty(value = "email")
+ private String email;
+
+ @JsonProperty(value = "contactType")
+ private String contactType;
+
+ public static ContactPoint from(String name, String email, String contactType) {
+ ContactPoint contactPoint = new ContactPoint();
+ contactPoint.name = name;
+ contactPoint.email = email;
+ contactPoint.contactType = contactType;
+ return contactPoint;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public String email() {
+ return email;
+ }
+
+ public String contactType() {
+ return contactType;
+ }
+
+}
diff --git a/user-interface/src/main/java/life/qbic/datamanager/export/model/ResearchProject.java b/user-interface/src/main/java/life/qbic/datamanager/export/model/ResearchProject.java
new file mode 100644
index 000000000..b8ac517f2
--- /dev/null
+++ b/user-interface/src/main/java/life/qbic/datamanager/export/model/ResearchProject.java
@@ -0,0 +1,53 @@
+package life.qbic.datamanager.export.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/**
+ * A schema.org ResearchProject representation in Java.
+ *
+ * @since 1.6.0.
+ */
+public class ResearchProject {
+
+ @JsonProperty(value = "@type")
+ private final String type = "ResearchProject";
+
+ @JsonProperty(value = "name")
+ private String name;
+
+ @JsonProperty(value = "identifier")
+ private String identifier;
+
+ @JsonProperty(value = "description")
+ private String description;
+
+ @JsonProperty(value = "contactPoint")
+ private List
+ * Builder class that helps to build a RO-Crate based on various QBiC data manager project
+ * information.
+ *
+ * @since 1.6.0
+ */
+@Component
+public class ROCreateBuilder {
+
+ private final TempDirectory tempDirectory;
+
+ @Autowired
+ public ROCreateBuilder(TempDirectory tempDir) {
+ this.tempDirectory = Objects.requireNonNull(tempDir);
+ }
+
+ private static RoCrate buildRoCrate(Path buildDir, ResearchProject researchProject) {
+ var projectInfoDocx = DocxSupplier.create()
+ .from(buildDir.resolve(SUMMARY_FILENAME_DOCX.value()).toString(), researchProject);
+ var projectInfoYaml = YamlSupplier.create()
+ .from(buildDir.resolve(SUMMARY_FILENAME_YAML.value()).toString(), researchProject);
+ return new RoCrate.RoCrateBuilder(
+ "QBiC-project-%s-ro-crate".formatted(researchProject.identifier()),
+ "Description of the project %s with the title '%s', managed on the Data Manager, Quantitative Biology Center, University of Tübingen.".formatted(
+ researchProject.identifier(), researchProject.name()))
+ .addDataEntity(
+ new FileEntity.FileEntityBuilder()
+ .setSource(projectInfoDocx)
+ .setId(SUMMARY_FILENAME_DOCX.value())
+ .addProperty("name", "Project Summary")
+ .addProperty("encodingFormat",
+ MimeTypes.DOCX.value())
+ .build())
+ .addDataEntity(
+ new FileEntity.FileEntityBuilder()
+ .setSource(projectInfoYaml)
+ .setId(SUMMARY_FILENAME_YAML.value())
+ .addProperty("name", "Project Summary")
+ .addProperty("encodingFormat", MimeTypes.YAML.value())
+ .build())
+ .build();
+ }
+
+ public RoCrate projectSummary(Project project, Path buildDirectory) throws ROCrateBuildException {
+ var researchProject = convertToResearchProject(project);
+ if (!buildDirectory.toFile().exists()) {
+ throw new ROCrateBuildException("File does not exist: " + buildDirectory);
+ }
+ return buildRoCrate(buildDirectory, researchProject);
+ }
+
+ private ResearchProject convertToResearchProject(Project project) {
+ var contactPoints = new ArrayList
+ * Creates YAML representations of various content types in a Data Manager's project.
+ *
+ * @since 1.0.0
+ */
+public class YamlSupplier implements FileFormatSupplier {
+
+ public static YamlSupplier create() {
+ return new YamlSupplier();
+ }
+
+ @Override
+ public File from(String fileName, ResearchProject researchProject) throws FormatException {
+ ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+ File file = new File(fileName);
+ try {
+ mapper.writeValue(file, researchProject);
+ return file;
+ } catch (IOException e) {
+ throw new FormatException("Could not write to file " + fileName, e);
+ }
+ }
+}
diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectEditEvent.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectEditEvent.java
index 4b51cdad9..d79eed491 100644
--- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectEditEvent.java
+++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectEditEvent.java
@@ -8,11 +8,11 @@
* Project Edit Event
*
* Event that indicates that the user wants to edit a project via the
- * {@link ProjectDetailsComponent}
+ * {@link ProjectSummaryComponent}
*
* @since 1.0.0
*/
-public class ProjectEditEvent extends ComponentEventtrue
if the event originated from the client
* side, false
otherwise
*/
- public ProjectEditEvent(ProjectDetailsComponent source, ProjectId projectId,
+ public ProjectEditEvent(ProjectSummaryComponent source, ProjectId projectId,
boolean fromClient) {
super(source, fromClient);
this.projectId = projectId;
diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectInformationMain.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectInformationMain.java
index 0a5de67df..2042c78a3 100644
--- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectInformationMain.java
+++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectInformationMain.java
@@ -84,7 +84,7 @@ public class ProjectInformationMain extends Main implements BeforeEnterObserver
private final transient ProjectPurchaseService projectPurchaseService;
private final transient QualityControlService qualityControlService;
private final transient UserPermissions userPermissions;
- private final ProjectDetailsComponent projectDetailsComponent;
+ private final ProjectSummaryComponent projectSummaryComponent;
private final ExperimentListComponent experimentListComponent;
private final OfferDownload offerDownload;
private final QualityControlDownload qualityControlDownload;
@@ -95,7 +95,7 @@ public class ProjectInformationMain extends Main implements BeforeEnterObserver
private final TerminologyService terminologyService;
private Context context;
- public ProjectInformationMain(@Autowired ProjectDetailsComponent projectDetailsComponent,
+ public ProjectInformationMain(@Autowired ProjectSummaryComponent projectSummaryComponent,
@Autowired ExperimentListComponent experimentListComponent,
@Autowired UserPermissions userPermissions,
@Autowired AddExperimentToProjectService addExperimentToProjectService,
@@ -106,7 +106,7 @@ public ProjectInformationMain(@Autowired ProjectDetailsComponent projectDetailsC
@Autowired TerminologyService terminologyService,
CancelConfirmationDialogFactory cancelConfirmationDialogFactory,
MessageSourceNotificationFactory messageSourceNotificationFactory) {
- this.projectDetailsComponent = requireNonNull(projectDetailsComponent,
+ this.projectSummaryComponent = requireNonNull(projectSummaryComponent,
"projectDetailsComponent must not be null");
this.experimentListComponent = requireNonNull(experimentListComponent,
"experimentListComponent must not be null");
@@ -140,7 +140,7 @@ public ProjectInformationMain(@Autowired ProjectDetailsComponent projectDetailsC
this.experimentListComponent.addExperimentSelectionListener(this::onExperimentSelectionEvent);
this.experimentListComponent.addAddButtonListener(this::onAddExperimentClicked);
addClassName("project");
- add(projectDetailsComponent, offerListComponent, offerDownload, experimentListComponent,
+ add(projectSummaryComponent, offerListComponent, offerDownload, experimentListComponent,
qualityControlListComponent, qualityControlDownload);
this.terminologyService = terminologyService;
}
@@ -300,7 +300,7 @@ private void onExperimentSelectionEvent(ExperimentSelectionEvent event) {
private void setContext(Context context) {
this.context = context;
- projectDetailsComponent.setContext(context);
+ projectSummaryComponent.setContext(context);
experimentListComponent.setContext(context);
refreshOffers(projectPurchaseService, context.projectId().orElseThrow().value(),
offerListComponent);
diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectDetailsComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectSummaryComponent.java
similarity index 83%
rename from user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectDetailsComponent.java
rename to user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectSummaryComponent.java
index c2ece073d..251fa440d 100644
--- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectDetailsComponent.java
+++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/info/ProjectSummaryComponent.java
@@ -8,11 +8,20 @@
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
+import edu.kit.datamanager.ro_crate.writer.RoCrateWriter;
+import edu.kit.datamanager.ro_crate.writer.ZipWriter;
+import java.io.File;
+import java.io.IOException;
import java.io.Serial;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import life.qbic.application.commons.ApplicationException;
+import life.qbic.datamanager.download.DownloadContentProvider;
+import life.qbic.datamanager.download.DownloadProvider;
+import life.qbic.datamanager.export.TempDirectory;
+import life.qbic.datamanager.export.rocrate.ROCreateBuilder;
import life.qbic.datamanager.security.UserPermissions;
import life.qbic.datamanager.views.Context;
import life.qbic.datamanager.views.general.PageArea;
@@ -42,7 +51,7 @@
*/
@UIScope
@SpringComponent
-public class ProjectDetailsComponent extends PageArea {
+public class ProjectSummaryComponent extends PageArea {
@Serial
private static final long serialVersionUID = -5781313306040217724L;
@@ -55,7 +64,8 @@ public class ProjectDetailsComponent extends PageArea {
private final Div projectManagerField = new Div();
private final Div principalInvestigatorField = new Div();
private final Div responsiblePersonField = new Div();
- private final InformationComponent projectInformationSection = InformationComponent.create("", "");
+ private final InformationComponent projectInformationSection = InformationComponent.create("",
+ "");
private final InformationComponent fundingInformationSection = InformationComponent.create(
"Funding Information", "Information about project funding");
private final InformationComponent collaboratorSection = InformationComponent.create(
@@ -65,13 +75,17 @@ public class ProjectDetailsComponent extends PageArea {
private final transient ContactRepository contactRepository;
private final UserPermissions userPermissions;
private final CancelConfirmationDialogFactory cancelConfirmationDialogFactory;
+ private final ROCreateBuilder roCrateBuilder;
+ private final TempDirectory tempDirectory;
+ private DownloadProvider downloadProvider;
private Context context;
- public ProjectDetailsComponent(@Autowired ProjectInformationService projectInformationService,
+ public ProjectSummaryComponent(@Autowired ProjectInformationService projectInformationService,
@Autowired ExperimentInformationService experimentInformationService,
@Autowired ContactRepository contactRepository,
@Autowired UserPermissions userPermissions,
- CancelConfirmationDialogFactory cancelConfirmationDialogFactory) {
+ CancelConfirmationDialogFactory cancelConfirmationDialogFactory,
+ @Autowired ROCreateBuilder rOCreateBuilder, TempDirectory tempDirectory) {
this.projectInformationService = requireNonNull(projectInformationService,
"projectInformationService must not be null");
this.experimentInformationService = requireNonNull(experimentInformationService,
@@ -84,6 +98,10 @@ public ProjectDetailsComponent(@Autowired ProjectInformationService projectInfor
layoutComponent();
addListenerForNewEditEvent();
addClassName("project-details-component");
+ this.roCrateBuilder = rOCreateBuilder;
+ this.tempDirectory = tempDirectory;
+ downloadProvider = new DownloadProvider(null);
+ add(downloadProvider);
}
private static List