Skip to content
Snippets Groups Projects
Commit 06576559 authored by OZGCloud's avatar OZGCloud
Browse files

Merge pull request 'OZG-3373 Formcycle-errorhandling' (#20) from...

Merge pull request 'OZG-3373 Formcycle-errorhandling' (#20) from OZG-3373-Formcycle-errorhandling into master

Reviewed-on: https://git.ozg-sh.de/ozgcloud-app/formcycle-plugin/pulls/20
parents 5e2648bb adbc8423
Branches
Tags 2.15.1
No related merge requests found
Showing
with 623 additions and 331 deletions
...@@ -13,7 +13,7 @@ pipeline { ...@@ -13,7 +13,7 @@ pipeline {
SH_SUCCESS_STATUS_CODE = 0 SH_SUCCESS_STATUS_CODE = 0
VORGANG_MANAGER_INTERFACE_VERSION="2.0.0" VORGANG_MANAGER_INTERFACE_VERSION="2.0.0"
FORMCYCLE_INTERFACE_VERSION="2.0.0"//readFormCycleInterfaceVersion() FORMCYCLE_INTERFACE_VERSION="2.2.0-SNAPSHOT"//readFormCycleInterfaceVersion()
} }
options { options {
...@@ -37,7 +37,7 @@ pipeline { ...@@ -37,7 +37,7 @@ pipeline {
steps { steps {
script { script {
echo "Using FormCacle Interface Version: ${FORMCYCLE_INTERFACE_VERSION}" echo "Using FORMCYCLE Interface Version: ${FORMCYCLE_INTERFACE_VERSION}"
cloneVorgangManagerRepo() cloneVorgangManagerRepo()
} }
...@@ -57,7 +57,7 @@ pipeline { ...@@ -57,7 +57,7 @@ pipeline {
steps { steps {
configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) {
sh 'mvn --version' sh 'mvn --version'
sh 'mvn -f intelliform-adapter/formcycle-adapter/formcycle-adapter-interface -s $MAVEN_SETTINGS -Djava.version=11 install' sh 'mvn -f eingang-manager/formcycle-adapter/formcycle-adapter-interface -s $MAVEN_SETTINGS -Djava.version=11 install'
} }
} }
} }
...@@ -147,21 +147,25 @@ Void cloneVorgangManagerRepo() { ...@@ -147,21 +147,25 @@ Void cloneVorgangManagerRepo() {
} }
dir("vorgang-manager") { dir("vorgang-manager") {
if ( !isSnapshotVersion([VORGANG_MANAGER_INTERFACE_VERSION]) ) {
sh 'git checkout tags/${VORGANG_MANAGER_INTERFACE_VERSION} -b branch' sh 'git checkout tags/${VORGANG_MANAGER_INTERFACE_VERSION} -b branch'
} }
}
configureGit("vorgang-manager") configureGit("vorgang-manager")
} }
Void cloneEingangAdapterRepo() { Void cloneEingangAdapterRepo() {
withCredentials([usernamePassword(credentialsId: 'jenkins-gitea-access-token', passwordVariable: 'TOKEN', usernameVariable: 'USER')]) { withCredentials([usernamePassword(credentialsId: 'jenkins-gitea-access-token', passwordVariable: 'TOKEN', usernameVariable: 'USER')]) {
sh 'git clone https://${USER}:${TOKEN}@git.ozg-sh.de/mgm/intelliform-adapter.git' sh 'git clone https://${USER}:${TOKEN}@git.ozg-sh.de/ozgcloud-app/eingang-manager.git'
} }
dir("intelliform-adapter") { dir("eingang-manager") {
if ( !isSnapshotVersion([FORMCYCLE_INTERFACE_VERSION]) ) {
sh 'git checkout tags/${FORMCYCLE_INTERFACE_VERSION} -b branch' sh 'git checkout tags/${FORMCYCLE_INTERFACE_VERSION} -b branch'
} }
configureGit("intelliform-adapter") }
configureGit("eingang-manager")
} }
Void configureGit(String directory) { Void configureGit(String directory) {
...@@ -177,3 +181,17 @@ Void configureGit(String directory) { ...@@ -177,3 +181,17 @@ Void configureGit(String directory) {
String readFormCycleInterfaceVersion() { String readFormCycleInterfaceVersion() {
return sh(returnStdout: true, script: 'xmlstarlet sel -N w="http://maven.apache.org/POM/4.0.0" -t -v "//w:project/w:version" -n pom.xml').trim() return sh(returnStdout: true, script: 'xmlstarlet sel -N w="http://maven.apache.org/POM/4.0.0" -t -v "//w:project/w:version" -n pom.xml').trim()
} }
Boolean isSnapshotVersion(List versions) {
return matchRegexVersion(versions, SNAPSHOT_REGEX)
}
Boolean matchRegexVersion(List versions, String regex) {
for (version in versions) {
if ( !(version ==~ regex) ) {
return false
}
}
return true
}
\ No newline at end of file
...@@ -16,11 +16,12 @@ ...@@ -16,11 +16,12 @@
<!-- Version of FORMCYCLE to built against. --> <!-- Version of FORMCYCLE to built against. -->
<xfc.version>7.4.0</xfc.version> <xfc.version>7.4.0</xfc.version>
<formcycle-adapter-interface.version>2.0.0</formcycle-adapter-interface.version> <formcycle-adapter-interface.version>2.2.0-SNAPSHOT</formcycle-adapter-interface.version>
<lombok.version>1.18.26</lombok.version> <lombok.version>1.18.30</lombok.version>
<junit-jupiter.version>5.6.0</junit-jupiter.version> <junit-jupiter.version>5.10.1</junit-jupiter.version>
<mockito.version>5.1.1</mockito.version> <mockito.version>5.8.0</mockito.version>
<assertj.version>3.24.2</assertj.version> <assertj.version>3.25.0</assertj.version>
<htmlcleaner.version>2.29</htmlcleaner.version>
<!-- Maven plugin versions --> <!-- Maven plugin versions -->
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version> <maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
...@@ -29,7 +30,6 @@ ...@@ -29,7 +30,6 @@
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version> <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<fc-server-maven-plugin.version>7.4.0</fc-server-maven-plugin.version> <fc-server-maven-plugin.version>7.4.0</fc-server-maven-plugin.version>
<fc-deploy-plugin-maven-plugin.version>7.0.1</fc-deploy-plugin-maven-plugin.version> <fc-deploy-plugin-maven-plugin.version>7.0.1</fc-deploy-plugin-maven-plugin.version>
<htmlcleaner.version>2.27</htmlcleaner.version>
</properties> </properties>
<!-- Dependencies required by this plugin. --> <!-- Dependencies required by this plugin. -->
...@@ -53,6 +53,12 @@ ...@@ -53,6 +53,12 @@
<groupId>de.ozgcloud.eingang</groupId> <groupId>de.ozgcloud.eingang</groupId>
<artifactId>formcycle-adapter-interface</artifactId> <artifactId>formcycle-adapter-interface</artifactId>
<version>${formcycle-adapter-interface.version}</version> <version>${formcycle-adapter-interface.version}</version>
<exclusions>
<exclusion>
<groupId>com.google.api.grpc</groupId>
<artifactId>proto-google-common-protos</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
...@@ -96,8 +102,6 @@ ...@@ -96,8 +102,6 @@
</dependencies> </dependencies>
<build> <build>
<finalName>${project.artifactId}</finalName>
<plugins> <plugins>
<!-- Configure the compilation process. At least Java 11 is required. --> <!-- Configure the compilation process. At least Java 11 is required. -->
<plugin> <plugin>
...@@ -166,26 +170,6 @@ ...@@ -166,26 +170,6 @@
</configuration> </configuration>
</plugin> </plugin>
<!-- Configure how the JAR is created, including manifest entries -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<formcycle-version-requirement>${xfc.version}</formcycle-version-requirement>
<Build-Timestamp>${maven.build.timestamp}</Build-Timestamp>
<Implementation-Title>${project.groupId}:${project.artifactId}</Implementation-Title>
<Implementation-Vendor-Id>${project.groupId}</Implementation-Vendor-Id>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
<!-- Configure how the final plugin JAR is created. --> <!-- Configure how the final plugin JAR is created. -->
<!-- It needs to be a fat JAR with the dependencies required by this plugin. --> <!-- It needs to be a fat JAR with the dependencies required by this plugin. -->
<plugin> <plugin>
...@@ -206,6 +190,9 @@ ...@@ -206,6 +190,9 @@
</descriptorRefs> </descriptorRefs>
<appendAssemblyId>false</appendAssemblyId> <appendAssemblyId>false</appendAssemblyId>
<archive> <archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries> <manifestEntries>
<Build-Timestamp>${maven.build.timestamp}</Build-Timestamp> <Build-Timestamp>${maven.build.timestamp}</Build-Timestamp>
<Implementation-Vendor-Id>${project.groupId}</Implementation-Vendor-Id> <Implementation-Vendor-Id>${project.groupId}</Implementation-Vendor-Id>
......
package de.ozgcloud.formcycle; package de.ozgcloud.formcycle;
import java.util.Map; import java.util.List;
import org.apache.commons.lang3.StringUtils;
import de.ozgcloud.formcycle.errorhandling.Warning;
import lombok.Builder; import lombok.Builder;
import lombok.Getter;
import lombok.Singular;
@Builder @Builder
@Getter
public class ExecutionResult { public class ExecutionResult {
public static final String VORGANGNUMMER_PROPERTY_KEY = "vorgangnummer";
private final String vorgangnummer; private final String vorgangnummer;
public Map<String, String> get() { @Singular
return Map.of(VORGANGNUMMER_PROPERTY_KEY, StringUtils.trimToEmpty(vorgangnummer)); public List<Warning> warnings;
}
} }
...@@ -39,7 +39,7 @@ class HttpPostRequestBuilder { ...@@ -39,7 +39,7 @@ class HttpPostRequestBuilder {
static final String FORMCYCLE_ENDPOINT = "formData"; static final String FORMCYCLE_ENDPOINT = "formData";
static final String FORM_DATA_KEY = "formData"; static final String FORM_DATA_KEY = "formData";
static final ContentType PROTOBUF_CONTENT_TYPE = ContentType.create("application/protobuf"); static final ContentType PROTOBUF_CONTENT_TYPE = ContentType.create("application/x-protobuf");
static final String ATTACHMENT_KEY = "attachments"; static final String ATTACHMENT_KEY = "attachments";
static final String REPRESENTATION_KEY = "representations"; static final String REPRESENTATION_KEY = "representations";
static final Pattern URL_WITH_ENDPOINT_PATTERN = Pattern.compile("^((https?://)?[^/]+)(/" + FORMCYCLE_ENDPOINT + ")$"); static final Pattern URL_WITH_ENDPOINT_PATTERN = Pattern.compile("^((https?://)?[^/]+)(/" + FORMCYCLE_ENDPOINT + ")$");
......
...@@ -26,18 +26,13 @@ import static java.util.Objects.*; ...@@ -26,18 +26,13 @@ import static java.util.Objects.*;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.*;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHost; import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope; import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.routing.HttpRoutePlanner; import org.apache.http.conn.routing.HttpRoutePlanner;
...@@ -47,12 +42,12 @@ import org.apache.http.impl.client.CloseableHttpClient; ...@@ -47,12 +42,12 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.DefaultProxyRoutePlanner; import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.ozgcloud.eingang.formcycle.FormCycleConfirmationResponse; import de.ozgcloud.eingang.formcycle.FormCycleConfirmationResponse;
import de.ozgcloud.eingang.formcycle.FormCycleFormData; import de.ozgcloud.eingang.formcycle.FormCycleFormData;
import de.ozgcloud.formcycle.attachment.FormcycleAttachment; import de.ozgcloud.formcycle.attachment.FormcycleAttachment;
import de.ozgcloud.formcycle.client.OzgCloudResponseHandler;
import de.ozgcloud.formcycle.errorhandling.OzgCloudRequestException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
...@@ -60,10 +55,11 @@ public class OzgCloudFormDataHttpClient { ...@@ -60,10 +55,11 @@ public class OzgCloudFormDataHttpClient {
static final int HTTPCLIENT_TIMEOUT = 10 * 60 * 1000; static final int HTTPCLIENT_TIMEOUT = 10 * 60 * 1000;
private static final Logger LOG = LoggerFactory.getLogger(OzgCloudFormDataHttpClient.class);
private final OzgCloudConfig config; private final OzgCloudConfig config;
private final SystemPropertiesProvider systemPropertiesProvider; private final SystemPropertiesProvider systemPropertiesProvider;
private final PortParser portParser;
private final OzgCloudResponseHandler responseHandler;
private Optional<ProxyConfig> systemProxyConfig; private Optional<ProxyConfig> systemProxyConfig;
private BasicCredentialsProvider credentialsProvider; private BasicCredentialsProvider credentialsProvider;
...@@ -71,15 +67,15 @@ public class OzgCloudFormDataHttpClient { ...@@ -71,15 +67,15 @@ public class OzgCloudFormDataHttpClient {
private HttpHost proxyHost; private HttpHost proxyHost;
public FormCycleConfirmationResponse send(FormCycleFormData formData, Collection<FormcycleAttachment> attachments, public FormCycleConfirmationResponse send(FormCycleFormData formData, Collection<FormcycleAttachment> attachments,
Collection<FormcycleAttachment> representations) throws IOException { Collection<FormcycleAttachment> representations) {
try ( try (var closeableHttpClient = createCloseableClient()) {
var closeableHttpClient = createCloseableClient(); return closeableHttpClient.execute(buildPostRequest(formData, attachments, representations), responseHandler, getContext());
var closeableResponse = closeableHttpClient.execute(buildPostRequest(formData, attachments, representations), getContext())) { } catch (OzgCloudRequestException e) {
return parseResponse(closeableResponse); e.setOzgCloudConfig(config);
} catch (Exception e) {
LOG.info("Used OzgCloudConfig: {}", config);
throw e; throw e;
} catch (IOException e) {
throw new OzgCloudRequestException("Cannot send data to formcycle adapter in OZG cloud.", config, e);
} }
} }
...@@ -101,7 +97,7 @@ public class OzgCloudFormDataHttpClient { ...@@ -101,7 +97,7 @@ public class OzgCloudFormDataHttpClient {
ProxyConfig getProxyConfig() { ProxyConfig getProxyConfig() {
var proxyConfig = config.getProxyConfig(); var proxyConfig = config.getProxyConfig();
if (isNoneBlank(proxyConfig.getHost())) { if (nonNull(proxyConfig) && isNoneBlank(proxyConfig.getHost())) {
return proxyConfig; return proxyConfig;
} }
if (isNull(systemProxyConfig)) { if (isNull(systemProxyConfig)) {
...@@ -123,7 +119,7 @@ public class OzgCloudFormDataHttpClient { ...@@ -123,7 +119,7 @@ public class OzgCloudFormDataHttpClient {
} }
return ProxyConfig.builder() return ProxyConfig.builder()
.host(systemProxyHost) .host(systemProxyHost)
.port(Utils.parsePort(systemPropertiesProvider.getHttpsProxyPort())) .port(portParser.parse(systemPropertiesProvider.getHttpsProxyPort()))
.user(systemPropertiesProvider.getHttpsProxyUser()) .user(systemPropertiesProvider.getHttpsProxyUser())
.password(systemPropertiesProvider.getHttpsProxyPassword()) .password(systemPropertiesProvider.getHttpsProxyPassword())
.build(); .build();
...@@ -136,7 +132,7 @@ public class OzgCloudFormDataHttpClient { ...@@ -136,7 +132,7 @@ public class OzgCloudFormDataHttpClient {
} }
return ProxyConfig.builder() return ProxyConfig.builder()
.host(systemProxyHost) .host(systemProxyHost)
.port(Utils.parsePort(systemPropertiesProvider.getHttpProxyPort())) .port(portParser.parse(systemPropertiesProvider.getHttpProxyPort()))
.user(systemPropertiesProvider.getHttpProxyUser()) .user(systemPropertiesProvider.getHttpProxyUser())
.password(systemPropertiesProvider.getHttpProxyPassword()) .password(systemPropertiesProvider.getHttpProxyPassword())
.build(); .build();
...@@ -204,37 +200,4 @@ public class OzgCloudFormDataHttpClient { ...@@ -204,37 +200,4 @@ public class OzgCloudFormDataHttpClient {
return clientContext; return clientContext;
} }
FormCycleConfirmationResponse parseResponse(CloseableHttpResponse response) throws IOException {
checkResponseStatus(response);
InputStream responseContent = response.getEntity().getContent();
return FormCycleConfirmationResponse.parseFrom(responseContent);
}
private void checkResponseStatus(CloseableHttpResponse response) {
var statusLine = response.getStatusLine();
if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
throw new UnexpectedStatusCodeException(response);
}
}
static class UnexpectedStatusCodeException extends RuntimeException {
private static final String MESSAGE = "Unexcepted Status received: %s";
UnexpectedStatusCodeException(CloseableHttpResponse response) {
super(buildFullMessage(response));
}
private static String buildFullMessage(CloseableHttpResponse response) {
try {
StringBuilder messageBuilder = new StringBuilder(String.format(MESSAGE, response.getStatusLine())).append("\n");
IOUtils.readLines(response.getEntity().getContent(), StandardCharsets.UTF_8)
.stream().forEach(line -> messageBuilder.append(line).append("\n"));
return messageBuilder.toString();
} catch (IOException e) {
return MESSAGE + "\nError reading body";
}
}
}
} }
...@@ -22,34 +22,31 @@ ...@@ -22,34 +22,31 @@
*/ */
package de.ozgcloud.formcycle; package de.ozgcloud.formcycle;
import static de.ozgcloud.formcycle.errorhandling.NodeThrewExceptionFactory.*;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.slf4j.Logger; import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.LoggerFactory;
import de.ozgcloud.formcycle.attachment.AttachmentMapper; import de.ozgcloud.formcycle.attachment.AttachmentMapper;
import de.ozgcloud.formcycle.errorhandling.EWorkflowElementNodeError; import de.ozgcloud.formcycle.client.OzgCloudResponseHandler;
import de.ozgcloud.formcycle.errorhandling.EWorkflowElementNodeSoftError; import de.ozgcloud.formcycle.errorhandling.OzgPluginError;
import de.ozgcloud.formcycle.errorhandling.OzgPluginSoftError;
import de.ozgcloud.formcycle.errorhandling.NodeThrewExceptionFactory; import de.ozgcloud.formcycle.errorhandling.NodeThrewExceptionFactory;
import de.ozgcloud.formcycle.errorhandling.UserErrorDispatcher; import de.ozgcloud.formcycle.errorhandling.OzgCloudRequestException;
import de.ozgcloud.formcycle.errorhandling.TechnicalException;
import de.ozgcloud.formcycle.formdata.OzgCloudFormDataMapper; import de.ozgcloud.formcycle.formdata.OzgCloudFormDataMapper;
import de.ozgcloud.formcycle.formdata.PluginFormDataAdapter; import de.ozgcloud.formcycle.formdata.PluginFormDataAdapter;
import de.xima.fc.beans.interfaces.IGuiIcon; import de.xima.fc.beans.interfaces.IGuiIcon;
import de.xima.fc.entities.Attachment; import de.xima.fc.entities.Attachment;
import de.xima.fc.exceptions.AbstractAbruptCompletionException; import de.xima.fc.exceptions.AbstractAbruptCompletionException;
import de.xima.fc.exceptions.NodeThrewException;
import de.xima.fc.interfaces.plugin.lifecycle.IPluginInitializeData; import de.xima.fc.interfaces.plugin.lifecycle.IPluginInitializeData;
import de.xima.fc.interfaces.workflow.elements.IElementHelpLocation;
import de.xima.fc.interfaces.workflow.execution.IFileValueDescriptor; import de.xima.fc.interfaces.workflow.execution.IFileValueDescriptor;
import de.xima.fc.interfaces.workflow.execution.INormalCompletionResult; import de.xima.fc.interfaces.workflow.execution.INormalCompletionResult;
import de.xima.fc.interfaces.workflow.execution.IWorkflowExecutionEnvironmentData;
import de.xima.fc.interfaces.workflow.mixin.IElementCategory; import de.xima.fc.interfaces.workflow.mixin.IElementCategory;
import de.xima.fc.interfaces.workflow.mixin.IKeyValueSummarizableNode; import de.xima.fc.interfaces.workflow.mixin.IKeyValueSummarizableNode;
import de.xima.fc.interfaces.workflow.mixin.ISummaryKeyValueModel; import de.xima.fc.interfaces.workflow.mixin.ISummaryKeyValueModel;
...@@ -61,39 +58,34 @@ import de.xima.fc.interfaces.workflow.value.IValueBuilder; ...@@ -61,39 +58,34 @@ import de.xima.fc.interfaces.workflow.value.IValueBuilder;
import de.xima.fc.interfaces.workflow.value.IValueDescriptor; import de.xima.fc.interfaces.workflow.value.IValueDescriptor;
import de.xima.fc.interfaces.workflow.value.IValueDescriptorFactory; import de.xima.fc.interfaces.workflow.value.IValueDescriptorFactory;
import de.xima.fc.mdl.ui.GuiIcon; import de.xima.fc.mdl.ui.GuiIcon;
import de.xima.fc.workflow.ElementHelpLocation;
import de.xima.fc.workflow.FileValueDescriptor; import de.xima.fc.workflow.FileValueDescriptor;
import de.xima.fc.workflow.SummaryKeyValueModelBuilder; import de.xima.fc.workflow.SummaryKeyValueModelBuilder;
import de.xima.fc.workflow.enums.EWorkflowElementCategory; import de.xima.fc.workflow.enums.EWorkflowElementCategory;
import de.xima.fc.workflow.mixin.INodePropertyPluginBean;
import de.xima.fc.workflow.mixin.IPluginActionNodeHandler; import de.xima.fc.workflow.mixin.IPluginActionNodeHandler;
public final class WorkflowElementNodePlugin public final class OzgCloudPlugin
implements IPluginActionNodeHandler<WorkflowElementNodeProps>, IKeyValueSummarizableNode<WorkflowElementNodeProps> { implements IPluginActionNodeHandler<OzgPluginWorkflowNodeProperties>, IKeyValueSummarizableNode<OzgPluginWorkflowNodeProperties> {
private static final Logger LOG = LoggerFactory.getLogger(WorkflowElementNodePlugin.class); static final String VORGANGNUMMER_PROPERTY_KEY = "vorgangnummer";
private final transient PortParser portParser = new PortParser();
private transient OzgPluginPropertiesSupplier ozgPluginPropertiesSupplier;
private transient IPluginInitializeData initializeData; private transient IPluginInitializeData initializeData;
private transient PluginPropertiesMapper pluginPropertiesMapper;
@Override @Override
public void initialize(IPluginInitializeData initializeData) { public void initialize(IPluginInitializeData initializeData) {
this.initializeData = initializeData; this.initializeData = initializeData;
this.pluginPropertiesMapper = createPropertiesMapper(); this.ozgPluginPropertiesSupplier = new OzgPluginPropertiesSupplier(initializeData, portParser);
}
PluginPropertiesMapper createPropertiesMapper() {
return new PluginPropertiesMapper(new UserErrorDispatcher());
} }
@Override @Override
public String getPropertiesViewXhtmlName() { public String getPropertiesViewXhtmlName() {
return "WorkflowElementNode.xhtml"; return "OzgCloudPlugin.xhtml";
} }
@Override @Override
public String getName() { public String getName() {
return "de.ozgcloud.formcycle.WorkflowElementNodePlugin"; return "de.ozgcloud.formcycle.OzgCloudPlugin";
} }
@Override @Override
...@@ -110,15 +102,6 @@ public final class WorkflowElementNodePlugin ...@@ -110,15 +102,6 @@ public final class WorkflowElementNodePlugin
return EWorkflowElementCategory.SUB_ACTION_COMMON; return EWorkflowElementCategory.SUB_ACTION_COMMON;
} }
@Override
public IElementHelpLocation getHelpPageLocation(Locale locale) throws IOException {
// Optional HTML page documenting how to use this workflow node.
// Use ElementHelpLocation#forExternalLink if you host the help page on an external server
// Use ElementHelpLocation#forOfficialHelpPage when you host the help page on the FORMCYCLE wiki
final var path = String.format("WEB-INF/properties/WorkflowElementNodeHelp_%s.html", locale.toLanguageTag());
return ElementHelpLocation.forHtmlFile(getClass().getClassLoader(), path, StandardCharsets.UTF_8);
}
@Override @Override
public IGuiIcon getPrototypeIcon(IGetElementPrototypesParams params) { public IGuiIcon getPrototypeIcon(IGetElementPrototypesParams params) {
// Optional icon for this workflow node. Shown next to the node's name. // Optional icon for this workflow node. Shown next to the node's name.
...@@ -130,7 +113,7 @@ public final class WorkflowElementNodePlugin ...@@ -130,7 +113,7 @@ public final class WorkflowElementNodePlugin
// Implement this method when this node can return files. // Implement this method when this node can return files.
// This node can then be selected by other actions that require files. // This node can then be selected by other actions that require files.
// If this node does not provide files, remove this method // If this node does not provide files, remove this method
return new FileValueDescriptor("WorkflowElementNodePlugin.files"); return new FileValueDescriptor("OzgCloudPlugin.files");
} }
@Override @Override
...@@ -140,7 +123,7 @@ public final class WorkflowElementNodePlugin ...@@ -140,7 +123,7 @@ public final class WorkflowElementNodePlugin
// [%$Action.RESULT.name%] // [%$Action.RESULT.name%]
// [%$Action.RESULT.items.name%] // [%$Action.RESULT.items.name%]
// [%$Action.RESULT.items.state%] // [%$Action.RESULT.items.state%]
return factory.recordBuilder().requiredProperty(ExecutionResult.VORGANGNUMMER_PROPERTY_KEY, factory.string()).build(); return factory.recordBuilder().requiredProperty(VORGANGNUMMER_PROPERTY_KEY, factory.string()).build();
} }
@Override @Override
...@@ -152,113 +135,92 @@ public final class WorkflowElementNodePlugin ...@@ -152,113 +135,92 @@ public final class WorkflowElementNodePlugin
// [%$Action.ERROR.file%] // [%$Action.ERROR.file%]
// [%$Action.ERROR.requiredPermissions[0]%] // [%$Action.ERROR.requiredPermissions[0]%]
// [%$Action.ERROR.message%] // [%$Action.ERROR.message%]
// TODO das ist dafür gedacht, Fehler an der Oberfläche zu zeigen, was wir eigentlich nicht brauchen man könnte es einfach löschen
return factory.unionStringBuilder() // return factory.unionStringBuilder() //
.add(EWorkflowElementNodeError.OZGCLOUD_CANNOT_SEND.name(), f -> f.recordBuilder() .add(OzgPluginError.INTERNAL_ERROR.name(),
.requiredProperty(NodeThrewExceptionFactory.ERROR_MESSAGE_KEY, f.string()) factory.recordBuilder().requiredProperty(EXCEPTION_ID_KEY, factory.string()).build())
.build() .add(OzgPluginError.OZGCLOUD_REQUEST_FAILED.name(),
) factory.recordBuilder().requiredProperty(EXCEPTION_ID_KEY, factory.string()).build())
.add(EWorkflowElementNodeError.PLUGIN_CANNOT_GET_ATTACHMENT.name(), f -> f.recordBuilder()
.requiredProperty(NodeThrewExceptionFactory.ERROR_MESSAGE_KEY, f.string())
.build()
)
.add(EWorkflowElementNodeError.PLUGIN_CONFIG_ERROR.name(), f -> f.recordBuilder()
.requiredProperty(NodeThrewExceptionFactory.ERROR_MESSAGE_KEY, v -> v.string())
.build())
.build(); .build();
} }
@Override @Override
public IUnionValueDescriptor<String> getSoftErrorValueDescriptor(IValueDescriptorFactory factory) { public ISummaryKeyValueModel getElementSummaryKeyValueModel(IGetElementSummaryParams<OzgPluginWorkflowNodeProperties> params) {
// For each error that can occur, additional data can be provided
// Users can access this data e.g. via placeholders, for example:
// [%$Action.ERROR_CODE%]
// [%$Action.ERROR_MESSAGE%]
// [%$Action.ERROR.file%]
// [%$Action.ERROR.requiredPermissions[0]%]
// [%$Action.ERROR.message%]
return factory.unionStringBuilder() //
.add(EWorkflowElementNodeSoftError.WRONG_HOST_NAME.name(), f -> f.recordBuilder() //
.requiredProperty("expectedHostName", v -> v.string()) //
.build()) //
.add(EWorkflowElementNodeSoftError.USER_DOES_NOT_EXIST.name(), f -> f.string()) //
.add(EWorkflowElementNodeError.PLUGIN_CONFIG_ERROR.name(), f -> f.recordBuilder()
.requiredProperty(NodeThrewExceptionFactory.ERROR_MESSAGE_KEY, v -> v.string())
.build())
.build();
}
@Override
public ISummaryKeyValueModel getElementSummaryKeyValueModel(IGetElementSummaryParams<WorkflowElementNodeProps> params) {
// A simple summary table with the most important fields from the properties // A simple summary table with the most important fields from the properties
// This is shown when the user clicks on the info icon on a node in the designer. // This is shown when the user clicks on the info icon on a node in the designer.
return new SummaryKeyValueModelBuilder().build(); return new SummaryKeyValueModelBuilder().build();
} }
@Override @Override
public INormalCompletionResult execute(INodeExecutionParams<WorkflowElementNodeProps> params) throws AbstractAbruptCompletionException { public INormalCompletionResult execute(INodeExecutionParams<OzgPluginWorkflowNodeProperties> params) throws AbstractAbruptCompletionException {
var workflowExecutionEnvironmentData = params.getWorkflowContext().env(); try {
var formProperties = params.getData(); return buildSuccessResult(createPluginExecutor(params).execute(), params);
PluginFormDataAdapter pluginFormDataAdapter = new PluginFormDataAdapter(workflowExecutionEnvironmentData, } catch (OzgCloudRequestException e) {
formProperties.getOrganisationsEinheitId()); throw createExceptionFactory(params).createOzgCloudRequestException(e);
var ozgCloudFormDataMapper = new OzgCloudFormDataMapper(); } catch (TechnicalException e) {
var exceptionFactory = new NodeThrewExceptionFactory(params.throwingException()); throw createExceptionFactory(params).createInternalException(e);
var attachmentsAdapter = new AttachmentMapper(exceptionFactory); } catch (Exception e) {
var pluginExecutor = new WorkflowElementNodeExecutor(ozgCloudFormDataMapper, createOzgClient(formProperties, exceptionFactory), throw createExceptionFactory(params).createInternalException("Unexpected error occurred", e);
exceptionFactory, }
attachmentsAdapter, pluginFormDataAdapter, createAttachmentSupplier(workflowExecutionEnvironmentData)); }
var executionResult = pluginExecutor.execute();
return params.normalResult().success(executionResult.get()).build(); OzgPluginExecutor createPluginExecutor(INodeExecutionParams<OzgPluginWorkflowNodeProperties> params) {
return new OzgPluginExecutor(new OzgCloudFormDataMapper(), createOzgCloudClient(params), new AttachmentMapper(),
createFormDataAdapter(params), createAttachmentSupplier(params));
}
OzgCloudFormDataHttpClient createOzgCloudClient(INodeExecutionParams<OzgPluginWorkflowNodeProperties> params) {
return new OzgCloudFormDataHttpClient(buildOzgCloudConfig(params.getData()), new SystemPropertiesProvider(), portParser,
new OzgCloudResponseHandler(new ObjectMapper()));
} }
// TODO hier braucht man die Factory eigentlich nicht, es würde reichen einfaches Exception zu werfen OzgCloudConfig buildOzgCloudConfig(OzgPluginWorkflowNodeProperties formProperties) {
OzgCloudFormDataHttpClient createOzgClient(WorkflowElementNodeProps formProperties, NodeThrewExceptionFactory exceptionFactory) var ozgCloudConfigBuilder = OzgCloudConfig.builder().eingangsAdapterUrl(getEingangsAdapterUrl(formProperties));
throws NodeThrewException { ozgPluginPropertiesSupplier.getProxyProperties().ifPresent(ozgCloudConfigBuilder::proxyConfig);
var ozgCloudConfig = buildOzgCloudConfig(formProperties, exceptionFactory); return ozgCloudConfigBuilder.build();
return new OzgCloudFormDataHttpClient(ozgCloudConfig, new SystemPropertiesProvider());
} }
OzgCloudConfig buildOzgCloudConfig(WorkflowElementNodeProps formProperties, NodeThrewExceptionFactory exceptionFactory) String getEingangsAdapterUrl(OzgPluginWorkflowNodeProperties formProperties) {
throws NodeThrewException { var eingangsAdapterUrl = formProperties.isUseCustomEingangsAdapterUrl()
var eingangsAdapterUrl = getEingangsAdapterUrl(formProperties); ? formProperties.getEingangsAdapterUrl()
: ozgPluginPropertiesSupplier.getEingangsAdapterUrl();
if (isBlank(eingangsAdapterUrl)) { if (isBlank(eingangsAdapterUrl)) {
throw exceptionFactory.createConfigurationException("Eingangsadapter URL cannot be blank"); throw new TechnicalException("Eingangsadapter URL cannot be blank");
}
return eingangsAdapterUrl;
} }
return OzgCloudConfig.builder() PluginFormDataAdapter createFormDataAdapter(INodeExecutionParams<OzgPluginWorkflowNodeProperties> params) {
.eingangsAdapterUrl(eingangsAdapterUrl) return new PluginFormDataAdapter(params.getWorkflowContext().env(), params.getData().getOrganisationsEinheitId());
.proxyConfig(pluginPropertiesMapper.mapProxyProperties(initializeData.getProperties()))
.build();
} }
String getEingangsAdapterUrl(WorkflowElementNodeProps formProperties) { private Supplier<List<Attachment>> createAttachmentSupplier(INodeExecutionParams<OzgPluginWorkflowNodeProperties> params) {
if (formProperties.isUseCustomEingangsAdapterUrl()) { var formRecord = params.getWorkflowContext().env().getFormRecord();
return formProperties.getEingangsAdapterUrl(); return () -> Optional.ofNullable(formRecord.getAttachments()).orElse(List.of());
} }
return pluginPropertiesMapper.getEingangsAdapterUrl(initializeData.getProperties());
NodeThrewExceptionFactory createExceptionFactory(INodeExecutionParams<OzgPluginWorkflowNodeProperties> params) {
return new NodeThrewExceptionFactory(params.throwingException());
} }
private Supplier<List<Attachment>> createAttachmentSupplier(IWorkflowExecutionEnvironmentData environmentData) { INormalCompletionResult buildSuccessResult(ExecutionResult pluginExecutionResult, INodeExecutionParams<OzgPluginWorkflowNodeProperties> params) {
return () -> Optional.ofNullable(environmentData.getFormRecord().getAttachments()).orElse(List.of()); var resultBuilder = params.normalResult();
resultBuilder.success(Map.of(VORGANGNUMMER_PROPERTY_KEY, pluginExecutionResult.getVorgangnummer()));
pluginExecutionResult.getWarnings()
.forEach(warning -> resultBuilder.softError(warning.getErrorCode(), warning.getMessage(), warning.getValue(), warning.getCause()));
return resultBuilder.build();
} }
@Override @Override
public Class<EWorkflowElementNodeError> getErrorCodeClass() { public Class<OzgPluginError> getErrorCodeClass() {
// Class of the enumeration with the possible types of errors that may occur. // Class of the enumeration with the possible types of errors that may occur.
return EWorkflowElementNodeError.class; return OzgPluginError.class;
} }
@Override @Override
public Class<EWorkflowElementNodeSoftError> getSoftErrorCodeClass() { public Class<OzgPluginSoftError> getSoftErrorCodeClass() {
// Class of the enumeration with the possible types of soft errors that may occur. // Class of the enumeration with the possible types of soft errors that may occur.
return EWorkflowElementNodeSoftError.class; return OzgPluginSoftError.class;
}
@Override
public Class<? extends INodePropertyPluginBean<WorkflowElementNodeProps>> getMainPluginBeanClass() {
// List of beans that should be available in the XHTML page.
// Often you do not need a bean, in this case you can just remove this method.
return WorkflowElementNodeBean.class;
} }
@Override @Override
......
...@@ -36,7 +36,7 @@ import de.xima.fc.plugin.config.IBundleProperties; ...@@ -36,7 +36,7 @@ import de.xima.fc.plugin.config.IBundleProperties;
import de.xima.fc.plugin.models.config.BundleConfigGroupItem; import de.xima.fc.plugin.models.config.BundleConfigGroupItem;
import de.xima.fc.plugin.models.config.BundleConfigParam; import de.xima.fc.plugin.models.config.BundleConfigParam;
public class PluginBundleProperties implements IBundleProperties { public class OzgPluginBundleProperties implements IBundleProperties {
private static final boolean CRYPTIC_VALUE = true; private static final boolean CRYPTIC_VALUE = true;
private static final boolean MANDATORY = true; private static final boolean MANDATORY = true;
......
...@@ -24,7 +24,6 @@ package de.ozgcloud.formcycle; ...@@ -24,7 +24,6 @@ package de.ozgcloud.formcycle;
import static java.util.Objects.*; import static java.util.Objects.*;
import java.io.IOException;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
...@@ -38,45 +37,43 @@ import org.slf4j.LoggerFactory; ...@@ -38,45 +37,43 @@ import org.slf4j.LoggerFactory;
import de.ozgcloud.formcycle.attachment.AttachmentMapper; import de.ozgcloud.formcycle.attachment.AttachmentMapper;
import de.ozgcloud.formcycle.attachment.AttachmentType; import de.ozgcloud.formcycle.attachment.AttachmentType;
import de.ozgcloud.formcycle.attachment.FormcycleAttachment; import de.ozgcloud.formcycle.attachment.FormcycleAttachment;
import de.ozgcloud.formcycle.errorhandling.NodeThrewExceptionFactory; import de.ozgcloud.formcycle.errorhandling.OzgPluginSoftError;
import de.ozgcloud.formcycle.errorhandling.Warning;
import de.ozgcloud.formcycle.formdata.OzgCloudFormDataMapper; import de.ozgcloud.formcycle.formdata.OzgCloudFormDataMapper;
import de.ozgcloud.formcycle.formdata.PluginFormDataAdapter; import de.ozgcloud.formcycle.formdata.PluginFormDataAdapter;
import de.xima.fc.entities.Attachment; import de.xima.fc.entities.Attachment;
import de.xima.fc.exceptions.AbstractAbruptCompletionException;
import de.xima.fc.exceptions.NodeThrewException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public final class WorkflowElementNodeExecutor { public final class OzgPluginExecutor {
private static final Logger LOG = LoggerFactory.getLogger(WorkflowElementNodeExecutor.class); private static final Logger LOG = LoggerFactory.getLogger(OzgPluginExecutor.class);
private final OzgCloudFormDataMapper ozgCloudFormDataMapper; private final OzgCloudFormDataMapper ozgCloudFormDataMapper;
private final OzgCloudFormDataHttpClient ozgHttpClient; private final OzgCloudFormDataHttpClient ozgHttpClient;
private final NodeThrewExceptionFactory exceptionFactory;
private final AttachmentMapper attachmentMapper; private final AttachmentMapper attachmentMapper;
private final PluginFormDataAdapter pluginFormDataAdapter; private final PluginFormDataAdapter pluginFormDataAdapter;
private final Supplier<List<Attachment>> attachmentsSupplier; private final Supplier<List<Attachment>> attachmentsSupplier;
public ExecutionResult execute() throws AbstractAbruptCompletionException { public ExecutionResult execute() {
LOG.debug("Executing plugin WorkflowElementNodePlugin"); LOG.debug("Executing plugin WorkflowElementNodePlugin");
var formData = pluginFormDataAdapter.readFormData(); var formData = pluginFormDataAdapter.readFormData();
var formcycleFormData = ozgCloudFormDataMapper.map(formData);
var attachments = getAttachedFiles(formData.getAttachmentUuids()); var attachments = getAttachedFiles(formData.getAttachmentUuids());
var resultBuilder = ExecutionResult.builder();
if (isNull(attachments.get(AttachmentType.REPRESENTATION))) {
resultBuilder.warning(
buildWarning("Representation is missing. Ensure workflow is configured to attach form view to form data."));
}
try { var formcycleFormData = ozgCloudFormDataMapper.map(formData);
var formCycleConfirmationResponse = ozgHttpClient.send(formcycleFormData, attachments.get(AttachmentType.ATTACHMENT), var formCycleConfirmationResponse = ozgHttpClient.send(formcycleFormData, attachments.get(AttachmentType.ATTACHMENT),
attachments.get(AttachmentType.REPRESENTATION)); attachments.get(AttachmentType.REPRESENTATION));
LOG.debug("Formcycle adapter response: {}", formCycleConfirmationResponse); LOG.debug("Formcycle adapter response: {}", formCycleConfirmationResponse);
return ExecutionResult.builder().vorgangnummer(formCycleConfirmationResponse.getVorgangNummer()).build(); return resultBuilder.vorgangnummer(formCycleConfirmationResponse.getVorgangNummer()).build();
} catch (IOException | RuntimeException e) {
throw exceptionFactory.createCannotSendException("Cannot send data to formcycle adapter in OZG cloud.", e);
}
} }
Map<AttachmentType, Set<FormcycleAttachment>> getAttachedFiles(Set<String> uploadFieldUuids) throws NodeThrewException { Map<AttachmentType, Set<FormcycleAttachment>> getAttachedFiles(Set<String> uploadFieldUuids) {
var resultMap = new EnumMap<AttachmentType, Set<FormcycleAttachment>>(AttachmentType.class); var resultMap = new EnumMap<AttachmentType, Set<FormcycleAttachment>>(AttachmentType.class);
for (Attachment attachment : attachmentsSupplier.get()) { for (Attachment attachment : attachmentsSupplier.get()) {
if (uploadFieldUuids.contains(attachment.getUUID())) { if (uploadFieldUuids.contains(attachment.getUUID())) {
...@@ -85,10 +82,10 @@ public final class WorkflowElementNodeExecutor { ...@@ -85,10 +82,10 @@ public final class WorkflowElementNodeExecutor {
} }
resultMap.computeIfAbsent(AttachmentType.REPRESENTATION, e -> new HashSet<>()).add(attachmentMapper.map(attachment)); resultMap.computeIfAbsent(AttachmentType.REPRESENTATION, e -> new HashSet<>()).add(attachmentMapper.map(attachment));
} }
if (isNull(resultMap.get(AttachmentType.REPRESENTATION))) {
LOG.warn("Representation is missing. Activate checkbox 'An den Vorgang anhaengen' in 'Word-Datei befuellen' plugin");
}
return resultMap; return resultMap;
} }
Warning buildWarning(String message) {
return Warning.builder().errorCode(OzgPluginSoftError.MISSING_REPRESENTATION).message(message).build();
}
} }
...@@ -26,21 +26,40 @@ package de.ozgcloud.formcycle; ...@@ -26,21 +26,40 @@ package de.ozgcloud.formcycle;
import static java.util.Objects.*; import static java.util.Objects.*;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.*;
import java.util.Optional;
import java.util.Properties; import java.util.Properties;
import de.ozgcloud.formcycle.errorhandling.UserErrorDispatcher; import de.ozgcloud.formcycle.errorhandling.TechnicalException;
import de.xima.fc.interfaces.plugin.lifecycle.IPluginInitializeData;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public class PluginPropertiesMapper { public class OzgPluginPropertiesSupplier {
private final UserErrorDispatcher userErrorDispatcher; private final IPluginInitializeData pluginData;
private final PortParser portParser;
public String getEingangsAdapterUrl(Properties pluginProperties) { public String getEingangsAdapterUrl() {
return pluginProperties.getProperty(OzgCloudConfig.KEY_EINGANGSADAPTER_URL); return pluginData.getProperties().getProperty(OzgCloudConfig.KEY_EINGANGSADAPTER_URL);
} }
public ProxyConfig mapProxyProperties(Properties pluginProperties) { public Optional<ProxyConfig> getProxyProperties() {
var pluginProperties = pluginData.getProperties();
validateProxyConfig(pluginProperties);
var proxyHostProperty = pluginProperties.getProperty(ProxyConfig.KEY_PROXY_HOST);
return isBlank(proxyHostProperty) ? Optional.empty() : Optional.of(buildProxyConfig(pluginProperties));
}
void validateProxyConfig(Properties pluginProperties) {
var proxyHostProperty = pluginProperties.getProperty(ProxyConfig.KEY_PROXY_HOST);
var port = getPort(pluginProperties);
if ((isBlank(proxyHostProperty) && nonNull(port))
|| (isNotBlank(proxyHostProperty) && isNull(port))) {
throw new TechnicalException("Proxy configuration is invalid. Both host and port must be set or none of them.");
}
}
ProxyConfig buildProxyConfig(Properties pluginProperties) {
return ProxyConfig.builder() return ProxyConfig.builder()
.host(pluginProperties.getProperty(ProxyConfig.KEY_PROXY_HOST)) .host(pluginProperties.getProperty(ProxyConfig.KEY_PROXY_HOST))
.port(getPort(pluginProperties)) .port(getPort(pluginProperties))
...@@ -51,14 +70,7 @@ public class PluginPropertiesMapper { ...@@ -51,14 +70,7 @@ public class PluginPropertiesMapper {
Integer getPort(Properties pluginProperties) { Integer getPort(Properties pluginProperties) {
var portProperty = pluginProperties.getProperty(ProxyConfig.KEY_PROXY_PORT); var portProperty = pluginProperties.getProperty(ProxyConfig.KEY_PROXY_PORT);
if (isBlank(portProperty)) { return isBlank(portProperty) ? null : portParser.parse(portProperty);
return null;
}
var port = Utils.parsePort(portProperty);
if (isNull(port)) {
userErrorDispatcher.sendWrongProxyPortFormatMessage();
}
return port;
} }
} }
...@@ -54,7 +54,7 @@ import lombok.Setter; ...@@ -54,7 +54,7 @@ import lombok.Setter;
withValues = {"true"}, withValues = {"true"},
requiredProperties = {"eingangsAdapterUrl"} requiredProperties = {"eingangsAdapterUrl"}
) )
public final class WorkflowElementNodeProps extends BaseActionProps { public final class OzgPluginWorkflowNodeProperties extends BaseActionProps {
@NotEmpty @NotEmpty
@Size(max = 20) @Size(max = 20)
......
...@@ -27,17 +27,18 @@ import static org.apache.commons.lang3.StringUtils.*; ...@@ -27,17 +27,18 @@ import static org.apache.commons.lang3.StringUtils.*;
import java.util.Optional; import java.util.Optional;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import lombok.NoArgsConstructor; public class PortParser {
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class Utils {
private static final Pattern PORT_PATTERN = Pattern.compile("\\s*\\d{1,5}\\s*"); private static final Pattern PORT_PATTERN = Pattern.compile("\\s*\\d{1,5}\\s*");
public static Integer parsePort(String portString) { public Integer parse(String portString) {
if (isBlank(portString) || !PORT_PATTERN.matcher(portString).matches()) { if (isBlank(portString)) {
return null; return null;
} }
return Optional.of(Integer.parseInt(portString.trim())).filter(port -> port > 0 && port < 65536).orElse(null); if (!PORT_PATTERN.matcher(portString).matches()) {
throw new IllegalArgumentException("Port must be a number");
}
return Optional.of(Integer.parseInt(portString.trim())).filter(port -> port > 0 && port < 65536)
.orElseThrow(() -> new IllegalArgumentException("Port must be a number between 1 and 65535."));
} }
} }
...@@ -31,7 +31,7 @@ import lombok.ToString; ...@@ -31,7 +31,7 @@ import lombok.ToString;
@Builder @Builder
@Getter @Getter
@ToString(exclude = "password") @ToString
public class ProxyConfig implements Serializable { public class ProxyConfig implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
...@@ -54,6 +54,8 @@ public class ProxyConfig implements Serializable { ...@@ -54,6 +54,8 @@ public class ProxyConfig implements Serializable {
private String host; private String host;
private Integer port; private Integer port;
private String user; private String user;
@ToString.Exclude
private String password; private String password;
} }
/*
* Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.formcycle;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import de.xima.fc.entities.Benutzer;
import de.xima.fc.entities.Mandant;
import de.xima.fc.entities.Projekt;
import de.xima.fc.entities.Vorgang;
import de.xima.fc.workflow.event.AFormRecordEventData;
/**
* Custom event for the workflow trigger that is initiated by the HTTP servlet action plugin when an HTTP request is
* made.
* @author unspecified
*/
@SuppressWarnings("serial")
public final class WorkflowElementEvent extends AFormRecordEventData {
private final String type;
private final Map<String, List<String>> requestParams;
/**
* Creates a new event data instance with the given data.
* @param client Client who owns the form record.
* @param user User who initiated the event.
* @param locale Request locale to use for locale-sensitive operations.
* @param project Project to which the form record belongs.
* @param formRecord Form record for which to trigger events.
* @param type Custom type of the event, a workflow trigger reacts only to a given type.
* @param requestParams HTTP parameters of the request that triggered this event.
*/
public WorkflowElementEvent(Mandant client, Benutzer user, Locale locale, Projekt project, Vorgang formRecord, String type, Map<String, List<String>> requestParams) {
super(client, user, locale, project, formRecord);
this.requestParams = requestParams;
this.type = type;
}
/**
* @return HTTP parameters of the request that triggered this event.
*/
public Map<String, List<String>> getRequestParams() {
return requestParams;
}
/**
* @return The type of this event. A trigger only reacts to a specific type.
*/
public String getType() {
return type;
}
@Override
public Boolean isAnonymize() {
return false;
}
}
/*
* Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.formcycle;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import javax.faces.view.ViewScoped;
import javax.inject.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.xima.fc.interfaces.plugin.lifecycle.IPluginInitializeBeanData;
import de.xima.fc.interfaces.plugin.lifecycle.helper.IPluginFileHelper;
import de.xima.fc.interfaces.plugin.lifecycle.helper.IPluginResourceHelper;
import de.xima.fc.plugin.exception.FCPluginException;
import de.xima.fc.workflow.mixin.INodePropertyPluginBean;
/**
* Controller bean for the custom UI when editing the properties of the workflow node.
* A bean is not required, often it suffices to add an XHTML page without a bean. This
* class is a stub that that you can expand when you do not a bean, such as when you
* want to enrich your UI with AJAX-enhanced logic.
* @author unspecified
*/
@Named
@ViewScoped
public final class WorkflowElementNodeBean implements INodePropertyPluginBean<WorkflowElementNodeProps> {
private static final Logger LOG = LoggerFactory.getLogger(WorkflowElementNodeBean.class);
private transient AtomicReference<IPluginInitializeBeanData> initializeBeanData;
/**
* A method that could be used the the action for some command button in your UI.
*/
public void controllerAction() {
// Add custom logic here when the button was pressed...
LOG.info("Controller action was called");
}
@Override
public IPluginFileHelper getFileHelper() {
return initializeBeanData.get().getFileHelper();
}
@Override
public Properties getProperties() {
return initializeBeanData.get().getProperties();
}
@Override
public IPluginResourceHelper getResourceHelper() {
return initializeBeanData.get().getResourceHelper();
}
@Override
public void initialize(IPluginInitializeBeanData initializeBeanData) throws FCPluginException {
this.initializeBeanData = new AtomicReference<>(initializeBeanData);
}
}
...@@ -30,27 +30,22 @@ import java.net.URLConnection; ...@@ -30,27 +30,22 @@ import java.net.URLConnection;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
import de.ozgcloud.formcycle.errorhandling.NodeThrewExceptionFactory; import de.ozgcloud.formcycle.errorhandling.TechnicalException;
import de.xima.fc.entities.Attachment; import de.xima.fc.entities.Attachment;
import de.xima.fc.exceptions.NodeThrewException;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class AttachmentMapper { public class AttachmentMapper {
static final String CANNOT_READ_ATTACHED_FILE = "Cannot read attached file. It doesn't exist."; static final String CANNOT_READ_ATTACHED_FILE = "Cannot read attached file. It doesn't exist.";
static final String FILENAME_IS_NULL = "File name cannot be null."; static final String FILENAME_IS_NULL = "File name cannot be null.";
private final NodeThrewExceptionFactory exceptionFactory; public FormcycleAttachment map(Attachment pluginAttachment) {
public FormcycleAttachment map(Attachment pluginAttachment) throws NodeThrewException {
if (isNull(pluginAttachment.getFileEntity().getDaten())) { if (isNull(pluginAttachment.getFileEntity().getDaten())) {
throw exceptionFactory.createCannotGetAttachmentException(CANNOT_READ_ATTACHED_FILE); throw new TechnicalException(CANNOT_READ_ATTACHED_FILE);
} }
return buildAttachment(pluginAttachment); return buildAttachment(pluginAttachment);
} }
FormcycleAttachment buildAttachment(Attachment pluginAttachment) throws NodeThrewException { FormcycleAttachment buildAttachment(Attachment pluginAttachment) {
return FormcycleAttachment.builder() return FormcycleAttachment.builder()
.uuid(pluginAttachment.getUUID()) .uuid(pluginAttachment.getUUID())
.fileName(pluginAttachment.getDateiName()) .fileName(pluginAttachment.getDateiName())
...@@ -58,9 +53,9 @@ public class AttachmentMapper { ...@@ -58,9 +53,9 @@ public class AttachmentMapper {
.content(new ByteArrayInputStream(pluginAttachment.getFileEntity().getDaten())).build(); .content(new ByteArrayInputStream(pluginAttachment.getFileEntity().getDaten())).build();
} }
private ContentType getContentType(String name) throws NodeThrewException { ContentType getContentType(String name) {
if(isNull(name)) { if(isNull(name)) {
throw exceptionFactory.createCannotGetAttachmentException(FILENAME_IS_NULL); throw new TechnicalException(FILENAME_IS_NULL);
} }
String contentType = requireNonNullElse(URLConnection.guessContentTypeFromName(name), MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE); String contentType = requireNonNullElse(URLConnection.guessContentTypeFromName(name), MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE);
return ContentType.create(contentType); return ContentType.create(contentType);
......
/*
* Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.formcycle.client;
import org.apache.http.HttpStatus;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class HttpUtils {
public static boolean isError(int statusCode) {
return is400Error(statusCode) || is500Error(statusCode);
}
public static boolean is400Error(int statusCode) {
return statusCode == HttpStatus.SC_BAD_REQUEST
|| statusCode == HttpStatus.SC_UNAUTHORIZED
|| statusCode == HttpStatus.SC_PAYMENT_REQUIRED
|| statusCode == HttpStatus.SC_FORBIDDEN
|| statusCode == HttpStatus.SC_NOT_FOUND
|| statusCode == HttpStatus.SC_METHOD_NOT_ALLOWED
|| statusCode == HttpStatus.SC_NOT_ACCEPTABLE
|| statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED
|| statusCode == HttpStatus.SC_REQUEST_TIMEOUT
|| statusCode == HttpStatus.SC_CONFLICT
|| statusCode == HttpStatus.SC_GONE
|| statusCode == HttpStatus.SC_LENGTH_REQUIRED
|| statusCode == HttpStatus.SC_PRECONDITION_FAILED
|| statusCode == HttpStatus.SC_REQUEST_TOO_LONG
|| statusCode == HttpStatus.SC_REQUEST_URI_TOO_LONG
|| statusCode == HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE
|| statusCode == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE
|| statusCode == HttpStatus.SC_EXPECTATION_FAILED
|| statusCode == HttpStatus.SC_INSUFFICIENT_SPACE_ON_RESOURCE
|| statusCode == HttpStatus.SC_METHOD_FAILURE
|| statusCode == HttpStatus.SC_UNPROCESSABLE_ENTITY
|| statusCode == HttpStatus.SC_LOCKED
|| statusCode == HttpStatus.SC_FAILED_DEPENDENCY
|| statusCode == HttpStatus.SC_TOO_MANY_REQUESTS;
}
public static boolean is500Error(int statusCode) {
return statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
|| statusCode == HttpStatus.SC_NOT_IMPLEMENTED
|| statusCode == HttpStatus.SC_BAD_GATEWAY
|| statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE
|| statusCode == HttpStatus.SC_GATEWAY_TIMEOUT
|| statusCode == HttpStatus.SC_HTTP_VERSION_NOT_SUPPORTED
|| statusCode == HttpStatus.SC_INSUFFICIENT_STORAGE;
}
}
/*
* Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.formcycle.client;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public class OzgCloudErrorDto {
private String message;
private String exceptionId;
}
/*
* Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.formcycle.client;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ResponseHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.ozgcloud.eingang.formcycle.FormCycleConfirmationResponse;
import de.ozgcloud.formcycle.errorhandling.OzgCloudRequestException;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class OzgCloudResponseHandler implements ResponseHandler<FormCycleConfirmationResponse> {
static final String MESSAGE = "Unexpected Status received: %s";
private final ObjectMapper objectMapper;
@Override
public FormCycleConfirmationResponse handleResponse(HttpResponse response) throws IOException {
checkResponseStatus(response);
return buildResponse(response);
}
FormCycleConfirmationResponse buildResponse(HttpResponse response) throws IOException {
return FormCycleConfirmationResponse.parseFrom(response.getEntity().getContent());
}
void checkResponseStatus(HttpResponse response) {
var statusCode = response.getStatusLine().getStatusCode();
if (HttpUtils.isError(statusCode)) {
var receivedError = getOzgCloudError(response.getEntity());
throw new OzgCloudRequestException(receivedError.getMessage(), receivedError.getExceptionId());
}
if (statusCode != HttpStatus.SC_OK) {
throw new OzgCloudRequestException(buildFullMessage(response));
}
}
OzgCloudErrorDto getOzgCloudError(HttpEntity entity) {
try {
return objectMapper.readValue(entity.getContent(), OzgCloudErrorDto.class);
} catch (IOException e) {
throw new OzgCloudRequestException("Cannot read error message.", e);
}
}
String buildFullMessage(HttpResponse response) {
StringBuilder messageBuilder = new StringBuilder(String.format(MESSAGE, response.getStatusLine()));
try {
IOUtils.readLines(response.getEntity().getContent(), StandardCharsets.UTF_8).forEach(line -> messageBuilder.append("\n").append(line));
} catch (IOException e) {
messageBuilder.append("\nError reading body");
}
return messageBuilder.toString();
}
}
/*
* Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.formcycle.errorhandling;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ExceptionUtil {
private static final String TEMPLATE_MESSAGE_WITH_EXCEPTION_ID = "%s (ExceptionId: %s)";
public static String formatMessageWithExceptionId(String message) {
return formatMessageWithExceptionId(message, createExceptionId());
}
public static String formatMessageWithExceptionId(String message, String exceptionId) {
return String.format(TEMPLATE_MESSAGE_WITH_EXCEPTION_ID, message, exceptionId);
}
public static String createExceptionId() {
return UUID.randomUUID().toString();
}
}
package de.ozgcloud.formcycle.errorhandling; package de.ozgcloud.formcycle.errorhandling;
import static de.ozgcloud.formcycle.errorhandling.EWorkflowElementNodeError.*; import static de.ozgcloud.formcycle.errorhandling.OzgPluginError.*;
import com.alibaba.fastjson.JSONObject; import java.util.Map;
import de.xima.fc.exceptions.NodeThrewException; import de.xima.fc.exceptions.NodeThrewException;
import de.xima.fc.interfaces.workflow.params.INodeThrewExceptionBuilder; import de.xima.fc.interfaces.workflow.params.INodeThrewExceptionBuilder;
...@@ -11,25 +11,27 @@ import lombok.RequiredArgsConstructor; ...@@ -11,25 +11,27 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public class NodeThrewExceptionFactory { public class NodeThrewExceptionFactory {
public static final String ERROR_MESSAGE_KEY = "message"; public static final String EXCEPTION_ID_KEY = "exceptionId";
private final INodeThrewExceptionBuilder exceptionBuilder; private final INodeThrewExceptionBuilder exceptionBuilder;
public NodeThrewException createCannotSendException(String message, Exception cause) { public NodeThrewException createInternalException(TechnicalException e) {
final var data = new JSONObject(); return exceptionBuilder.error(INTERNAL_ERROR.name(), createDataMap(e.getExceptionId())).message(e.getMessage()).cause(e)
data.put(ERROR_MESSAGE_KEY, message); .build();
return exceptionBuilder.error(OZGCLOUD_CANNOT_SEND.name(), data).message(message).cause(cause).build();
} }
public NodeThrewException createCannotGetAttachmentException(String message) { public NodeThrewException createInternalException(String message, Exception e) {
final var data = new JSONObject(); return exceptionBuilder.error(INTERNAL_ERROR.name(), createDataMap(ExceptionUtil.createExceptionId()))
data.put(ERROR_MESSAGE_KEY, message); .message(ExceptionUtil.formatMessageWithExceptionId(message)).cause(e).build();
return exceptionBuilder.error(PLUGIN_CANNOT_GET_ATTACHMENT.name(), data).message(message).build();
} }
public NodeThrewException createConfigurationException(String message) { public NodeThrewException createOzgCloudRequestException(OzgCloudRequestException e) {
final var data = new JSONObject(); return exceptionBuilder.error(OZGCLOUD_REQUEST_FAILED.name(), createDataMap(e.getExceptionId())).message(e.getMessageWithConfig()).cause(e)
data.put(ERROR_MESSAGE_KEY, message); .build();
return exceptionBuilder.error(PLUGIN_CONFIG_ERROR.name(), data).message(message).build();
} }
Map<String, String> createDataMap(String exceptionId) {
return Map.of(EXCEPTION_ID_KEY, exceptionId);
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment