finished, tested and fixed initial impl
This commit is contained in:
parent
0541cf502b
commit
3d353cf6d5
5
pom.xml
5
pom.xml
@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>com.inteligr8.alfresco</groupId>
|
||||
<artifactId>aps-public-rest-api</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<version>1.2.1</version>
|
||||
<classifier>aps1</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -216,8 +216,7 @@
|
||||
<properties>
|
||||
<aps-model.baseUrl>${aps-model.baseUrl}</aps-model.baseUrl>
|
||||
<aps-model.authType>${aps-model.authType}</aps-model.authType>
|
||||
<aps-model.basicAuth.username>${aps-model.basicAuth.username}</aps-model.basicAuth.username>
|
||||
<aps-model.basicAuth.password>${aps-model.basicAuth.password}</aps-model.basicAuth.password>
|
||||
<aps-model.basicAuth.mavenServerId>${aps-model.basicAuth.mavenServerId}</aps-model.basicAuth.mavenServerId>
|
||||
<aps-model.appName>${aps-model.appName}</aps-model.appName>
|
||||
</properties>
|
||||
<pomIncludes>
|
||||
|
@ -12,10 +12,6 @@
|
||||
|
||||
<name>Deploy App Plugin Tests</name>
|
||||
|
||||
<properties>
|
||||
<aps.app>FORMS Core</aps.app>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
@ -24,14 +20,46 @@
|
||||
<version>@pom.version@</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>download-app</id>
|
||||
<phase>validate</phase>
|
||||
<id>download-unpack-app</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>download-app</goal>
|
||||
<goal>unpack-app</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<reformat>true</reformat>
|
||||
<normalize>true</normalize>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>translate-app</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>translate-app</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalDirectory>${project.build.directory}/aps-dev</finalDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>pack-app</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>pack-app</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<unzipDirectory>${project.build.directory}/aps-dev</unzipDirectory>
|
||||
<zipDirectory>${project.build.directory}/aps-test</zipDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>deploy-app</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>deploy-app</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<apsAppName>${aps.app}</apsAppName>
|
||||
<zipDirectory>${project.build.directory}/aps-test</zipDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -21,14 +21,14 @@
|
||||
<executions>
|
||||
<execution>
|
||||
<id>download-app</id>
|
||||
<phase>validate</phase>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>download-app</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>download-app-other</id>
|
||||
<phase>validate</phase>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>download-app</goal>
|
||||
</goals>
|
||||
|
@ -12,10 +12,6 @@
|
||||
|
||||
<name>Pack App Plugin Tests</name>
|
||||
|
||||
<properties>
|
||||
<aps.app>FORMS Core</aps.app>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
@ -25,7 +21,7 @@
|
||||
<executions>
|
||||
<execution>
|
||||
<id>download-unpack-app</id>
|
||||
<phase>validate</phase>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>download-app</goal>
|
||||
<goal>unpack-app</goal>
|
||||
@ -36,11 +32,12 @@
|
||||
</execution>
|
||||
<execution>
|
||||
<id>pack-app</id>
|
||||
<phase>validate</phase>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>pack-app</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<zipDirectory>${basedir}</zipDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
@ -56,7 +53,7 @@
|
||||
<rules>
|
||||
<requireFilesExist>
|
||||
<files>
|
||||
<file>${basedir}/src/main/app/${aps.app}.zip</file>
|
||||
<file>${basedir}/${aps-model.appName}.zip</file>
|
||||
</files>
|
||||
</requireFilesExist>
|
||||
</rules>
|
||||
|
@ -21,7 +21,7 @@
|
||||
<executions>
|
||||
<execution>
|
||||
<id>download-unpack-app</id>
|
||||
<phase>validate</phase>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>download-app</goal>
|
||||
<goal>unpack-app</goal>
|
||||
@ -33,7 +33,7 @@
|
||||
</execution>
|
||||
<execution>
|
||||
<id>translate-app</id>
|
||||
<phase>validate</phase>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>translate-app</goal>
|
||||
</goals>
|
||||
|
@ -21,7 +21,7 @@
|
||||
<executions>
|
||||
<execution>
|
||||
<id>download-unpack-app</id>
|
||||
<phase>validate</phase>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>download-app</goal>
|
||||
<goal>unpack-app</goal>
|
||||
@ -29,7 +29,7 @@
|
||||
</execution>
|
||||
<execution>
|
||||
<id>unpack-app</id>
|
||||
<phase>validate</phase>
|
||||
<phase>process-sources</phase>
|
||||
<goals>
|
||||
<goal>unpack-app</goal>
|
||||
</goals>
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.inteligr8.maven.aps.modeling.goal;
|
||||
|
||||
import org.apache.maven.execution.MavenSession;
|
||||
import org.apache.maven.plugins.annotations.Parameter;
|
||||
import org.apache.maven.settings.Server;
|
||||
|
||||
import com.inteligr8.alfresco.activiti.ApsClientConfiguration;
|
||||
import com.inteligr8.alfresco.activiti.ApsClientJerseyImpl;
|
||||
@ -8,32 +10,26 @@ import com.inteligr8.alfresco.activiti.ApsPublicRestApiJerseyImpl;
|
||||
|
||||
public abstract class ApsAddressibleGoal extends DisablableGoal {
|
||||
|
||||
@Parameter( defaultValue = "${session}", readonly = true )
|
||||
protected MavenSession session;
|
||||
|
||||
@Parameter( property = "aps-model.baseUrl", required = true, defaultValue = "http://localhost:8080/activiti-app" )
|
||||
protected String activitiAppBaseUrl;
|
||||
|
||||
@Parameter( property = "aps-model.authType", required = true, defaultValue = "BASIC" )
|
||||
protected String activitiAppAuthType;
|
||||
|
||||
@Parameter( property = "aps-model.basicAuth.username", required = false, defaultValue = "admin@app.activiti.com" )
|
||||
protected String activitiAppAuthBasicUsername;
|
||||
|
||||
@Parameter( property = "aps-model.basicAuth.password", required = false, defaultValue = "admin" )
|
||||
protected String activitiAppAuthBasicPassword;
|
||||
@Parameter( property = "aps-model.basicAuth.mavenServerId", required = true, defaultValue = "aps" )
|
||||
protected String activitiAppAuthBasicServerId;
|
||||
|
||||
@Parameter( property = "aps-model.oauth.code", required = false )
|
||||
protected String oauthCode;
|
||||
|
||||
@Parameter( property = "aps-model.oauth.clientId", required = false )
|
||||
protected String oauthClientId;
|
||||
@Parameter( property = "aps-model.oauth.client.mavenServerId", required = false )
|
||||
protected String oauthClientServerId;
|
||||
|
||||
@Parameter( property = "aps-model.oauth.clientSecret", required = false )
|
||||
protected String oauthClientSecret;
|
||||
|
||||
@Parameter( property = "aps-model.oauth.username", required = false )
|
||||
protected String oauthUsername;
|
||||
|
||||
@Parameter( property = "aps-model.oauth.password", required = false )
|
||||
protected String oauthPassword;
|
||||
@Parameter( property = "aps-model.oauth.mavenServerId", required = false )
|
||||
protected String oauthServerId;
|
||||
|
||||
@Parameter( property = "aps-model.oauth.tokenUrl", required = false )
|
||||
protected String oauthTokenUrl;
|
||||
@ -47,21 +43,41 @@ public abstract class ApsAddressibleGoal extends DisablableGoal {
|
||||
switch (this.activitiAppAuthType.toUpperCase()) {
|
||||
case "BASIC":
|
||||
this.getLog().info("Configuring APS with BASIC authentication");
|
||||
this.getLog().debug("Username: " + this.activitiAppAuthBasicUsername);
|
||||
config.setBasicAuthUsername(this.activitiAppAuthBasicUsername);
|
||||
config.setBasicAuthPassword(this.activitiAppAuthBasicPassword);
|
||||
|
||||
Server creds = this.session.getSettings().getServer(this.activitiAppAuthBasicServerId);
|
||||
if (this.activitiAppAuthBasicServerId != null && creds == null)
|
||||
this.getLog().warn("The Maven configuration has no server '" + this.activitiAppAuthBasicServerId + "'; continuing with default credentials");
|
||||
|
||||
if (creds != null) {
|
||||
this.getLog().debug("Username: " + creds.getUsername());
|
||||
config.setBasicAuthUsername(creds.getUsername());
|
||||
config.setBasicAuthPassword(creds.getPassword());
|
||||
}
|
||||
break;
|
||||
case "OAUTH":
|
||||
this.getLog().info("Configuring APS with OAuth authentication");
|
||||
|
||||
Server clientCreds = this.session.getSettings().getServer(this.oauthClientServerId);
|
||||
Server oauthCreds = this.session.getSettings().getServer(this.oauthServerId);
|
||||
if ((this.oauthClientServerId != null || this.oauthServerId != null) && clientCreds == null && oauthCreds == null)
|
||||
this.getLog().warn("The Maven configuration has no server '" + this.oauthClientServerId + "' or '" + this.oauthServerId + "'; continuing without credentials");
|
||||
|
||||
this.getLog().debug("OAuth Code: " + this.oauthCode);
|
||||
this.getLog().debug("OAuth Client ID: " + this.oauthClientId);
|
||||
this.getLog().debug("OAuth Username: " + this.oauthUsername);
|
||||
config.setOAuthAuthCode(this.oauthCode);
|
||||
|
||||
if (clientCreds != null) {
|
||||
this.getLog().debug("OAuth Client ID: " + clientCreds.getUsername());
|
||||
config.setOAuthClientId(clientCreds.getUsername());
|
||||
config.setOAuthClientSecret(clientCreds.getPassword());
|
||||
}
|
||||
|
||||
if (oauthCreds != null) {
|
||||
this.getLog().debug("OAuth Username: " + oauthCreds.getUsername());
|
||||
config.setOAuthUsername(oauthCreds.getUsername());
|
||||
config.setOAuthPassword(oauthCreds.getPassword());
|
||||
}
|
||||
|
||||
config.setOAuthTokenUrl(this.oauthTokenUrl);
|
||||
config.setOAuthClientId(this.oauthClientId);
|
||||
config.setOAuthClientSecret(this.oauthClientSecret);
|
||||
config.setOAuthUsername(this.oauthUsername);
|
||||
config.setOAuthPassword(this.oauthPassword);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("The 'activitiAppAuthType' configuration must be either 'Basic' or 'OAuth'");
|
||||
|
@ -5,12 +5,15 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.maven.plugin.MojoExecutionException;
|
||||
import org.apache.maven.plugin.MojoFailureException;
|
||||
import org.apache.maven.plugins.annotations.Mojo;
|
||||
import org.apache.maven.plugins.annotations.Parameter;
|
||||
import org.codehaus.plexus.component.annotations.Component;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
|
||||
import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
|
||||
|
||||
import com.inteligr8.alfresco.activiti.ApsPublicRestApiJerseyImpl;
|
||||
import com.inteligr8.alfresco.activiti.model.AppDefinitionUpdateResultRepresentation;
|
||||
@ -19,13 +22,14 @@ import com.inteligr8.alfresco.activiti.model.AppDefinitionUpdateResultRepresenta
|
||||
@Component( role = org.apache.maven.plugin.Mojo.class )
|
||||
public class DeployAppGoal extends ApsAppAccessibleGoal {
|
||||
|
||||
@Parameter( property = "sourceDirectory", required = true, defaultValue = "${project.build.directory}/aps/app" )
|
||||
protected File sourceDirectory;
|
||||
@Parameter( property = "aps-model.upload.directory", required = true, defaultValue = "${project.build.directory}/aps" )
|
||||
protected File zipDirectory;
|
||||
|
||||
@Parameter( property = "publish", required = true, defaultValue = "false" )
|
||||
protected boolean publish = false;
|
||||
|
||||
protected final int bufferSize = 128 * 1024;
|
||||
private final MediaType zipMediaType = MediaType.valueOf("application/zip");
|
||||
|
||||
@Override
|
||||
public void executeEnabled() throws MojoExecutionException, MojoFailureException {
|
||||
@ -36,21 +40,21 @@ public class DeployAppGoal extends ApsAppAccessibleGoal {
|
||||
try {
|
||||
this.uploadApp(appId, sourceFile);
|
||||
} catch (IOException ie) {
|
||||
throw new MojoExecutionException("The downloaded APS App could not be unpacked", ie);
|
||||
throw new MojoExecutionException("The APS App could not be uploaded", ie);
|
||||
}
|
||||
}
|
||||
|
||||
protected File validateSourceDirectory() {
|
||||
if (!this.sourceDirectory.exists()) {
|
||||
throw new IllegalStateException("The 'sourceDirectory' does not exist: " + this.sourceDirectory);
|
||||
} else if (!this.sourceDirectory.isDirectory()) {
|
||||
throw new IllegalStateException("The 'sourceDirectory' is not a directory: " + this.sourceDirectory);
|
||||
if (!this.zipDirectory.exists()) {
|
||||
throw new IllegalStateException("The 'zipDirectory' does not exist: " + this.zipDirectory);
|
||||
} else if (!this.zipDirectory.isDirectory()) {
|
||||
throw new IllegalStateException("The 'zipDirectory' is not a directory: " + this.zipDirectory);
|
||||
}
|
||||
|
||||
File sourceFile = new File(this.sourceDirectory, this.apsAppName + ".zip");
|
||||
File sourceFile = new File(this.zipDirectory, this.apsAppName + ".zip");
|
||||
|
||||
if (!sourceFile.exists()) {
|
||||
throw new IllegalStateException("The App file does not exists in the 'sourceDirectory': " + sourceFile);
|
||||
throw new IllegalStateException("The App file does not exists in the 'zipDirectory': " + sourceFile);
|
||||
} else if (!sourceFile.isFile()) {
|
||||
throw new IllegalStateException("The App file is not a file: " + sourceFile);
|
||||
}
|
||||
@ -61,33 +65,31 @@ public class DeployAppGoal extends ApsAppAccessibleGoal {
|
||||
private void uploadApp(Long appId, File appZip) throws IOException, MojoExecutionException {
|
||||
ApsPublicRestApiJerseyImpl api = this.getApsApi();
|
||||
|
||||
FormDataContentDisposition cdisposition = FormDataContentDisposition.name("file")
|
||||
.size(appZip.length())
|
||||
.fileName(appZip.getName())
|
||||
.build();
|
||||
|
||||
FileInputStream fistream = new FileInputStream(appZip);
|
||||
BufferedInputStream bistream = new BufferedInputStream(fistream, this.bufferSize);
|
||||
try {
|
||||
FormDataMultiPart multipart = new FormDataMultiPart();
|
||||
multipart.bodyPart(new StreamDataBodyPart("file", bistream, appZip.getName(), this.zipMediaType));
|
||||
|
||||
if (appId == null) {
|
||||
if (this.publish) {
|
||||
this.getLog().info("Uploading & publishing new APS App: " + this.apsAppName);
|
||||
AppDefinitionUpdateResultRepresentation appDefUpdate = api.getAppDefinitionsJerseyApi().publishApp(bistream, cdisposition);
|
||||
AppDefinitionUpdateResultRepresentation appDefUpdate = api.getAppDefinitionsJerseyApi().publishApp(multipart);
|
||||
if (Boolean.TRUE.equals(appDefUpdate.getError()))
|
||||
throw new MojoExecutionException(appDefUpdate.getErrorDescription());
|
||||
} else {
|
||||
this.getLog().info("Uploading new APS App: " + this.apsAppName);
|
||||
api.getAppDefinitionsJerseyApi().import_(bistream, cdisposition);
|
||||
api.getAppDefinitionsJerseyApi().importApp(multipart, true);
|
||||
}
|
||||
} else {
|
||||
if (this.publish) {
|
||||
this.getLog().info("Uploading, versioning, & publishing APS App: " + this.apsAppName);
|
||||
AppDefinitionUpdateResultRepresentation appDefUpdate = api.getAppDefinitionsJerseyApi().publishApp(appId, bistream, cdisposition);
|
||||
this.getLog().info("Uploading, versioning, & publishing APS App: " + this.apsAppName + " (" + appId + ")");
|
||||
AppDefinitionUpdateResultRepresentation appDefUpdate = api.getAppDefinitionsJerseyApi().publishApp(appId, multipart);
|
||||
if (Boolean.TRUE.equals(appDefUpdate.getError()))
|
||||
throw new MojoExecutionException(appDefUpdate.getErrorDescription());
|
||||
} else {
|
||||
this.getLog().info("Uploading & versioning APS App: " + this.apsAppName);
|
||||
api.getAppDefinitionsJerseyApi().import_(appId, bistream, cdisposition);
|
||||
this.getLog().info("Uploading & versioning APS App: " + this.apsAppName + " (" + appId + ")");
|
||||
api.getAppDefinitionsJerseyApi().importApp(appId, multipart, true);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
@ -22,11 +22,11 @@ import org.codehaus.plexus.component.annotations.Component;
|
||||
@Component( role = org.apache.maven.plugin.Mojo.class )
|
||||
public class PackAppGoal extends ApsAppGoal {
|
||||
|
||||
@Parameter( property = "sourceDirectory", required = true, defaultValue = "${project.build.directory}/aps/app" )
|
||||
protected File sourceDirectory;
|
||||
|
||||
@Parameter( property = "targetDirectory", required = true, defaultValue = "${project.build.directory}/aps/app" )
|
||||
protected File targetDirectory;
|
||||
@Parameter( property = "aps-model.app.directory", required = true, defaultValue = "${project.build.directory}/aps/app" )
|
||||
protected File unzipDirectory;
|
||||
|
||||
@Parameter( property = "aps-model.upload.directory", required = true, defaultValue = "${project.build.directory}/aps" )
|
||||
protected File zipDirectory;
|
||||
|
||||
protected final int bufferSize = 128 * 1024;
|
||||
|
||||
@ -45,17 +45,17 @@ public class PackAppGoal extends ApsAppGoal {
|
||||
}
|
||||
|
||||
protected void validateSourceDirectory() {
|
||||
if (!this.sourceDirectory.exists()) {
|
||||
throw new IllegalStateException("The 'sourceDirectory' does not exist: " + this.sourceDirectory);
|
||||
} else if (!this.sourceDirectory.isDirectory()) {
|
||||
throw new IllegalStateException("The 'sourceDirectory' is not a directory: " + this.sourceDirectory);
|
||||
if (!this.unzipDirectory.exists()) {
|
||||
throw new IllegalStateException("The 'unzipDirectory' does not exist: " + this.unzipDirectory);
|
||||
} else if (!this.unzipDirectory.isDirectory()) {
|
||||
throw new IllegalStateException("The 'unzipDirectory' is not a directory: " + this.unzipDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
protected File validateAppDirectory() {
|
||||
File appDirectory = new File(this.sourceDirectory, this.apsAppName);
|
||||
File appDirectory = new File(this.unzipDirectory, this.apsAppName);
|
||||
if (!appDirectory.exists()) {
|
||||
throw new IllegalStateException("The 'apsAppName' does not exist in the source directory: " + this.sourceDirectory);
|
||||
throw new IllegalStateException("The 'apsAppName' does not exist in the source directory: " + this.unzipDirectory);
|
||||
} else if (!appDirectory.isDirectory()) {
|
||||
throw new IllegalStateException("The 'apsAppName' refers to a file and not a directory: " + appDirectory);
|
||||
}
|
||||
@ -64,19 +64,19 @@ public class PackAppGoal extends ApsAppGoal {
|
||||
}
|
||||
|
||||
protected File validateTargetDirectory() {
|
||||
if (!this.targetDirectory.exists()) {
|
||||
this.getLog().debug("The 'targetDirectory' does not exist; creating: " + this.targetDirectory);
|
||||
this.targetDirectory.mkdirs();
|
||||
} else if (!this.targetDirectory.isDirectory()) {
|
||||
throw new IllegalStateException("The 'targetDirectory' is not a directory: " + this.targetDirectory);
|
||||
if (!this.zipDirectory.exists()) {
|
||||
this.getLog().debug("The 'zipDirectory' does not exist; creating: " + this.zipDirectory);
|
||||
this.zipDirectory.mkdirs();
|
||||
} else if (!this.zipDirectory.isDirectory()) {
|
||||
throw new IllegalStateException("The 'zipDirectory' is not a directory: " + this.zipDirectory);
|
||||
}
|
||||
|
||||
File targetFile = new File(this.targetDirectory, this.apsAppName + ".zip");
|
||||
File targetFile = new File(this.zipDirectory, this.apsAppName + ".zip");
|
||||
|
||||
if (targetFile.isDirectory()) {
|
||||
throw new IllegalStateException("The App file is not a file: " + targetFile);
|
||||
} else if (targetFile.exists()) {
|
||||
this.getLog().debug("The App file in the 'targetDirectory' exists; deleting: " + targetFile);
|
||||
this.getLog().debug("The App file in the 'zipDirectory' exists; deleting: " + targetFile);
|
||||
targetFile.delete();
|
||||
}
|
||||
|
||||
@ -92,6 +92,7 @@ public class PackAppGoal extends ApsAppGoal {
|
||||
ZipOutputStream zostream = new ZipOutputStream(bostream);
|
||||
try {
|
||||
this.zipDirectory(appDirectory, null, zostream);
|
||||
zostream.flush();
|
||||
} finally {
|
||||
zostream.close();
|
||||
}
|
||||
@ -118,7 +119,8 @@ public class PackAppGoal extends ApsAppGoal {
|
||||
|
||||
this.getLog().debug("Packing APS file: " + path);
|
||||
|
||||
ZipEntry zentry = new ZipEntry(path.toString());
|
||||
// Activit App processing requires forward slashes (WOW)
|
||||
ZipEntry zentry = new ZipEntry(path.toString().replace('\\', '/'));
|
||||
zostream.putNextEntry(zentry);
|
||||
try {
|
||||
FileInputStream fistream = new FileInputStream(file);
|
||||
|
@ -4,12 +4,10 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.maven.execution.MavenSession;
|
||||
import org.apache.maven.plugin.MojoExecutionException;
|
||||
import org.apache.maven.plugin.MojoFailureException;
|
||||
import org.apache.maven.plugins.annotations.Mojo;
|
||||
import org.apache.maven.plugins.annotations.Parameter;
|
||||
import org.apache.maven.project.MavenProject;
|
||||
import org.codehaus.plexus.component.annotations.Component;
|
||||
|
||||
import com.inteligr8.maven.aps.modeling.crawler.ApsAppCrawler;
|
||||
@ -19,12 +17,6 @@ import com.inteligr8.maven.aps.modeling.translator.ApsAppTranslator;
|
||||
@Component( role = org.apache.maven.plugin.Mojo.class )
|
||||
public class TranslateAppGoal extends ApsAppAccessibleGoal {
|
||||
|
||||
@Parameter( defaultValue = "${project}", readonly = true )
|
||||
protected MavenProject project;
|
||||
|
||||
@Parameter( defaultValue = "${session}", readonly = true )
|
||||
protected MavenSession session;
|
||||
|
||||
@Parameter( property = "aps-model.app.directory", required = true, defaultValue = "${project.build.directory}/aps/app" )
|
||||
protected File unzipDirectory;
|
||||
|
||||
|
@ -188,7 +188,7 @@ public class UnpackAppGoal extends ApsAppGoal {
|
||||
case "json":
|
||||
this.getLog().debug("Reformatting file: " + file);
|
||||
Map<String, Object> json = ModelUtil.getInstance().readJsonAsMap(file);
|
||||
ModelUtil.getInstance().writeJson(json, file);
|
||||
ModelUtil.getInstance().writeJson(json, file, false);
|
||||
break;
|
||||
case "xml":
|
||||
case "bpmn":
|
||||
|
@ -36,17 +36,18 @@ public class ApsAppJsonNormalizer implements ApsFileNormalizer {
|
||||
ObjectNode jsonModel = (ObjectNode)_jsonModel;
|
||||
|
||||
int fields = jsonModel.size();
|
||||
jsonModel.remove(Arrays.asList("createdBy", "createdByFullName", "lastUpdatedBy", "lastUpdatedByFullName", "lastUpdated"));
|
||||
jsonModel.remove(Arrays.asList("lastUpdated"));
|
||||
// jsonModel.remove(Arrays.asList("createdBy", "createdByFullName", "lastUpdatedBy", "lastUpdatedByFullName", "lastUpdated"));
|
||||
if (jsonModel.size() < fields)
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// sort the models for better 'diff' support
|
||||
if (this.enableSorting)
|
||||
ApsModelArrayNodeSorter.getInstance().sort(jsonModels, "name");
|
||||
changed = ApsModelArrayNodeSorter.getInstance().sort(jsonModels, "name") || changed;
|
||||
|
||||
if (changed)
|
||||
ModelUtil.getInstance().writeJson(descriptor, file);
|
||||
ModelUtil.getInstance().writeJson(descriptor, file, this.enableSorting);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ public class ApsProcessJsonNormalizer implements ApsFileNormalizer {
|
||||
changed = ApsModelArrayNodeSorter.getInstance().sort(jsonChildShapes, "resourceId") || changed;
|
||||
|
||||
if (changed)
|
||||
ModelUtil.getInstance().writeJson(jsonDescriptor, file);
|
||||
ModelUtil.getInstance().writeJson(jsonDescriptor, file, this.enableSorting);
|
||||
}
|
||||
|
||||
private ArrayNode getChildShapes(ObjectNode jsonDescriptor) {
|
||||
|
@ -2,7 +2,6 @@ package com.inteligr8.maven.aps.modeling.translator;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -67,10 +66,14 @@ public class ApsAppJsonTranslator implements ApsFileTranslator {
|
||||
}
|
||||
}
|
||||
|
||||
int fields = jsonModel.size();
|
||||
jsonModel.remove(Arrays.asList("version"));
|
||||
if (jsonModel.size() < fields)
|
||||
if (jsonModel.has("version")) {
|
||||
jsonModel.put("version", 0);
|
||||
changed = true;
|
||||
}
|
||||
// int fields = jsonModel.size();
|
||||
// jsonModel.remove(Arrays.asList("version"));
|
||||
// if (jsonModel.size() < fields)
|
||||
// changed = true;
|
||||
}
|
||||
|
||||
for (JsonNode _jsonIdentityInfo : descriptor.get("definition").get("publishIdentityInfo")) {
|
||||
@ -90,11 +93,15 @@ public class ApsAppJsonTranslator implements ApsFileTranslator {
|
||||
} else {
|
||||
this.logger.trace("The organization '{}' ID does not change; leaving unchanged", fileOrgName);
|
||||
}
|
||||
|
||||
|
||||
if (jsonGroup.has("parentGroupId")) {
|
||||
jsonGroup.remove("parentGroupName");
|
||||
jsonGroup.put("parentGroupId", 0);
|
||||
changed = true;
|
||||
}
|
||||
// if (jsonGroup.has("parentGroupId")) {
|
||||
// jsonGroup.remove("parentGroupId");
|
||||
// changed = true;
|
||||
// }
|
||||
} else {
|
||||
this.logger.warn("The organization '{}' does not exist in APS", fileOrgName);
|
||||
// FIXME create the organization
|
||||
@ -102,7 +109,7 @@ public class ApsAppJsonTranslator implements ApsFileTranslator {
|
||||
}
|
||||
|
||||
if (changed)
|
||||
ModelUtil.getInstance().writeJson(descriptor, file);
|
||||
ModelUtil.getInstance().writeJson(descriptor, file, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ public class ApsFormJsonTranslator implements ApsFileTranslator {
|
||||
}
|
||||
|
||||
if (changed)
|
||||
ModelUtil.getInstance().writeJson(jsonDescriptor, file);
|
||||
ModelUtil.getInstance().writeJson(jsonDescriptor, file, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import org.xml.sax.SAXException;
|
||||
import com.inteligr8.alfresco.activiti.model.Group;
|
||||
import com.inteligr8.maven.aps.modeling.util.Index;
|
||||
import com.inteligr8.maven.aps.modeling.util.ModelUtil;
|
||||
import com.inteligr8.maven.aps.modeling.xml.CDATAFlexSection;
|
||||
|
||||
public class ApsProcessBpmnTranslator implements ApsFileTranslator {
|
||||
|
||||
@ -106,6 +105,7 @@ public class ApsProcessBpmnTranslator implements ApsFileTranslator {
|
||||
if (apsProcessId != null)
|
||||
changed = this.setAttributeIfSet(definitionsElement, NAMESPACE_ACTIVITI_MODELER, "modelId", apsProcessId) || changed;
|
||||
changed = this.removeAttributeIfSet(definitionsElement, NAMESPACE_ACTIVITI_MODELER, "modelVersion") || changed;
|
||||
//changed = this.setAttributeIfSet(definitionsElement, NAMESPACE_ACTIVITI_MODELER, "modelVersion", "0") || changed;
|
||||
|
||||
return changed;
|
||||
}
|
||||
@ -200,17 +200,17 @@ public class ApsProcessBpmnTranslator implements ApsFileTranslator {
|
||||
this.logger.trace("The form '{}' key does not change; leaving unchanged", formRefName);
|
||||
}
|
||||
|
||||
CDATAFlexSection formRefIdData = (CDATAFlexSection)ModelUtil.getInstance().xpath(
|
||||
CharacterData formRefIdData = (CharacterData)ModelUtil.getInstance().xpath(
|
||||
taskElement,
|
||||
"tns:extensionElements/modeler:form-reference-id",
|
||||
ModelUtil.CDATA_FLEX);
|
||||
if (formRefIdData == null)
|
||||
throw new IllegalStateException("A form was detected in the task, but no form reference ID was found: " + taskElement.getAttribute("id"));
|
||||
|
||||
long formRefId = Long.parseLong(formRefIdData.getValue());
|
||||
long formRefId = Long.parseLong(formRefIdData.getData());
|
||||
if (apsFormId != formRefId) {
|
||||
this.logger.debug("The form '{}' reference exists in APS with ID {}; changing model", formRefName, apsFormId);
|
||||
formRefIdData.setValue(apsFormId.toString());
|
||||
formRefIdData.setData(apsFormId.toString());
|
||||
changed = true;
|
||||
} else {
|
||||
this.logger.trace("The form '{}' reference ID does not change; leaving unchanged", formRefName);
|
||||
@ -244,14 +244,14 @@ public class ApsProcessBpmnTranslator implements ApsFileTranslator {
|
||||
|
||||
this.logger.trace("Found ID {} for the subprocess '{}' in the APS Process BPMN model", apsProcessId, subprocessName);
|
||||
|
||||
CDATAFlexSection subprocessIdData = (CDATAFlexSection)ModelUtil.getInstance().xpath(
|
||||
CharacterData subprocessIdData = (CharacterData)ModelUtil.getInstance().xpath(
|
||||
subprocessElement,
|
||||
"tns:extensionElements/modeler:subprocess-id",
|
||||
ModelUtil.CDATA_FLEX);
|
||||
long subprocessId = Long.parseLong(subprocessIdData.getValue());
|
||||
long subprocessId = Long.parseLong(subprocessIdData.getData());
|
||||
if (apsProcessId != subprocessId) {
|
||||
this.logger.debug("The process '{}' exists in APS with ID {}; changing model", subprocessName, apsProcessId);
|
||||
subprocessIdData.setValue(apsProcessId.toString());
|
||||
subprocessIdData.setData(apsProcessId.toString());
|
||||
changed = true;
|
||||
} else {
|
||||
this.logger.trace("The process '{}' ID does not change; leaving unchanged", subprocessName);
|
||||
@ -289,23 +289,37 @@ public class ApsProcessBpmnTranslator implements ApsFileTranslator {
|
||||
boolean changed = false;
|
||||
|
||||
String conditionExpression = conditionExpressionCdata.getData();
|
||||
Pattern formOutcomeInExpressionPattern = Pattern.compile("[^A-Za-z0-9]form([0-9]+)outcome[^A-Za-z0-9]");
|
||||
StringBuilder newce = new StringBuilder();
|
||||
int pos = 0;
|
||||
|
||||
Pattern formOutcomeInExpressionPattern = Pattern.compile("form([0-9]+)outcome");
|
||||
Matcher matcher = formOutcomeInExpressionPattern.matcher(conditionExpression);
|
||||
while (matcher.find()) {
|
||||
newce.append(conditionExpression.substring(pos, matcher.start(1)));
|
||||
|
||||
long formId = Long.parseLong(matcher.group(1));
|
||||
String formName = this.fileFormIndex.getValue(formId);
|
||||
if (formName == null) {
|
||||
this.logger.warn("The form ID '{}' does not exist in the form model files; ignoring", formId);
|
||||
newce.append(formId);
|
||||
} else {
|
||||
Long apsFormId = this.apsFormIndex.getFirstKey(formName);
|
||||
if (apsFormId == null) {
|
||||
this.logger.warn("The form '{}' does not exist in APS; leaving unchanged", formName);
|
||||
newce.append(formId);
|
||||
} else {
|
||||
conditionExpressionCdata.setData(apsFormId.toString());
|
||||
newce.append(apsFormId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
pos = matcher.end(1);
|
||||
}
|
||||
|
||||
newce.append(conditionExpression.substring(pos));
|
||||
|
||||
if (changed)
|
||||
conditionExpressionCdata.setData(newce.toString());
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ public class ApsProcessJsonTranslator implements ApsFileTranslator {
|
||||
changed = this.translateSubprocesses(jsonDescriptor) || changed;
|
||||
|
||||
if (changed)
|
||||
ModelUtil.getInstance().writeJson(jsonDescriptor, file);
|
||||
ModelUtil.getInstance().writeJson(jsonDescriptor, file, false);
|
||||
}
|
||||
|
||||
private boolean translateResourceId(ObjectNode jsonDescriptor, Long apsProcessId) {
|
||||
@ -130,8 +130,8 @@ public class ApsProcessJsonTranslator implements ApsFileTranslator {
|
||||
|
||||
for (JsonNode jsonChildShape : this.getChildShapes(jsonDescriptor)) {
|
||||
try {
|
||||
JsonNode jsonConditionalSequenceFlow = (JsonNode)jsonChildShape.get("properties").get("conditionalsequenceflow");
|
||||
changed = this.translateConditionalSequenceFlow(jsonConditionalSequenceFlow) || changed;
|
||||
JsonNode jsonConditionalSequenceFlow = (JsonNode)jsonChildShape.get("properties").get("conditionsequenceflow");
|
||||
changed = this.translateConditionSequenceFlow(jsonConditionalSequenceFlow) || changed;
|
||||
|
||||
JsonNode jsonFormRef = (JsonNode)jsonChildShape.get("properties").get("formreference");
|
||||
changed = this.translateReference(jsonFormRef, this.apsFormIndex) || changed;
|
||||
@ -143,7 +143,7 @@ public class ApsProcessJsonTranslator implements ApsFileTranslator {
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean translateConditionalSequenceFlow(JsonNode jsonConditionalSequenceFlow) {
|
||||
private boolean translateConditionSequenceFlow(JsonNode jsonConditionalSequenceFlow) {
|
||||
if (jsonConditionalSequenceFlow == null || !jsonConditionalSequenceFlow.isObject())
|
||||
return false;
|
||||
|
||||
|
@ -27,7 +27,7 @@ import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.w3c.dom.CDATASection;
|
||||
import org.w3c.dom.CharacterData;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
@ -38,12 +38,10 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.inteligr8.maven.aps.modeling.xml.CDATAFlexSection;
|
||||
import com.inteligr8.maven.aps.modeling.xml.DomNamespaceContext;
|
||||
|
||||
public class ModelUtil {
|
||||
|
||||
public final static QName CDATA = QName.valueOf("inteligr8:xml:cdata");
|
||||
public final static QName CDATA_FLEX = QName.valueOf("inteligr8:xml:cdata_or_element");
|
||||
private final static ModelUtil INSTANCE = new ModelUtil();
|
||||
|
||||
@ -54,6 +52,7 @@ public class ModelUtil {
|
||||
|
||||
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
private final ObjectMapper omsorted = new ObjectMapper();
|
||||
private final DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
|
||||
private final DocumentBuilder docBuilder;
|
||||
private final XPathFactory xpathfactory = XPathFactory.newInstance();
|
||||
@ -81,26 +80,28 @@ public class ModelUtil {
|
||||
|
||||
// makes Git handling a whole lot better
|
||||
this.om.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
this.omsorted.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
|
||||
// makes Git handling better between environment nuances
|
||||
this.om.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);
|
||||
this.om.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
|
||||
this.omsorted.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);
|
||||
this.omsorted.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void json(File inplaceFile) throws IOException {
|
||||
public void json(File inplaceFile, boolean enableSorting) throws IOException {
|
||||
JsonNode json = this.readJson(inplaceFile);
|
||||
this.writeJson(json, inplaceFile);
|
||||
this.writeJson(json, inplaceFile, enableSorting);
|
||||
}
|
||||
|
||||
public void json(File sourceFile, File targetFile) throws IOException {
|
||||
public void json(File sourceFile, File targetFile, boolean enableSorting) throws IOException {
|
||||
FileInputStream fistream = new FileInputStream(sourceFile);
|
||||
BufferedInputStream bistream = new BufferedInputStream(fistream, this.bufferSize);
|
||||
try {
|
||||
FileOutputStream fostream = new FileOutputStream(targetFile);
|
||||
BufferedOutputStream bostream = new BufferedOutputStream(fostream, this.bufferSize);
|
||||
try {
|
||||
this.json(bistream, bostream);
|
||||
this.json(bistream, bostream, enableSorting);
|
||||
} finally {
|
||||
bostream.close();
|
||||
}
|
||||
@ -109,10 +110,10 @@ public class ModelUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public void json(InputStream istream, OutputStream ostream) throws IOException {
|
||||
public void json(InputStream istream, OutputStream ostream, boolean enableSorting) throws IOException {
|
||||
// FIXME stream it for lower memory/IO usage
|
||||
JsonNode json = this.readJson(istream);
|
||||
this.writeJson(json, ostream);
|
||||
this.writeJson(json, ostream, enableSorting);
|
||||
}
|
||||
|
||||
public JsonNode readJson(File file) throws IOException {
|
||||
@ -167,32 +168,40 @@ public class ModelUtil {
|
||||
return this.om.readValue(istream, Map.class);
|
||||
}
|
||||
|
||||
public void writeJson(JsonNode json, File file) throws IOException {
|
||||
public void writeJson(JsonNode json, File file, boolean enableSorting) throws IOException {
|
||||
FileOutputStream fostream = new FileOutputStream(file);
|
||||
BufferedOutputStream bostream = new BufferedOutputStream(fostream, this.bufferSize);
|
||||
try {
|
||||
this.writeJson(json, bostream);
|
||||
this.writeJson(json, bostream, enableSorting);
|
||||
} finally {
|
||||
bostream.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void writeJson(Map<String, Object> map, File file) throws IOException {
|
||||
public void writeJson(Map<String, Object> map, File file, boolean enableSorting) throws IOException {
|
||||
FileOutputStream fostream = new FileOutputStream(file);
|
||||
BufferedOutputStream bostream = new BufferedOutputStream(fostream, this.bufferSize);
|
||||
try {
|
||||
this.writeJson(map, bostream);
|
||||
this.writeJson(map, bostream, enableSorting);
|
||||
} finally {
|
||||
bostream.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void writeJson(JsonNode json, OutputStream ostream) throws IOException {
|
||||
this.om.writeValue(ostream, json);
|
||||
public void writeJson(JsonNode json, OutputStream ostream, boolean enableSorting) throws IOException {
|
||||
if (enableSorting) {
|
||||
this.omsorted.writeValue(ostream, json);
|
||||
} else {
|
||||
this.om.writeValue(ostream, json);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeJson(Map<String, Object> map, OutputStream ostream) throws IOException {
|
||||
this.om.writeValue(ostream, map);
|
||||
public void writeJson(Map<String, Object> map, OutputStream ostream, boolean enableSorting) throws IOException {
|
||||
if (enableSorting) {
|
||||
this.omsorted.writeValue(ostream, map);
|
||||
} else {
|
||||
this.om.writeValue(ostream, map);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -277,22 +286,23 @@ public class ModelUtil {
|
||||
XPath xpath = this.xpathfactory.newXPath();
|
||||
xpath.setNamespaceContext(new DomNamespaceContext(node));
|
||||
|
||||
boolean isFlex = returnType.equals(ModelUtil.CDATA_FLEX);
|
||||
|
||||
if (returnType.equals(ModelUtil.CDATA) || isFlex) {
|
||||
if (returnType.equals(ModelUtil.CDATA_FLEX)) {
|
||||
NodeList nodes = (NodeList)xpath.evaluate(xpathExpr, node, XPathConstants.NODESET);
|
||||
Node anode = this.getFirstCDataSectionOrLastElement(nodes);
|
||||
if (anode instanceof CDATASection) {
|
||||
if (anode instanceof CharacterData) {
|
||||
// nothing special
|
||||
} else if (anode instanceof Element) {
|
||||
Node anode2 = this.getFirstCDataSectionOrLastElement(anode.getChildNodes());
|
||||
if (anode2 != null)
|
||||
return anode = anode2;
|
||||
if (anode2 == null)
|
||||
return null;
|
||||
if (!(anode2 instanceof CharacterData))
|
||||
throw new IllegalStateException();
|
||||
anode = anode2;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isFlex ? new CDATAFlexSection(anode) : anode;
|
||||
return anode;
|
||||
} else {
|
||||
return xpath.evaluate(xpathExpr, node, returnType);
|
||||
}
|
||||
@ -303,8 +313,8 @@ public class ModelUtil {
|
||||
|
||||
for (int n = 0; n < nodes.getLength(); n++) {
|
||||
Node anode = nodes.item(n);
|
||||
if (anode.getNodeType() == Node.CDATA_SECTION_NODE) {
|
||||
return (CDATASection)anode;
|
||||
if (anode instanceof CharacterData) {
|
||||
return (CharacterData)anode;
|
||||
} else if (anode.getNodeType() == Node.ELEMENT_NODE) {
|
||||
element = (Element)anode;
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
package com.inteligr8.maven.aps.modeling.xml;
|
||||
|
||||
import org.w3c.dom.CDATASection;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
public class CDATAFlexSection {
|
||||
|
||||
private final Node node;
|
||||
|
||||
public CDATAFlexSection(Node node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
if (this.node instanceof CDATASection) {
|
||||
return ((CDATASection)this.node).getData();
|
||||
} else {
|
||||
String value = this.node.getTextContent();
|
||||
return value == null ? null : this.node.getTextContent().trim();
|
||||
}
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
if (this.node instanceof CDATASection) {
|
||||
((CDATASection)this.node).setData(value);
|
||||
} else {
|
||||
this.node.setTextContent(value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,14 @@
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output indent="yes"/>
|
||||
<xsl:output indent="yes" />
|
||||
<xsl:strip-space elements="*"/>
|
||||
<xsl:template match="@*|node()">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()"/>
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
<xsl:template match="text()">
|
||||
<xsl:text disable-output-escaping="yes"><![CDATA[</xsl:text>
|
||||
<xsl:value-of select="." disable-output-escaping="yes" />
|
||||
<xsl:text disable-output-escaping="yes">]]></xsl:text>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
|
Loading…
x
Reference in New Issue
Block a user