diff --git a/buildpatterns/maven/.gitignore b/buildpatterns/maven/.gitignore new file mode 100644 index 0000000..cddd2a3 --- /dev/null +++ b/buildpatterns/maven/.gitignore @@ -0,0 +1 @@ +.classpath diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/.classpath b/buildpatterns/maven/headlessdesigner-maven-plugin/.classpath deleted file mode 100644 index efcf778..0000000 --- a/buildpatterns/maven/headlessdesigner-maven-plugin/.classpath +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/pom.xml b/buildpatterns/maven/headlessdesigner-maven-plugin/pom.xml index c3611c8..7f92c49 100644 --- a/buildpatterns/maven/headlessdesigner-maven-plugin/pom.xml +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/pom.xml @@ -60,6 +60,16 @@ commons-io 2.4 + + com.ibm.sbt + com.ibm.commons + 9.0.0 + + + com.ibm.sbt + com.ibm.commons.xml + 9.0.0 + maven-plugin @@ -181,6 +191,15 @@ + + + + artifactory.openntf.org + artifactory.openntf.org + https://artifactory.openntf.org/openntf + + + release @@ -230,12 +249,16 @@ + + nexus-deploy + + + + sonatype-nexus-staging-openntf + https://oss.sonatype.org/content/repositories/snapshots + + + - - - sonatype-nexus-staging-openntf - https://oss.sonatype.org/content/repositories/snapshots - - The headless designer plugins enables you build XPages Application from the On-Disk-Project, invoking the IBM Domino Designer. \ No newline at end of file diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/HeadlessDesignerBuilder.java b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/HeadlessDesignerBuilder.java index f125ab1..2babcb1 100644 --- a/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/HeadlessDesignerBuilder.java +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/HeadlessDesignerBuilder.java @@ -2,6 +2,7 @@ import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -24,6 +25,15 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.StringUtils; +import org.openntf.maven.design.ACL; +import org.openntf.maven.design.ACLEntry; +import org.openntf.maven.util.HDUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.ibm.commons.util.StringUtil; +import com.ibm.commons.util.io.StreamUtil; +import com.ibm.commons.xml.DOMUtil; @Mojo(name = "ddehd") @Execute(goal = "ddehd") @@ -77,6 +87,12 @@ public class HeadlessDesignerBuilder extends AbstractDesignerPlugin { @Parameter(defaultValue="${project}", readonly=true, required=true) private MavenProject project; + + /** + * The ACL for the generated database. This overrides any ACL present in the ODP. + */ + @Parameter + private ACL acl; /** * Path to File with the build instructions for the headless designer. If @@ -115,9 +131,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { // Create/overwrite the $TemplateBuild field if needed if(StringUtils.isNotEmpty(templateBuildName)) { - getLog().debug("Want to build $TemplateBuild for name=" + templateBuildName + ", version=" + templateBuildVersion); + getLog().info("Configuring template build name " + templateBuildName + ", version= " + templateBuildVersion); configureTemplateBuild(tempOdp); } + if(acl != null) { + getLog().info("Configuring ACL"); + configureAcl(tempOdp); + } installFeature(); enableFeatures(); @@ -159,11 +179,17 @@ private void executeDesigner(String fileName) throws MojoExecutionException { sbDesignerArgs.append("\""); getLog().debug("Designer call = " + sbDesignerArgs.toString()); - ProcessBuilder pb = new ProcessBuilder(m_DesignerExec, "-RPARAMS", "-console", "-vmargs", sbDesignerArgs.toString()); + // This uses Runtime instead of ProcessBuilder because the latter doesn't properly launch + // Designer on all systems, and a String instead of an array because Designer is quite + // finicky about that as well + String cmd = m_DesignerExec + " -RPARAMS -console -vmargs " + sbDesignerArgs; try { - Process process = pb.start(); + Process process = Runtime.getRuntime().exec(cmd); int result = process.waitFor(); + if(result != 0) { + throw new MojoExecutionException("Designer task ended with non-zero exit code: " + result); + } getLog().debug("DDE HeadlessDesigner ended with: " + result); boolean finished = false; int nCounter = 0; @@ -175,6 +201,8 @@ private void executeDesigner(String fileName) throws MojoExecutionException { throw new MojoExecutionException("DDE HeadlessDesignerPlugin not finished in 120 sec timeout"); } + } catch (MojoExecutionException ex) { + throw ex; } catch (Exception ex) { throw new MojoExecutionException("DDE HeadlessDesignerPlugin reports an error: ", ex); } @@ -224,6 +252,115 @@ private void configureTemplateBuild(File odpPath) throws MojoExecutionException IOUtils.closeQuietly(templateXmlStream); } } + + private void configureAcl(File odpPath) throws MojoExecutionException { + File databaseProperties = new File(odpPath, "AppProperties" + File.separator + "database.properties"); + if(!databaseProperties.exists()) { + throw new MojoExecutionException("Could not locate database properties file: " + databaseProperties.getAbsolutePath()); + } + + try { + FileInputStream fis = new FileInputStream(databaseProperties); + String xmlString; + try { + xmlString = StreamUtil.readString(fis); + } finally { + StreamUtil.close(fis); + } + getLog().debug("Read XML of length " + xmlString.length() + " from database.properties file " + databaseProperties.getAbsolutePath()); + + Document xml = DOMUtil.createDocument(xmlString); + Element aclNode = (Element)DOMUtil.evaluateXPath(xml, "/database/acl").getSingleNode(); + if(aclNode == null) { + // Then add a new one under the document element + aclNode = xml.createElement("acl"); + xml.getDocumentElement().appendChild(aclNode); + } + // Clear out existing children in favor of our new ACL + DOMUtil.removeChildren(aclNode); + + aclNode.setAttribute("adminserver", StringUtil.toString(acl.getAdminServer())); + aclNode.setAttribute("consistentacl", String.valueOf(acl.isConsistentAcl())); + aclNode.setAttribute("maxinternetaccess", StringUtil.toString(acl.getMaxInternetAccess())); + List roles = acl.getRoles(); + if(roles != null) { + for(String role : roles) { + String name = HDUtils.getBracketedName(role); + if(StringUtil.isNotEmpty(name)) { + Element roleElement = xml.createElement("role"); + roleElement.setTextContent(name); + aclNode.appendChild(roleElement); + } + } + } + List entries = acl.getEntries(); + if(entries != null) { + for(ACLEntry entry : entries) { + String name = entry.getName(); + if(StringUtil.isNotEmpty(name)) { + Element entryElement = xml.createElement("aclentry"); + entryElement.setAttribute("name", name); + if("-Default-".equals(name)) { + entryElement.setAttribute("default", "true"); + } + if(entry.getType() != null) { + entryElement.setAttribute("type", entry.getType().name()); + } + entryElement.setAttribute("level", entry.getLevel().name()); + if(!entry.isCreateDocs()) { + entryElement.setAttribute("createdocs", "false"); + } + if(entry.isDeleteDocs()) { + entryElement.setAttribute("deletedocs", "true"); + } + if(!entry.isCreateLsJavaAgents()) { + entryElement.setAttribute("createlsjavaagents", "false"); + } + if(!entry.isCreatePersonalAgents()) { + entryElement.setAttribute("createpersonalagents", "false"); + } + if(!entry.isCreatePersonalViews()) { + entryElement.setAttribute("createpersonalviews", "false"); + } + if(!entry.isCreateSharedViews()) { + entryElement.setAttribute("createsharedviews", "false"); + } + if(!entry.isReadPublicDocs()) { + entryElement.setAttribute("readpublicdocs", "false"); + } + if(!entry.isWritePublicDocs()) { + entryElement.setAttribute("writepublicdocs", "false"); + } + List entryRoles = entry.getRoles(); + if(entryRoles != null) { + for(String role : entryRoles) { + String roleName = HDUtils.getBracketedName(role); + if(StringUtil.isNotEmpty(roleName)) { + Element roleElement = xml.createElement("role"); + roleElement.setTextContent(roleName); + entryElement.appendChild(roleElement); + } + } + } + + aclNode.appendChild(entryElement); + } + } + } + + String resultXml = DOMUtil.getXMLString(xml, false); + getLog().debug("Writing result XML of length " + resultXml.length()); + FileOutputStream fos = new FileOutputStream(databaseProperties); + try { + IOUtils.write(resultXml, fos); + } finally { + IOUtils.closeQuietly(fos); + } + + } catch(Throwable ex) { + throw new MojoExecutionException("Failed to configure ACL", ex); + } + } private void installFeature() throws MojoExecutionException { if (m_Features != null && m_Features.size() > 0) { @@ -236,9 +373,8 @@ private void installFeature() throws MojoExecutionException { StringBuilder sb = new StringBuilder(); sb.append("com.ibm.designer.domino.tools.userlessbuild.jobs.UpdateManagerJob,-command install -from "); sb.append(site.getUrl()); - sb.append(" -to file:/"); - sb.append(m_NotesData); - sb.append("/workspace/applications"); + sb.append(" -to "); + sb.append(HDUtils.fileUri(m_NotesData, "workspace", "applications")); sb.append(" -featureId "); sb.append(site.getFeatureId()); sb.append(" -version "); @@ -277,9 +413,8 @@ private void enableFeatures() throws MojoExecutionException { pw.println("config,true,true"); for (Feature site : m_Features) { StringBuilder sb = new StringBuilder(); - sb.append("com.ibm.designer.domino.tools.userlessbuild.jobs.UpdateManagerJob,-command enable -to file:/"); - sb.append(m_NotesData); - sb.append("/workspace/applications"); + sb.append("com.ibm.designer.domino.tools.userlessbuild.jobs.UpdateManagerJob,-command enable -to "); + sb.append(HDUtils.fileUri(m_NotesData, "workspace", "applications")); sb.append(" -featureId "); sb.append(site.getFeatureId()); sb.append(" -version "); @@ -330,9 +465,8 @@ private void disableFeatures() throws MojoExecutionException { pw.println("config,true,true"); for (Feature site : m_Features) { StringBuilder sb = new StringBuilder(); - sb.append("com.ibm.designer.domino.tools.userlessbuild.jobs.UpdateManagerJob,-command disable -to file:/"); - sb.append(m_NotesData); - sb.append("/workspace/applications"); + sb.append("com.ibm.designer.domino.tools.userlessbuild.jobs.UpdateManagerJob,-command disable -to "); + sb.append(HDUtils.fileUri(m_NotesData, "workspace", "applications")); sb.append(" -featureId "); sb.append(site.getFeatureId()); sb.append(" -version "); @@ -364,9 +498,8 @@ private void uninstallFeatures() throws MojoExecutionException { pw.println("config,true,true"); for (Feature site : m_Features) { StringBuilder sb = new StringBuilder(); - sb.append("com.ibm.designer.domino.tools.userlessbuild.jobs.UpdateManagerJob,-command uninstall -to file:/"); - sb.append(m_NotesData); - sb.append("/workspace/applications"); + sb.append("com.ibm.designer.domino.tools.userlessbuild.jobs.UpdateManagerJob,-command uninstall -to "); + sb.append(HDUtils.fileUri(m_NotesData, "workspace", "applications")); sb.append(" -featureId "); sb.append(site.getFeatureId()); sb.append(" -version "); diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACL.java b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACL.java new file mode 100644 index 0000000..99f0073 --- /dev/null +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACL.java @@ -0,0 +1,97 @@ +package org.openntf.maven.design; + +import java.util.List; + +import org.apache.maven.plugins.annotations.Parameter; + +/** + * Represents an ACL specification to be added to the database.properties file during building. + * + * @author Jesse Gallagher + * @since 1.4.0 + */ +public class ACL { + /** + * A List of ACL entries to populate the ACL + */ + @Parameter + private List entries; + /** + * The administration server to specify for the ACL + */ + @Parameter + private String adminServer; + /** + * Whether the database should be set to maintain a consistent ACL across replicas + */ + @Parameter + private boolean consistentAcl; + /** + * The maximum effective access level for Internet sessions + */ + @Parameter(defaultValue="editor") + private ACLAccessLevel maxInternetAccess = ACLAccessLevel.editor; + /** + * A List of roles available in the database + */ + @Parameter + private List roles; + + /** + * @return the entries + */ + public List getEntries() { + return entries; + } + /** + * @param entries the entries to set + */ + public void setEntries(List entries) { + this.entries = entries; + } + /** + * @return the adminServer + */ + public String getAdminServer() { + return adminServer; + } + /** + * @param adminServer the adminServer to set + */ + public void setAdminServer(String adminServer) { + this.adminServer = adminServer; + } + /** + * @return the consistentAcl + */ + public boolean isConsistentAcl() { + return consistentAcl; + } + /** + * @param consistentAcl the consistentAcl to set + */ + public void setConsistentAcl(boolean consistentAcl) { + this.consistentAcl = consistentAcl; + } + /** + * @return the maxInternetAccess + */ + public ACLAccessLevel getMaxInternetAccess() { + return maxInternetAccess; + } + /** + * @param maxInternetAccess the maxInternetAccess to set + */ + public void setMaxInternetAccess(ACLAccessLevel maxInternetAccess) { + this.maxInternetAccess = maxInternetAccess; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + +} diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLAccessLevel.java b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLAccessLevel.java new file mode 100644 index 0000000..42d2aa2 --- /dev/null +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLAccessLevel.java @@ -0,0 +1,11 @@ +package org.openntf.maven.design; + +/** + * This enumeration specifies the available levels for an ACL entry. + * + * @author Jesse Gallagher + * @since 1.4.0 + */ +public enum ACLAccessLevel { + noaccess, depositor, reader, author, editor, designer, manager +} diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLEntry.java b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLEntry.java new file mode 100644 index 0000000..6f6d940 --- /dev/null +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLEntry.java @@ -0,0 +1,197 @@ +package org.openntf.maven.design; + +import java.util.List; + +import org.apache.maven.plugins.annotations.Parameter; + +public class ACLEntry { + /** + * A List of roles to apply to this entry + */ + @Parameter + private List roles; + /** + * The name of the entry, in canonical format + */ + @Parameter(required=true) + private String name; + /** + * The access level for the entry + */ + @Parameter(required=true) + private ACLAccessLevel level; + /** + * Whether the entry is allowed to delete documents + */ + @Parameter(defaultValue="false") + private boolean deleteDocs = false; + /** + * Whether the entry is allowed to write public documents + */ + @Parameter(defaultValue="true") + private boolean writePublicDocs = true; + /** + * Whether the entry is allowed to read public documents + */ + @Parameter(defaultValue="true") + private boolean readPublicDocs = true; + /** + * Whether the entry is allowed to create shared agents + */ + @Parameter(defaultValue="true") + private boolean createLsJavaAgents = true; + /** + * Whether the entry is allowed to create personal views + */ + @Parameter(defaultValue="true") + private boolean createPersonalViews = true; + /** + * Whether the entry is allowed to create personal agents + */ + @Parameter(defaultValue="true") + private boolean createPersonalAgents = true; + /** + * Whether the entry is allowed to create shared agents + */ + @Parameter(defaultValue="true") + private boolean createSharedViews = true; + /** + * Whether the entry is allowed to create documents + */ + @Parameter(defaultValue="true") + private boolean createDocs = true; + /** + * The type of the named entity + */ + @Parameter + private ACLEntryType type; + + /** + * @return the roles + */ + public List getRoles() { + return roles; + } + /** + * @param roles the roles to set + */ + public void setRoles(List roles) { + this.roles = roles; + } + /** + * @return the name + */ + public String getName() { + return name; + } + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + /** + * @return the level + */ + public ACLAccessLevel getLevel() { + return level; + } + /** + * @param level the level to set + */ + public void setLevel(ACLAccessLevel level) { + this.level = level; + } + /** + * @return the deleteDocs + */ + public boolean isDeleteDocs() { + return deleteDocs; + } + /** + * @param deleteDocs the deleteDocs to set + */ + public void setDeleteDocs(boolean deleteDocs) { + this.deleteDocs = deleteDocs; + } + /** + * @return the writePublicDocs + */ + public boolean isWritePublicDocs() { + return writePublicDocs; + } + /** + * @param writePublicDocs the writePublicDocs to set + */ + public void setWritePublicDocs(boolean writePublicDocs) { + this.writePublicDocs = writePublicDocs; + } + /** + * @return the readPublicDocs + */ + public boolean isReadPublicDocs() { + return readPublicDocs; + } + /** + * @param readPublicDocs the readPublicDocs to set + */ + public void setReadPublicDocs(boolean readPublicDocs) { + this.readPublicDocs = readPublicDocs; + } + /** + * @return the createLsJavaAgents + */ + public boolean isCreateLsJavaAgents() { + return createLsJavaAgents; + } + /** + * @param createLsJavaAgents the createLsJavaAgents to set + */ + public void setCreateLsJavaAgents(boolean createLsJavaAgents) { + this.createLsJavaAgents = createLsJavaAgents; + } + /** + * @return the createPersonalViews + */ + public boolean isCreatePersonalViews() { + return createPersonalViews; + } + /** + * @param createPersonalViews the createPersonalViews to set + */ + public void setCreatePersonalViews(boolean createPersonalViews) { + this.createPersonalViews = createPersonalViews; + } + /** + * @return the createPersonalAgents + */ + public boolean isCreatePersonalAgents() { + return createPersonalAgents; + } + /** + * @param createPersonalAgents the createPersonalAgents to set + */ + public void setCreatePersonalAgents(boolean createPersonalAgents) { + this.createPersonalAgents = createPersonalAgents; + } + /** + * @return the createSharedViews + */ + public boolean isCreateSharedViews() { + return createSharedViews; + } + /** + * @param createSharedViews the createSharedViews to set + */ + public void setCreateSharedViews(boolean createSharedViews) { + this.createSharedViews = createSharedViews; + } + + public ACLEntryType getType() { + return type; + } + + public boolean isCreateDocs() { + return createDocs; + } +} diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLEntryType.java b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLEntryType.java new file mode 100644 index 0000000..e39ab0d --- /dev/null +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/design/ACLEntryType.java @@ -0,0 +1,5 @@ +package org.openntf.maven.design; + +public enum ACLEntryType { + person, server, servergroup, persongroup, mixedgroup +} diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/util/HDUtils.java b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/util/HDUtils.java new file mode 100644 index 0000000..1dd3ac7 --- /dev/null +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/src/main/java/org/openntf/maven/util/HDUtils.java @@ -0,0 +1,49 @@ +package org.openntf.maven.util; + +import java.io.File; + +public enum HDUtils { + ; + + /** + * Constructs a file:// URI from the provided system file path and subfolders. + * + * @param filePath the base file path in system format + * @param subfolders any subfolders to append + * @return a file:// URI for the provided path + */ + public static String fileUri(String filePath, String... subfolders) { + StringBuilder sub = new StringBuilder(); + if(subfolders != null) { + for(String subfolder : subfolders) { + if(sub.length() > 0) { + sub.append('/'); + } + sub.append(subfolder); + } + } + File baseFile = new File(filePath); + if(sub.length() > 0) { + File result = new File(baseFile, sub.toString()); + return result.toURI().toString(); + } else { + return baseFile.toURI().toString(); + } + + } + + /** + * @return the Domino role name in bracketed form, in case it was provided without + */ + public static String getBracketedName(String name) { + if(name == null || name.isEmpty()) { + return name; + } else { + if(name.startsWith("[") && name.endsWith("]")) { + return name; + } else { + return "[" + name + "]"; + } + } + } +} diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/src/test/java/org/openntf/maven/test/AllTests.java b/buildpatterns/maven/headlessdesigner-maven-plugin/src/test/java/org/openntf/maven/test/AllTests.java new file mode 100644 index 0000000..78dd3dc --- /dev/null +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/src/test/java/org/openntf/maven/test/AllTests.java @@ -0,0 +1,13 @@ +package org.openntf.maven.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.openntf.maven.test.util.TestHDUtils; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + TestHDUtils.class +}) +public class AllTests { + +} diff --git a/buildpatterns/maven/headlessdesigner-maven-plugin/src/test/java/org/openntf/maven/test/util/TestHDUtils.java b/buildpatterns/maven/headlessdesigner-maven-plugin/src/test/java/org/openntf/maven/test/util/TestHDUtils.java new file mode 100644 index 0000000..09b83f9 --- /dev/null +++ b/buildpatterns/maven/headlessdesigner-maven-plugin/src/test/java/org/openntf/maven/test/util/TestHDUtils.java @@ -0,0 +1,22 @@ +package org.openntf.maven.test.util; + +import static org.junit.Assert.*; + +import java.io.File; + +import org.junit.Test; +import org.openntf.maven.util.HDUtils; + +public class TestHDUtils { + @Test + public void testFileUri() { + String filePath = System.getProperty("java.io.tmpdir"); + String result = HDUtils.fileUri(filePath, "workspace", "applications"); + + File file = new File(filePath); + File concat = new File(file, "workspace/applications"); + String expected = concat.toURI().toString(); + + assertEquals("file URI should match expected", expected, result); + } +} diff --git a/testpatterns/org.openntf.junit.xsp.parent/pom.xml b/testpatterns/org.openntf.junit.xsp.parent/pom.xml index 77e3381..44dbe93 100644 --- a/testpatterns/org.openntf.junit.xsp.parent/pom.xml +++ b/testpatterns/org.openntf.junit.xsp.parent/pom.xml @@ -114,12 +114,12 @@ eclipse-plugin com.ibm.notes.java.api.win32.linux - [9.0.1,9.0.2) + 9.0.1 eclipse-plugin com.ibm.designer.lib.jsf - [9.0.1,9.0.2) + 9.0.1 @@ -322,18 +322,13 @@ - - - com.mycila - license-maven-plugin - 2.6 - maven-plugin - - - org.codehaus.plexus - plexus-utils - 3.0.15 - - + + + artifactory.openntf.org + artifactory.openntf.org + https://artifactory.openntf.org/openntf + + + JUnit4XPages Plugin \ No newline at end of file