|
32 | 32 | import java.nio.file.Paths; |
33 | 33 | import java.nio.file.StandardCopyOption; |
34 | 34 | import java.util.ArrayList; |
| 35 | +import java.util.Collection; |
35 | 36 | import java.util.Collections; |
36 | 37 | import java.util.Comparator; |
37 | 38 | import java.util.Enumeration; |
38 | 39 | import java.util.HashMap; |
| 40 | +import java.util.HashSet; |
39 | 41 | import java.util.LinkedHashSet; |
40 | 42 | import java.util.List; |
41 | 43 | import java.util.Map; |
|
45 | 47 | import java.util.concurrent.ConcurrentLinkedQueue; |
46 | 48 | import java.util.regex.Pattern; |
47 | 49 |
|
| 50 | +import javax.xml.parsers.DocumentBuilder; |
| 51 | +import javax.xml.parsers.DocumentBuilderFactory; |
| 52 | +import javax.xml.parsers.ParserConfigurationException; |
| 53 | + |
48 | 54 | import io.helidon.config.mp.MpConfigSources; |
49 | 55 |
|
50 | 56 | import jakarta.enterprise.inject.se.SeContainer; |
51 | 57 | import jakarta.enterprise.inject.spi.CDI; |
52 | 58 | import jakarta.enterprise.inject.spi.DefinitionException; |
53 | 59 | import jakarta.enterprise.util.AnnotationLiteral; |
| 60 | +import jakarta.ws.rs.core.Application; |
54 | 61 | import org.eclipse.microprofile.config.Config; |
55 | 62 | import org.eclipse.microprofile.config.ConfigProvider; |
56 | 63 | import org.eclipse.microprofile.config.spi.ConfigBuilder; |
|
64 | 71 | import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; |
65 | 72 | import org.jboss.shrinkwrap.api.Archive; |
66 | 73 | import org.jboss.shrinkwrap.api.ArchivePath; |
| 74 | +import org.jboss.shrinkwrap.api.ArchivePaths; |
67 | 75 | import org.jboss.shrinkwrap.api.Node; |
68 | 76 | import org.jboss.shrinkwrap.api.spec.JavaArchive; |
69 | 77 | import org.jboss.shrinkwrap.descriptor.api.Descriptor; |
| 78 | +import org.w3c.dom.Document; |
| 79 | +import org.w3c.dom.NodeList; |
| 80 | +import org.xml.sax.SAXException; |
70 | 81 |
|
71 | 82 | /** |
72 | 83 | * Implementation of DeployableContainer for launching Helidon microprofile server. |
|
77 | 88 | * <li>A temporary directory is created</li> |
78 | 89 | * <li>The WebArchive contents are written to the temporary directory</li> |
79 | 90 | * <li>beans.xml is created in WEB-INF/classes if not present</li> |
| 91 | + * <li>WEB-INF/beans.xml will be moved to WEB-INF/classes/META-INF if present</li> |
80 | 92 | * <li>The server is started with WEB-INF/classes and all libraries in WEB-INF/libon the classpath.</li> |
81 | 93 | * </ol> |
82 | 94 | * |
@@ -180,19 +192,26 @@ public ProtocolMetaData deploy(Archive<?> archive) throws DeploymentException { |
180 | 192 |
|
181 | 193 | Path rootDir = context.deployDir.resolve(""); |
182 | 194 | if (isJavaArchive) { |
183 | | - ensureBeansXml(rootDir); |
| 195 | + ensureBeansXml(rootDir, null); |
184 | 196 | classPath.add(rootDir); |
185 | 197 | } else { |
186 | 198 | // Prepare the launcher files |
187 | 199 | Path webInfDir = context.deployDir.resolve("WEB-INF"); |
188 | 200 | Path classesDir = webInfDir.resolve("classes"); |
189 | 201 | Path libDir = webInfDir.resolve("lib"); |
190 | | - ensureBeansXml(classesDir); |
| 202 | + ensureBeansXml(classesDir, webInfDir); |
191 | 203 | addServerClasspath(classPath, classesDir, libDir, rootDir); |
| 204 | + if (containerConfig.isInWebContainer()) { |
| 205 | + context.rootContext = archive.getName().split("\\.")[0]; |
| 206 | + if (!loadApplicationFromWebXml(context, webInfDir)) { |
| 207 | + // Search Application in classes |
| 208 | + loadApplicationFromClasses(context, archive); |
| 209 | + } |
| 210 | + } |
192 | 211 | } |
193 | 212 |
|
194 | 213 | startServer(context, classPath.toArray(new Path[0])); |
195 | | - } catch (IOException e) { |
| 214 | + } catch (IOException | SAXException | ParserConfigurationException e) { |
196 | 215 | LOGGER.log(Level.INFO, "Failed to start container", e); |
197 | 216 | throw new DeploymentException("Failed to copy the archive assets into the deployment directory", e); |
198 | 217 | } catch (InvocationTargetException e) { |
@@ -221,6 +240,78 @@ public ProtocolMetaData deploy(Archive<?> archive) throws DeploymentException { |
221 | 240 | return new ProtocolMetaData(); |
222 | 241 | } |
223 | 242 |
|
| 243 | + private boolean loadApplicationFromClasses(RunContext context, Archive<?> archive) |
| 244 | + throws ClassNotFoundException, ReflectiveOperationException { |
| 245 | + ArchivePath classes = ArchivePaths.create("WEB-INF", "classes"); |
| 246 | + org.jboss.shrinkwrap.api.Node root = archive.getContent().get(classes); |
| 247 | + Collection<Class<Application>> applications = new HashSet<>(); |
| 248 | + if (root != null) { |
| 249 | + deepApplicationFind(root, applications); |
| 250 | + context.applications.addAll(applications); |
| 251 | + } |
| 252 | + return !context.applications.isEmpty(); |
| 253 | + } |
| 254 | + |
| 255 | + private void deepApplicationFind(org.jboss.shrinkwrap.api.Node parent, |
| 256 | + Collection<Class<Application>> applications) throws ClassNotFoundException { |
| 257 | + for (org.jboss.shrinkwrap.api.Node child : parent.getChildren()) { |
| 258 | + if (child.getChildren().isEmpty()) { |
| 259 | + String name = child.toString(); |
| 260 | + if (name.endsWith(".class")) { |
| 261 | + name = name.replaceFirst("\\.class", "").replaceFirst("/WEB-INF/classes/", "").replaceAll("/", "."); |
| 262 | + Class<?> clazz = Class.forName(name); |
| 263 | + if (Application.class.isAssignableFrom(clazz)) { |
| 264 | + applications.add((Class<Application>) clazz); |
| 265 | + } |
| 266 | + } |
| 267 | + } else { |
| 268 | + deepApplicationFind(child, applications); |
| 269 | + } |
| 270 | + } |
| 271 | + } |
| 272 | + |
| 273 | + private boolean loadApplicationFromWebXml(RunContext context, Path webInfDir) throws IOException, |
| 274 | + ParserConfigurationException, SAXException, ReflectiveOperationException { |
| 275 | + Path webXml = webInfDir.resolve("web.xml"); |
| 276 | + if (Files.exists(webXml)) { |
| 277 | + try (InputStream inputStream = Files.newInputStream(webXml)) { |
| 278 | + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
| 279 | + DocumentBuilder db = dbf.newDocumentBuilder(); |
| 280 | + Document doc = db.parse(inputStream); |
| 281 | + NodeList nodes = doc.getElementsByTagName("init-param"); |
| 282 | + for (int i = 0; i < nodes.getLength(); i++) { |
| 283 | + NodeList childs = nodes.item(i).getChildNodes(); |
| 284 | + Class<Application> application = application(childs); |
| 285 | + if (application != null) { |
| 286 | + context.applications.add(application); |
| 287 | + return true; |
| 288 | + } |
| 289 | + } |
| 290 | + } |
| 291 | + } |
| 292 | + return false; |
| 293 | + } |
| 294 | + |
| 295 | + private Class<Application> application(NodeList childs) throws ClassNotFoundException { |
| 296 | + boolean isApp = false; |
| 297 | + String appName = null; |
| 298 | + for (int j = 0; j < childs.getLength(); j++) { |
| 299 | + org.w3c.dom.Node element = childs.item(j); |
| 300 | + String name = element.getNodeName(); |
| 301 | + String value = element.getTextContent(); |
| 302 | + if ("param-name".equals(name) && "jakarta.ws.rs.Application".equals(value)) { |
| 303 | + isApp = true; |
| 304 | + } else if ("param-value".equals(name)) { |
| 305 | + appName = value; |
| 306 | + } |
| 307 | + } |
| 308 | + if (isApp) { |
| 309 | + return (Class<Application>) Class.forName(appName); |
| 310 | + } else { |
| 311 | + return null; |
| 312 | + } |
| 313 | + } |
| 314 | + |
224 | 315 | static Optional<Exception> lookForSupressedDeploymentException(Throwable t) { |
225 | 316 | if (t == null) { |
226 | 317 | return Optional.empty(); |
@@ -290,6 +381,18 @@ void startServer(RunContext context, Path[] classPath) |
290 | 381 | // META-INF/microprofile-config.properties (such as JWT-Auth) |
291 | 382 | ConfigBuilder builder = ConfigProviderResolver.instance() |
292 | 383 | .getBuilder(); |
| 384 | + // Add root context path per Application |
| 385 | + if (context.rootContext != null && !context.applications.isEmpty()) { |
| 386 | + containerConfig.addConfigBuilderConsumer(configBuilder -> { |
| 387 | + Map<String, String> properties = new HashMap<>(); |
| 388 | + for (Class<Application> app : context.applications) { |
| 389 | + String key = app.getName() + ".routing-path.path"; |
| 390 | + String value = "/" + context.rootContext; |
| 391 | + properties.put(key, value); |
| 392 | + } |
| 393 | + configBuilder.withSources(MpConfigSources.create(properties)); |
| 394 | + }); |
| 395 | + } |
293 | 396 | // we must use the default configuration to support profiles (and test them correctly in config TCK) |
294 | 397 | // we may need to have a custom configuration for TCKs that do require workarounds |
295 | 398 | /* |
@@ -376,17 +479,24 @@ void addServerClasspath(List<Path> classpath, Path classesDir, Path libDir, Path |
376 | 479 | classpath.add(rootDir); |
377 | 480 | } |
378 | 481 |
|
379 | | - private void ensureBeansXml(Path classesDir) throws IOException { |
| 482 | + private void ensureBeansXml(Path classesDir, Path webinfDir) throws IOException { |
380 | 483 | Path beansPath = classesDir.resolve("META-INF/beans.xml"); |
| 484 | + Path metaInfPath = beansPath.getParent(); |
| 485 | + if (null != metaInfPath) { |
| 486 | + Files.createDirectories(metaInfPath); |
| 487 | + } |
| 488 | + if (containerConfig.isInWebContainer() && webinfDir != null) { |
| 489 | + // In case exists WEB-INF/beans.xml, then move it to classes/META-INF/beans.xml |
| 490 | + Path webInfBeansPath = webinfDir.resolve("beans.xml"); |
| 491 | + if (Files.exists(webInfBeansPath)) { |
| 492 | + Files.move(webInfBeansPath, beansPath); |
| 493 | + return; |
| 494 | + } |
| 495 | + } |
381 | 496 | if (Files.exists(beansPath)) { |
382 | 497 | return; |
383 | 498 | } |
384 | 499 | try (InputStream beanXmlTemplate = HelidonDeployableContainer.class.getResourceAsStream("/templates/beans.xml")) { |
385 | | - Path metaInfPath = beansPath.getParent(); |
386 | | - if (null != metaInfPath) { |
387 | | - Files.createDirectories(metaInfPath); |
388 | | - } |
389 | | - |
390 | 500 | if (null == beanXmlTemplate) { |
391 | 501 | Files.write(beansPath, new byte[0]); |
392 | 502 | } else { |
@@ -531,6 +641,8 @@ private static class RunContext { |
531 | 641 | private Object runner; |
532 | 642 | // existing class loader |
533 | 643 | private ClassLoader oldClassLoader; |
| 644 | + private String rootContext; |
| 645 | + private Set<Class<Application>> applications = new HashSet<>(); |
534 | 646 | } |
535 | 647 |
|
536 | 648 | static class HelidonContainerClassloader extends ClassLoader implements Closeable { |
@@ -576,8 +688,10 @@ public Enumeration<URL> getResources(String name) throws IOException { |
576 | 688 | } |
577 | 689 | } |
578 | 690 | } |
579 | | - |
580 | | - return Collections.enumeration(result); |
| 691 | + // Give priority to WebApp resources (for example ServiceLoader provided by WebApp) |
| 692 | + List<URL> toRevert = new ArrayList<URL>(result); |
| 693 | + Collections.reverse(toRevert); |
| 694 | + return Collections.enumeration(toRevert); |
581 | 695 | } |
582 | 696 |
|
583 | 697 | @Override |
|
0 commit comments