diff --git a/pom.xml b/pom.xml index 12af93a..8cbc4ea 100755 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ luteceSnapshot luteceSnapshot - http://dev.lutece.paris.fr/snapshot_repository + https://dev.lutece.paris.fr/snapshot_repository true @@ -39,7 +39,7 @@ lutece luteceRepository - http://dev.lutece.paris.fr/maven_repository + https://dev.lutece.paris.fr/maven_repository default @@ -102,6 +102,16 @@ provided + + org.apache.tomcat + tomcat-catalina + ${tomcat.version} + + + org.apache.tomcat + tomcat-jasper + ${tomcat.version} + @@ -230,6 +240,7 @@ MAVENPLUGIN 10157 UTF-8 + 9.0.64 diff --git a/src/main/java/fr/paris/lutece/maven/AbstractAssemblySiteMojo.java b/src/main/java/fr/paris/lutece/maven/AbstractAssemblySiteMojo.java new file mode 100755 index 0000000..5ba1b30 --- /dev/null +++ b/src/main/java/fr/paris/lutece/maven/AbstractAssemblySiteMojo.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2002-2015, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.maven; + +import org.apache.maven.archiver.MavenArchiveConfiguration; +import org.apache.maven.archiver.MavenArchiver; +import org.apache.maven.artifact.ArtifactUtils; +import org.apache.maven.plugin.MojoExecutionException; +import org.codehaus.plexus.archiver.jar.JarArchiver; +import org.codehaus.plexus.util.StringUtils; + +import java.io.File; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Abstract mojo to create a site. + * + * Builds a site's final WAR from a Lutece core artifact and a set of Lutece + * plugin artifacts.
Note that the Lutece dependencies (core and plugins) + * will only be updated the first time the WAR is created. Subsequent calls to + * this goal will only update the site's specific files.
If you wish to + * force webapp re-creation (for instance, if you changed the version of a + * dependency), call the clean phase before this goal. + */ + +public abstract class AbstractAssemblySiteMojo + extends AbstractLuteceWebappMojo +{ + private static final String SNAPSHOT_PATTERN = "SNAPSHOT"; + + /** + * The output date format. + * + * @parameter default-value="yyyyMMdd.HHmmss" + * @required + */ + private String utcTimestampPattern; + + /** + * The name of the generated WAR file. + * + * @parameter expression="${project.build.finalName}" + * @required + */ + private String finalName; + + /** + * A temporary directory used to hold the exploded version of the webapp. + * + * @parameter expression="${project.build.directory}/${project.build.finalName}" + * @required + */ + private File explodedDirectory; + + /** + * Whether creating the archive should be forced. + * + * @parameter expression="${jar.forceCreation}" default-value="false" + */ + private boolean forceCreation; + + /** + * The maven archive configuration to use. + * + * See the + * Javadocs for MavenArchiveConfiguration. + * + * @parameter + */ + private MavenArchiveConfiguration archive = new MavenArchiveConfiguration( ); + + /** + * The Jar archiver. + * + * @component role="org.codehaus.plexus.archiver.Archiver" roleHint="jar" + * @required + */ + private JarArchiver jarArchiver; + + protected File assemblySite( ) + throws MojoExecutionException + { + prepare(); + return createWAR(); + } + + protected File prepare( ) + throws MojoExecutionException { + // Explode the webapp in the temporary directory + explodeWebapp( explodedDirectory ); + explodeConfigurationFiles( explodedDirectory ); + return explodedDirectory; + } + + protected File createWAR( ) + throws MojoExecutionException { + // put the timestamp in the assembly name + if ( ArtifactUtils.isSnapshot( project.getVersion( ) ) ) + { + DateFormat utcDateFormatter = new SimpleDateFormat( utcTimestampPattern ); + String newVersion = utcDateFormatter.format( new Date( ) ); + finalName = StringUtils.replace( finalName, SNAPSHOT_PATTERN, newVersion ); + } + + // Make a war from the exploded directory + File warFile = new File( outputDirectory, finalName + ".war" ); + + MavenArchiver archiver = new MavenArchiver( ); + archiver.setArchiver( jarArchiver ); + archiver.setOutputFile( warFile ); + archive.setForced( forceCreation ); + + try + { + if ( explodedDirectory.exists( ) ) + { + archiver.getArchiver( ) + .addDirectory( explodedDirectory, PACKAGE_WEBAPP_INCLUDES, PACKAGE_WEBAPP_RESOURCES_EXCLUDES ); + } + + archiver.createArchive( project, archive ); + } catch ( Exception e ) + { + throw new MojoExecutionException( "Error assembling WAR", e ); + } + + return warFile; + } +} diff --git a/src/main/java/fr/paris/lutece/maven/AbstractLuteceWebappMojo.java b/src/main/java/fr/paris/lutece/maven/AbstractLuteceWebappMojo.java index 20c0c85..0544941 100644 --- a/src/main/java/fr/paris/lutece/maven/AbstractLuteceWebappMojo.java +++ b/src/main/java/fr/paris/lutece/maven/AbstractLuteceWebappMojo.java @@ -90,7 +90,7 @@ public abstract class AbstractLuteceWebappMojo * * @component */ - private org.apache.maven.artifact.factory.ArtifactFactory artifactFactory; + protected org.apache.maven.artifact.factory.ArtifactFactory artifactFactory; /** * The artifact resolver. diff --git a/src/main/java/fr/paris/lutece/maven/AssemblySiteMojo.java b/src/main/java/fr/paris/lutece/maven/AssemblySiteMojo.java index f02714a..bc17679 100755 --- a/src/main/java/fr/paris/lutece/maven/AssemblySiteMojo.java +++ b/src/main/java/fr/paris/lutece/maven/AssemblySiteMojo.java @@ -33,20 +33,9 @@ */ package fr.paris.lutece.maven; -import org.apache.maven.archiver.MavenArchiveConfiguration; -import org.apache.maven.archiver.MavenArchiver; -import org.apache.maven.artifact.ArtifactUtils; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; -import org.codehaus.plexus.archiver.jar.JarArchiver; -import org.codehaus.plexus.util.StringUtils; - -import java.io.File; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; import org.apache.maven.plugins.annotations.Mojo; /** @@ -65,61 +54,8 @@ @Mojo( name = "site-assembly" ) public class AssemblySiteMojo - extends AbstractLuteceWebappMojo + extends AbstractAssemblySiteMojo { - private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" ); - private static final String SNAPSHOT_PATTERN = "SNAPSHOT"; - - /** - * The output date format. - * - * @parameter default-value="yyyyMMdd.HHmmss" - * @required - */ - private String utcTimestampPattern; - - /** - * The name of the generated WAR file. - * - * @parameter expression="${project.build.finalName}" - * @required - */ - private String finalName; - - /** - * A temporary directory used to hold the exploded version of the webapp. - * - * @parameter expression="${project.build.directory}/${project.build.finalName}" - * @required - */ - private File explodedDirectory; - - /** - * Whether creating the archive should be forced. - * - * @parameter expression="${jar.forceCreation}" default-value="false" - */ - private boolean forceCreation; - - /** - * The maven archive configuration to use. - * - * See the - * Javadocs for MavenArchiveConfiguration. - * - * @parameter - */ - private MavenArchiveConfiguration archive = new MavenArchiveConfiguration( ); - - /** - * The Jar archiver. - * - * @component role="org.codehaus.plexus.archiver.Archiver" roleHint="jar" - * @required - */ - private JarArchiver jarArchiver; - /** * Executes the mojo on the current project. * @@ -139,42 +75,4 @@ public void execute( ) assemblySite( ); } } - - private void assemblySite( ) - throws MojoExecutionException - { - // Explode the webapp in the temporary directory - explodeWebapp( explodedDirectory ); - explodeConfigurationFiles( explodedDirectory ); - - // put the timestamp in the assembly name - if ( ArtifactUtils.isSnapshot( project.getVersion( ) ) ) - { - DateFormat utcDateFormatter = new SimpleDateFormat( utcTimestampPattern ); - String newVersion = utcDateFormatter.format( new Date( ) ); - finalName = StringUtils.replace( finalName, SNAPSHOT_PATTERN, newVersion ); - } - - // Make a war from the exploded directory - File warFile = new File( outputDirectory, finalName + ".war" ); - - MavenArchiver archiver = new MavenArchiver( ); - archiver.setArchiver( jarArchiver ); - archiver.setOutputFile( warFile ); - archive.setForced( forceCreation ); - - try - { - if ( explodedDirectory.exists( ) ) - { - archiver.getArchiver( ) - .addDirectory( explodedDirectory, PACKAGE_WEBAPP_INCLUDES, PACKAGE_WEBAPP_RESOURCES_EXCLUDES ); - } - - archiver.createArchive( project, archive ); - } catch ( Exception e ) - { - throw new MojoExecutionException( "Error assembling WAR", e ); - } - } } diff --git a/src/main/java/fr/paris/lutece/maven/RunMojo.java b/src/main/java/fr/paris/lutece/maven/RunMojo.java new file mode 100644 index 0000000..75a9382 --- /dev/null +++ b/src/main/java/fr/paris/lutece/maven/RunMojo.java @@ -0,0 +1,898 @@ +/* + * Copyright (c) 2002-2015, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.maven; + +import fr.paris.lutece.maven.tomcat.TomcatLog; +import fr.paris.lutece.maven.utils.plugindat.PluginDataService; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Server; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.session.StandardManager; +import org.apache.catalina.startup.ContextConfig; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.util.StandardSessionIdGenerator; +import org.apache.catalina.valves.AbstractAccessLogValve; +import org.apache.catalina.valves.ErrorReportValve; +import org.apache.jasper.servlet.JasperInitializer; +import org.apache.jasper.servlet.JspServlet; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.resolver.ArtifactNotFoundException; +import org.apache.maven.artifact.resolver.ArtifactResolutionException; +import org.apache.maven.artifact.resolver.filter.ArtifactFilter; +import org.apache.maven.artifact.resolver.filter.TypeArtifactFilter; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.tomcat.util.descriptor.web.WebXml; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.scan.StandardJarScanFilter; +import org.apache.tomcat.util.scan.StandardJarScanner; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import java.io.BufferedReader; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static java.util.Collections.emptySet; +import static java.util.Locale.ROOT; +import static java.util.stream.Collectors.toList; +import static org.apache.maven.plugins.annotations.ResolutionScope.COMPILE; + +/** + * Plugin intended to run a dev instance of lucene from a plugin. + * It avoids to create a fake/demo/example module to test a plugin during its development. + *

+ * IMPORTANT: {@code mvn package} (to run assembly mojo) is required before this plugin execution. + * It is generally already bound in maven module lifecyle thanks its packaging. + * + * @goal run + * @requiresDependencyResolution compile + */ +// dev note: @Parameter are not yet supported by the plugin version used in the pom but it is used to enable to drop +// legacy javadoc annotations. +@Mojo(name = "run", requiresDependencyResolution = COMPILE) +public class RunMojo extends AbstractAssemblySiteMojo { + private static final Pattern HSQLDB_INT_PRECISION_DROP = Pattern.compile(" int\\([0-9]+\\)", Pattern.CASE_INSENSITIVE); + private static final Pattern HSQLDB_SPACE_NOT_NULL_NORMALISER = Pattern.compile(" NOT NULL +", Pattern.CASE_INSENSITIVE); + /** + * Webapp work directory (for JSP etc). + * + * @parameter expression="${lutece.run.workDir}" default-value="${project.build.directory}/run_workdir" + */ + @Parameter(property = "lutece.run.workDir", defaultValue = "${project.build.directory}/run_workdir") + private File workdir; + + /** + * Should the exploded directory be used or the war. + * + * @parameter expression="${lutece.run.useWar}" default-value="false" + */ + @Parameter(property = "lutece.run.useWar", defaultValue = "false") + private boolean useWar; + + /** + * Enables a faster startup but ensure to disable that if you use servlet web annotation scanning ({@code @WebXXX}). + * + * @parameter expression="${lutece.run.skipWebAnnotationScanning}" default-value="true" + */ + @Parameter(property = "lutece.run.skipWebAnnotationScanning", defaultValue = "true") + private boolean skipWebAnnotationScanning; + + /** + * Should HSQLDB be used instead of MySQL. + * + * @parameter expression="${lutece.run.useHSQLDB}" default-value="true" + */ + @Parameter(property = "lutece.run.useHSQLDB", defaultValue = "true") + private boolean useHSQLDB; + + /** + * Should Tomcat JMX be disabled (faster to startup). + * + * @parameter expression="${lutece.run.tomcat.disableJMX}" default-value="true" + */ + @Parameter(property = "lutece.run.tomcat.disableJMX", defaultValue = "true") + private boolean disableJMX; + + /** + * Tomcat port. + * + * @parameter expression="${lutece.run.tomcat.port}" default-value="8080" + */ + @Parameter(property = "lutece.run.tomcat.port", defaultValue = "8080") + private int port; + + /** + * Context (webapp) name, default to ROOT. + * + * @parameter expression="${lutece.run.tomcat.context}" default-value="/" + */ + @Parameter(property = "lutece.run.tomcat.context", defaultValue = "/") + private String context; + + /** + * Context (webapp) name, default to ROOT. + * + * @parameter expression="${lutece.run.tomcat.jarFilterExclusions}" default-value="/" + */ + @Parameter(property = "lutece.run.tomcat.jarFilterExclusions", defaultValue = "") + private String jarFilterExclusions; + + /** + * Custom SQL scripts to executes (often plugins or upgrades ones). + * Path is relative to the execution directory so ensure to use {@code ${project.build.directory}} or other maven variables. + * It is only done for HSQLDB mode, not for MySQL one which is assumed reusing an external/existing database. + * + * @parameter + */ + @Parameter + private List sqlScripts; + + /** + * Some replacement (key=token, value=replacement) for SQL scripts (mainly for plugins, defaults are already handled). + * It enables to execute standard SQL scripts on HSQLDB. + * + * @parameter + */ + @Parameter + private Map hsqldbScriptReplacements; + + /** + * Database configuration ({@code conf/db.properties}). + * It overwrites the existing configuration entries. + * + * @parameter + */ + @Parameter + private Map dbProperties; + + /** + * Custom core artifacts to use. + * + * @parameter + */ + @Parameter + private List coreArtifacts; + + /** + * Custom plugins artifacts to use. + * + * @parameter + */ + @Parameter + private List pluginArtifacts; + + /** + * Custom site artifacts to use. + * + * @parameter + */ + @Parameter + private List siteArtifacts; + + @Override + protected Set filterArtifacts(final ArtifactFilter filter) { + if (!TypeArtifactFilter.class.isInstance(filter)) { + return super.filterArtifacts(filter); + } + try { + final Field field = TypeArtifactFilter.class.getDeclaredField("type"); + if (!field.isAccessible()) { + field.setAccessible(true); + } + final String type = String.valueOf(field.get(filter)); + switch (type) { + case LUTECE_CORE_TYPE: + return coreArtifacts == null || coreArtifacts.isEmpty() ? + super.filterArtifacts(filter) : + new HashSet<>(coreArtifacts); + case LUTECE_PLUGIN_TYPE: + return pluginArtifacts == null || pluginArtifacts.isEmpty() ? + super.filterArtifacts(filter) : + new HashSet<>(pluginArtifacts); + case LUTECE_SITE_TYPE: + return siteArtifacts == null || siteArtifacts.isEmpty() ? + super.filterArtifacts(filter) : + new HashSet<>(siteArtifacts); + default: + return super.filterArtifacts(filter); + + } + } catch (final Exception e) { + throw new IllegalArgumentException(e); + } + } + + @Override + protected File prepare() throws MojoExecutionException { + final File explodedDir = super.prepare(); + if (useHSQLDB) { + setupHSQLDB(explodedDir); + } + if (dbProperties != null && !dbProperties.isEmpty()) { + final Path db = explodedDir.toPath().resolve("WEB-INF/conf/db.properties"); + final Properties properties = new Properties(); + try { + try (final Reader in = Files.newBufferedReader(db)) { + properties.load(in); + } + dbProperties.forEach(properties::setProperty); + try (final Writer out = Files.newBufferedWriter(db)) { + properties.store(out, ""); + } + } catch (final IOException ioe) { + throw new MojoExecutionException(ioe.getMessage(), ioe); + } + } + PluginDataService.generatePluginsDataFile(explodedDir.getAbsolutePath()); + return explodedDir; + } + + @Override + public void execute() throws MojoExecutionException { + run(useWar ? assemblySite() : prepare()); + } + + private void setupHSQLDB(final File explodedDir) { + final String artifactId = "hsqldb"; + final String version = "2.3.4"; + try { + final File out = new File(explodedDir, "WEB-INF/lib/" + artifactId + "-" + version + ".jar"); + if (!out.exists()) { // already done + final Artifact artifact = artifactFactory.createArtifact("org.hsqldb", artifactId, version, "compile", "jar"); + resolver.resolve(artifact, remoteRepositories, localRepository); + FileUtils.copyFile(artifact.getFile(), out); + } + + Files.write(explodedDir.toPath().resolve("WEB-INF/conf/db.properties"), ("" + + "portal.poolservice=fr.paris.lutece.util.pool.service.LuteceConnectionService\n" + + "portal.driver=org.hsqldb.jdbc.JDBCDriver\n" + + "portal.url=jdbc:hsqldb:mem:lutece;mode=MySQL\n" + + "portal.user=SA\n" + + "portal.password=\n" + + "portal.initconns=2\n" + + "portal.maxconns=10\n" + + "portal.logintimeout=2\n" + + "portal.checkvalidconnectionsql=SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS\n" + + "").getBytes(StandardCharsets.UTF_8)); + } catch (final ArtifactResolutionException | ArtifactNotFoundException | IOException e) { + throw new IllegalStateException(e); + } + } + + private void run(final File dir) { + if (disableJMX) { + Registry.disableRegistry(); + } + + TomcatLog.delegate = getLog(); + + try { + final Tomcat tomcat = new NoBaseDirTomcat(); + tomcat.setBaseDir(""); + tomcat.setPort(port); + + final StandardHost host = new StandardHost(); + host.setAutoDeploy(false); + host.setName("localhost"); + tomcat.getEngine().addChild(host); + + try { + tomcat.init(); + } catch (final LifecycleException e) { + try { + tomcat.destroy(); + } catch (final LifecycleException ex) { + // no-op + } + throw new IllegalStateException(e); + } + try { + tomcat.start(); + } catch (final LifecycleException e) { + stop(tomcat); + throw new IllegalStateException(e); + } + + // bind the connector + tomcat.getConnector(); + + // deploy the app + final StandardContext ctx = new StandardContext(); + ctx.setWorkDir(workdir.getAbsolutePath()); + ctx.setPath(context.equals("/") ? "" : context); + ctx.setName(context.startsWith("/") ? context.substring(1) : context); + ctx.setParentClassLoader(getClass().getClassLoader()); + ctx.setDocBase(dir.getAbsolutePath()); + ctx.setFailCtxIfServletStartFails(true); + ctx.addLifecycleListener(new ContextConfig() { + @Override + protected void processClasses(final WebXml webXml, final Set orderedFragments) { + if (skipWebAnnotationScanning) { + return; + } + super.processClasses(webXml, orderedFragments); + } + }); + ctx.addServletContainerInitializer(new JasperInitializer(), emptySet()); + ctx.addServletContainerInitializer((set, servletContext) -> { // replaces default web.xml + final ServletRegistration.Dynamic def = servletContext.addServlet("default", DefaultServlet.class); + def.setInitParameter("listings", "false"); + def.setInitParameter("debug", "0"); + def.setLoadOnStartup(1); + def.addMapping("/"); + + final ServletRegistration.Dynamic jspDef = servletContext.addServlet("jsp", new JspServlet()); + if (jspDef != null) { + jspDef.setInitParameter("fork", "false"); + jspDef.setInitParameter("xpoweredBy", "false"); + jspDef.setInitParameter("development", "true"); + jspDef.setLoadOnStartup(3); + jspDef.addMapping("*.jsp"); + jspDef.addMapping("*.jspx"); + } + + }, emptySet()); + ctx.setJarScanner(createScanFilter()); + if (useHSQLDB) { + ctx.addServletContainerInitializer((ignored, servletContext) -> initHSQLDBDatabase(dir, servletContext), emptySet()); + } + + Tomcat.addDefaultMimeTypeMappings(ctx); + + final Pipeline pipeline = ctx.getPipeline(); + pipeline.addValve(new AbstractAccessLogValve() { + @Override + protected void log(final CharArrayWriter charArrayWriter) { + getLog().info(charArrayWriter.toString()); + } + }); + pipeline.addValve(new ErrorReportValve()); + + final StandardManager mgr = new StandardManager(); + mgr.setSessionIdGenerator(new FastSessionIdGenerator()); + ctx.setManager(mgr); + + tomcat.getHost().addChild(ctx); + + final LifecycleState state = ctx.getState(); + if (state == LifecycleState.STOPPED || state == LifecycleState.FAILED) { + try { + stop(tomcat); + } catch (final RuntimeException re) { + // no-op + } + throw new IllegalStateException("Context didn't start"); + } + + getLog().info("Hit ENTER to exit."); + new Scanner(System.in).nextLine(); + } finally { + TomcatLog.delegate = null; + } + } + + private StandardJarScanner createScanFilter() { + final StandardJarScanFilter jarScanFilter = new StandardJarScanFilter(); + jarScanFilter.setTldSkip("tomcat.util.scan.StandardJarScanFilter.jarsToSkip=" + + "annotations-api.jar," + + "ant-junit*.jar," + + "ant-launcher.jar," + + "ant.jar," + + "asm-*.jar," + + "aspectj*.jar," + + "bootstrap.jar," + + "catalina-ant.jar," + + "catalina-ha.jar," + + "catalina-ssi.jar," + + "catalina-storeconfig.jar," + + "catalina-tribes.jar," + + "catalina.jar," + + "cglib-*.jar," + + "cobertura-*.jar," + + "commons-beanutils*.jar," + + "commons-codec*.jar," + + "commons-collections*.jar," + + "commons-daemon.jar," + + "commons-dbcp*.jar," + + "commons-digester*.jar," + + "commons-fileupload*.jar," + + "commons-httpclient*.jar," + + "commons-io*.jar," + + "commons-lang*.jar," + + "commons-logging*.jar," + + "commons-math*.jar," + + "commons-pool*.jar," + + "derby-*.jar," + + "dom4j-*.jar," + + "easymock-*.jar," + + "ecj-*.jar," + + "el-api.jar," + + "geronimo-spec-jaxrpc*.jar," + + "h2*.jar," + + "ha-api-*.jar," + + "hamcrest-*.jar," + + "hibernate*.jar," + + "httpclient*.jar," + + "icu4j-*.jar," + + "jasper-el.jar," + + "jasper.jar," + + "jaspic-api.jar," + + "jaxb-*.jar," + + "jaxen-*.jar," + + "jaxws-rt-*.jar," + + "jdom-*.jar," + + "jetty-*.jar," + + "jmx-tools.jar," + + "jmx.jar," + + "jsp-api.jar," + + "jstl.jar," + + "jta*.jar," + + "junit-*.jar," + + "junit.jar," + + "log4j*.jar," + + "mail*.jar," + + "objenesis-*.jar," + + "oraclepki.jar," + + "oro-*.jar," + + "servlet-api-*.jar," + + "servlet-api.jar," + + "slf4j*.jar," + + "taglibs-standard-spec-*.jar," + + "tagsoup-*.jar," + + "tomcat-api.jar," + + "tomcat-coyote.jar," + + "tomcat-dbcp.jar," + + "tomcat-i18n-*.jar," + + "tomcat-jdbc.jar," + + "tomcat-jni.jar," + + "tomcat-juli-adapters.jar," + + "tomcat-juli.jar," + + "tomcat-util-scan.jar," + + "tomcat-util.jar," + + "tomcat-websocket.jar," + + "tools.jar," + + "websocket-api.jar," + + "wsdl4j*.jar," + + "xercesImpl.jar," + + "xml-apis.jar," + + "xmlParserAPIs-*.jar," + + "xmlParserAPIs.jar," + + "xom-*.jar," + + // specific + "activation-*.jar," + + "apache-mime4j-core-*.jar," + + "apache-mime4j-dom-*.jar," + + "asm-*.jar," + + "bcmail-jdk15on-*.jar," + + "bcpkix-jdk15on-*.jar," + + "bcprov-jdk15on-*.jar," + + "bcutil-jdk15on-*.jar," + + "boilerpipe-*.jar," + + "c3p0*.jar," + + "cglib-*.jar," + + "checker-qual-*.jar," + + "classmate-*.jar," + + "commons-beanutils-*.jar," + + "commons-codec-*.jar," + + "commons-collections-*.jar," + + "commons-collections*.jar," + + "commons-compress-*.jar," + + "commons-csv-*.jar," + + "commons-dbcp*.1.jar," + + "commons-digester-*.jar," + + "commons-digester*.jar," + + "commons-exec-*.jar," + + "commons-fileupload-*.jar," + + "commons-http*.jar," + + "commons-io-*.jar," + + "commons-lang*.jar," + + "commons-logging-*.jar," + + "commons-math*.jar," + + "commons-pool*.jar," + + "commons-text-*.jar," + + "curvesapi-*.jar," + + "dd-plist-*.jar," + + "dec-*.jar," + + "ehcache-core-*.jar," + + "ehcache-web-*.jar," + + "error_prone_annotations-*.jar," + + "failureaccess-*.jar," + + "fontbox-*.jar," + + "freemarker-*.jar," + + "gson-*.jar," + + "guava-*-jre.jar," + + "hibernate-validator-*.Final.jar," + + "HikariCP-java*.jar," + + "hsqldb-*.jar," + + "isoparser-*.jar," + + "istack-commons-runtime-*.jar," + + "j2objc-annotations-*.jar," + + "jackson-annotations-*.jar," + + "jackson-core-*.jar," + + "jackson-databind-*.jar," + + "jackson-jaxrs-*.jar," + + "jackson-mapper-asl-*.jar," + + "jackson-module-parameter-names-*.jar," + + "jackson-xc-*.jar," + + "jai-imageio-core-*.jar," + + "jakarta.activation-api-*.jar," + + "jakarta.annotation-api-*.jar," + + "jakarta.xml.bind-api-*.jar," + + "javassist-*.jar," + + "javax.annotation-api-*.jar," + + "javax.inject-1.jar," + + "javax.mail-*.jar," + + "javax.persistence-*.jar," + + "jaxb-runtime-*.jar," + + "jbig2-imageio-*.jar," + + "jboss-logging-*.Final.jar," + + "jcip-annotations-*.jar," + + "jcommander-*.jar," + + "jdom*.jar," + + "jempbox-*.jar," + + "jersey-atom-*.jar," + + "jersey-client-*.jar," + + "jersey-core-*.jar," + + "jersey-json-*.jar," + + "jersey-server-*.jar," + + "jersey-spring-*.jar," + + "jettison-*.jar," + + "jhighlight-*.jar," + + "jmatio-*.jar," + + "jna-*.jar," + + "json-simple-*.jar," + + "jsoup-*.jar," + + "jsr*.jar," + + "jtidy-*.jar," + + "jwt-*.jar," + + "jjwt-*.jar," + + "juniversalchardet-*.jar," + + "junrar-*.jar," + + "library-freemarker-*.jar," + + "library-jmx-api-*.jar," + + "library-jwt-*.jar," + + "library-lucene-*.jar," + + "library-rbac-api-*.jar," + + "library-signrequest-*.jar," + + "library-user-api-*.jar," + + "library-workflow-core-*.jar," + + "library-workgroup-api-*.jar," + + "listenablefuture-*.jar," + + "log4j-*-api-*.jar," + + "log4j-api-*.jar," + + "log4j-core-*.jar," + + "log4j-jcl-*.jar," + + "log4j-slf4j-impl-*.jar," + + "log4j-web-*.jar," + + "lucene-analyzers-common-*.jar," + + "lucene-core-*.jar," + + "lucene-highlighter-*.jar," + + "lucene-memory-*.jar," + + "lucene-misc-*.jar," + + "lucene-queries-*.jar," + + "lucene-queryparser-*.jar," + + "lucene-sandbox-*.jar," + + "lucene-suggest-*.jar," + + "lutece-core-*.jar," + + "mchange-commons-java-*.jar," + + "metadata-extractor-*.jar," + + "mysql-connector-java-*.jar," + + "opencsv-*.jar," + + "openjson-*.jar," + + "parso-*.jar," + + "pdfbox-*.jar," + + "pdfbox-tools-*.jar," + + "plugin-rest-*.jar," + + "poi-*.jar," + + "poi-ooxml-*.jar," + + "poi-ooxml-schemas-*.jar," + + "poi-scratchpad-*.jar," + + "preflight-*.jar," + + "protobuf-java-*.jar," + + "quartz-*.jar," + + "rome-*.jar," + + "rome-utils-*.jar," + + "scannotation-*.jar," + + "sentiment-analysis-parser-*.jar," + + "slf4j-api-*.jar," + + "SparseBitSet-*.jar," + + "spring-aop-*.jar," + + "spring-beans-*.jar," + + "spring-context-*.jar," + + "spring-core-*.jar," + + "spring-expression-*.jar," + + "spring-jdbc-*.jar," + + "spring-orm-*.jar," + + "spring-tx-*.jar," + + "spring-web-*.jar," + + "stax-api-*.jar," + + "stax2-api-*.jar," + + "tagsoup-*.jar," + + "tika-core-*.jar," + + "tika-parsers-*.jar," + + "txw*.jar," + + "validation-api-*.Final.jar," + + "woodstox-core-*.jar," + + "xercesImpl-*.jar," + + "xml-apis-*.jar," + + "xmlbeans-*.jar," + + "xmpbox-*.jar," + + "xmpcore-shaded-*.jar," + + "xalan-*.jar," + + "xz-*.jar" + + (jarFilterExclusions != null && !jarFilterExclusions.isEmpty() ? "," + jarFilterExclusions : "")); + + final StandardJarScanner jarScanner = new StandardJarScanner(); + jarScanner.setScanBootstrapClassPath(false); + jarScanner.setScanClassPath(false); + jarScanner.setJarScanFilter(jarScanFilter); + return jarScanner; + } + + private void stop(final Tomcat tomcat) { + try { + tomcat.stop(); + tomcat.destroy(); + final Server server = tomcat.getServer(); + if (server != null) { // give a change to stop the utility executor otherwise it just leaks and stop later + final ExecutorService utilityExecutor = server.getUtilityExecutor(); + if (utilityExecutor != null) { + try { + utilityExecutor.awaitTermination(1, TimeUnit.MINUTES); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } catch (final LifecycleException e) { + throw new IllegalStateException(e); + } + } + + private void initHSQLDBDatabase(final File dir, final ServletContext servletContext) throws ServletException { + try (final Connection c = servletContext.getClassLoader() + .loadClass("org.hsqldb.jdbc.JDBCDriver") + .asSubclass(Driver.class) + .getConstructor() + .newInstance() + .connect("jdbc:hsqldb:mem:lutece", new Properties() {{ + setProperty("user", "sa"); + setProperty("password", ""); + }}); + final Statement statement = c.createStatement()) { + final Path base = dir.toPath(); + hsqldbStatement(base.resolve("WEB-INF/sql/create_db_lutece_core.sql"), statement); + hsqldbStatement(base.resolve("WEB-INF/sql/init_db_lutece_core.sql"), statement); + + final Path plugins = base.resolve("WEB-INF/sql/plugins"); + try (final Stream paths = Files.walk(plugins) + .filter(it -> { + final String name = it.getFileName().toString(); + return name.startsWith("create_db_") && name.endsWith(".sql"); + }) + .sorted(Path::compareTo)) { + for (final Path path : new Iterable() { + @Override + public Iterator iterator() { + return paths.iterator(); + } + }) { + hsqldbStatement(path, statement); + final Path init = path.getParent().resolve("create" + path.getFileName().toString().substring("init".length())); + if (Files.exists(init)) { + hsqldbStatement(init, statement); + } + } + } + + if (sqlScripts != null && !sqlScripts.isEmpty()) { + for (final String script : sqlScripts) { + hsqldbStatement(Paths.get(script), statement); + } + } + } catch (final Exception e) { + throw new ServletException(e); + } + } + + private void hsqldbStatement(final Path script, final Statement statement) throws SQLException, ServletException { + final boolean debug = getLog().isDebugEnabled(); + if (debug) { + getLog().debug("Reading SQL Script '" + script + "'"); + } + try (final BufferedReader reader = Files.newBufferedReader(script)) { + String line; + final StringBuilder buffer = new StringBuilder(); + while ((line = reader.readLine()) != null) { + if (line.startsWith("--") || line.trim().isEmpty()) { + if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == ';') { + doExecuteHSQLDB(statement, buffer.toString(), debug); + buffer.setLength(0); + } + continue; + } + + if (buffer.length() > 0) { + buffer.append(' '); + } + buffer.append(line.trim()); + if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == ';') { + doExecuteHSQLDB(statement, buffer.toString(), debug); + buffer.setLength(0); + } + } + if (buffer.length() > 0) { + doExecuteHSQLDB(statement, buffer.toString(), debug); + } + } catch (final IOException e) { + throw new ServletException(e); + } + } + + private void doExecuteHSQLDB(final Statement statement, final String line, final boolean debug) throws SQLException { + final String rewritten = rewriteHSQLDBStatement(line); + if (debug) { + getLog().debug("Executing\n " + line + "\nrewritten as\n " + rewritten); + } + for (final String sql : explodeStatementIfNeeded(rewritten, 0).collect(toList())) { + try { + statement.execute(sql); + } catch (final SQLException sqle) { // just eases the debugging when rewriting statements + if (sql.startsWith("ALTER TABLE ") && (sql.contains("ADD CONSTRAINT ") || sql.contains("ADD INDEX "))) { + getLog().debug("ignoring alter table for HSQLDB: " + sql, sqle); + continue; + } + throw sqle; + } + } + } + + private Stream explodeStatementIfNeeded(final String sql, final int idx) { + final String lc = sql.toLowerCase(ROOT); + final int start = lc.lastIndexOf("index ("); + if (start < 0) { + return Stream.of(sql); + } + final int end = lc.indexOf(")", start); + if (end < 0) { // wrongly formatted, will fail at execution + return Stream.of(sql); + } + final String table = sql.substring("create table ".length(), sql.indexOf("(")).trim(); + final String createTableSql = sql.substring(0, lc.lastIndexOf(',', start)) + ");"; + final String createIndexSql = "CREATE INDEX idx_" + table + "_" + idx + " ON " + table + + " (" + sql.substring(start + "INDEX (".length(), end) + ")"; + return Stream.concat(explodeStatementIfNeeded(createTableSql, idx + 1), Stream.of(createIndexSql)); + } + + private String rewriteHSQLDBStatement(final String line) { + final String lc = line.toLowerCase(ROOT) + .replace(" ", " "); // there are some typo in some scripts, tolerate them + if (lc.contains("create table")) { // if we are a DDL statement, we fix the types for HSQLDB + return hsqldbReplacements(HSQLDB_INT_PRECISION_DROP.matcher( + HSQLDB_SPACE_NOT_NULL_NORMALISER.matcher(line) + .replaceAll(" NOT NULL ") + // replace is faster than a case insensitive regex + .replace(" AUTO_INCREMENT", " IDENTITY") + .replace(" auto_increment", " IDENTITY") + .replace(" LONG VARCHAR", " LONGVARCHAR") + .replace(" long varchar", " LONGVARCHAR") + .replace(" LONG VARBINARY", " LONGVARBINARY") + .replace(" long varbinary", " LONGVARBINARY") + .replace(" LONG ", " BIGINT ") + .replace(" long ", " BIGINT ") + .replace(" NOT NULL DEFAULT ", " DEFAULT ") + .replace(" NOT NULL default ", " DEFAULT ") + .replace(" not null default ", " DEFAULT ") + .replace(" DEFAULT NULL", "") + .replace(" default NULL", "") + .replace(" default null", "")) + .replaceAll(" int")); + } + if (lc.startsWith("insert into")) { + return hsqldbReplacements(replaceHSQLDBBinaryValue(line, lc) + .replace("\\'", "''")); + } + return line; + } + + private String hsqldbReplacements(String replaced) { + if (hsqldbScriptReplacements != null) { + for (final Map.Entry entry : hsqldbScriptReplacements.entrySet()) { + replaced = replaced.replace(entry.getKey(), entry.getValue()); + } + } + return replaced; + } + + private String replaceHSQLDBBinaryValue(final String fallback, final String testable) { + final int binaryStart = testable.indexOf(",0x"); + if (binaryStart > 0) { + final int end = testable.indexOf(")", binaryStart); + final int previousEnd = testable.indexOf(",", binaryStart + 1); + final int usedEnd = previousEnd < 0 ? end : previousEnd; + final String value = testable.substring(0, binaryStart) + + ",X'" + + testable.substring(binaryStart + ",0x".length(), usedEnd) + "'" + + testable.substring(usedEnd); + return replaceHSQLDBBinaryValue(value, value); + } + return fallback; + } + + private static class NoBaseDirTomcat extends Tomcat { + @Override + protected void initBaseDir() { + // no-op + } + } + + private static class FastSessionIdGenerator extends StandardSessionIdGenerator { + @Override + protected void getRandomBytes(final byte bytes[]) { + ThreadLocalRandom.current().nextBytes(bytes); + } + } +} diff --git a/src/main/java/fr/paris/lutece/maven/tomcat/TomcatLog.java b/src/main/java/fr/paris/lutece/maven/tomcat/TomcatLog.java new file mode 100644 index 0000000..953d15e --- /dev/null +++ b/src/main/java/fr/paris/lutece/maven/tomcat/TomcatLog.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2002-2015, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.maven.tomcat; + +import org.apache.juli.logging.Log; + +public class TomcatLog implements Log { + public static org.apache.maven.plugin.logging.Log delegate; + + public TomcatLog() { + // no-op + } + + public TomcatLog(final String name) { + // ignored, we just use mojo logger + } + + @Override + public boolean isDebugEnabled() { + return delegate.isDebugEnabled(); + } + + @Override + public boolean isErrorEnabled() { + return delegate.isErrorEnabled(); + } + + @Override + public boolean isFatalEnabled() { + return delegate.isErrorEnabled(); + } + + @Override + public boolean isInfoEnabled() { + return delegate.isInfoEnabled(); + } + + @Override + public boolean isTraceEnabled() { + return delegate.isDebugEnabled(); + } + + @Override + public boolean isWarnEnabled() { + return delegate.isWarnEnabled(); + } + + @Override + public void trace(final Object o) { + delegate.debug(String.valueOf(o)); + } + + @Override + public void trace(final Object o, final Throwable throwable) { + delegate.debug(String.valueOf(o), throwable); + } + + @Override + public void debug(final Object o) { + delegate.debug(String.valueOf(o)); + } + + @Override + public void debug(final Object o, final Throwable throwable) { + delegate.debug(String.valueOf(o), throwable); + } + + @Override + public void info(final Object o) { + delegate.info(String.valueOf(o)); + } + + @Override + public void info(final Object o, final Throwable throwable) { + delegate.info(String.valueOf(o), throwable); + } + + @Override + public void warn(final Object o) { + delegate.warn(String.valueOf(o)); + } + + @Override + public void warn(final Object o, final Throwable throwable) { + delegate.warn(String.valueOf(o), throwable); + } + + @Override + public void error(final Object o) { + delegate.error(String.valueOf(o)); + } + + @Override + public void error(final Object o, final Throwable throwable) { + delegate.error(String.valueOf(o), throwable); + } + + @Override + public void fatal(final Object o) { + delegate.error(String.valueOf(o)); + } + + @Override + public void fatal(final Object o, final Throwable throwable) { + delegate.error(String.valueOf(o), throwable); + } +} diff --git a/src/main/resources/META-INF/services/org.apache.juli.logging.Log b/src/main/resources/META-INF/services/org.apache.juli.logging.Log new file mode 100644 index 0000000..1266ec3 --- /dev/null +++ b/src/main/resources/META-INF/services/org.apache.juli.logging.Log @@ -0,0 +1 @@ +fr.paris.lutece.maven.tomcat.TomcatLog