/* * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.errai.cdi.server.gwt; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.BindException; import java.util.List; import java.util.regex.Pattern; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.io.filefilter.TrueFileFilter; import org.apache.commons.lang3.StringUtils; import org.jboss.errai.cdi.server.as.JBossServletContainerAdaptor; import org.jboss.errai.cdi.server.gwt.util.JBossUtil; import org.jboss.errai.cdi.server.gwt.util.StackTreeLogger; import org.wildfly.core.embedded.EmbeddedProcessFactory; import org.wildfly.core.embedded.StandaloneServer; import com.google.gwt.core.ext.ServletContainer; import com.google.gwt.core.ext.ServletContainerLauncher; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.TreeLogger.Type; import com.google.gwt.core.ext.UnableToCompleteException; /** * A {@link ServletContainerLauncher} controlling an embedded WildFly instance. * * This launcher will add exclusions for client-side classes to the project's * beans.xml. These classes are not needed on the server and are going to cause * class loading issues if deployed (i.e. because they reference GWT classes * that are not present at runtime). By default, classes in packages under * /client/local will be considered client-side but this can be configured using * the system property errai.client.local.class.pattern. Existing exclusions * will stay intact. If no beans.xml is present, a new one will be created. * * @author Christian Sadilek <christian.sadilek@gmail.com> */ public class EmbeddedWildFlyLauncher extends ServletContainerLauncher { private static final String CLIENT_LOCAL_CLASS_PATTERN_PROPERTY = "errai.client.local.class.pattern"; private static final String DEFAULT_CLIENT_LOCAL_CLASS_PATTERN = ".*/client/local/.*"; private static final String ERRAI_SCANNER_HINT_START = "\n <!-- These exclusions were added by Errai to avoid deploying client-side classes to the server -->\n"; private static final String ERRAI_SCANNER_HINT_END = " <!-- End of Errai exclusions -->\n"; private static final String DEVMODE_BEANS_XML_TEMPLATE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<beans xmlns=\"http://xmlns.jcp.org/xml/ns/javaee\">\n" + " <scan>" + "$EXCLUSIONS" + " </scan>\n" + "</beans>"; private static final String DEVMODE_BEANS_XML_EXCLUSION_TEMPLATE = " <exclude name = \"$CLASSNAME\" />\n"; private static final String ERRAI_PROPERTIES_HINT_START = "#Errai-Start\n"; private static final String ERRAI_PROPERTIES_HINT_END = "\n#Errai-End\n"; private static final String ERRAI_PROPERTIES_REALM_TOKEN = "\n#$REALM_NAME=ApplicationRealm$ This line is used by " + "the add-user utility to identify the realm name already used in this file.\n"; static { System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager"); } private StackTreeLogger logger; @Override public ServletContainer start(final TreeLogger treeLogger, final int port, final File appRootDir) throws BindException, Exception { logger = new StackTreeLogger(treeLogger); try { final String jbossHome = JBossUtil.getJBossHome(logger); final String[] cmdArgs = JBossUtil.getCommandArguments(logger); System.setProperty("jboss.http.port", "" + port); File cliConfigFile = new File(jbossHome, JBossUtil.CLI_CONFIGURATION_FILE); if (cliConfigFile.exists()) { System.setProperty("jboss.cli.config", cliConfigFile.getAbsolutePath()); } final StandaloneServer embeddedWildFly = EmbeddedProcessFactory.createStandaloneServer(jbossHome, null, new String[0], cmdArgs); embeddedWildFly.start(); prepareBeansXml(appRootDir); prepareUsersAndRoles(jbossHome); JBossServletContainerAdaptor controller = new JBossServletContainerAdaptor(port, appRootDir, JBossUtil.getDeploymentContext(), logger.peek(), null); return controller; } catch (UnableToCompleteException e) { logger.log(Type.ERROR, "Could not start servlet container controller", e); throw new UnableToCompleteException(); } } /** * Reads application-users.properties and application-roles.properties from * the classpath and amends the corresponding files under * standalone/configuration. This allows applications to specify users and * roles for development mode. */ private void prepareUsersAndRoles(final String jbossHome) { InputStream appUsersStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(JBossUtil.APP_USERS_PROPERTY_FILE); if (appUsersStream != null) { processPropertiesFile(JBossUtil.APP_USERS_PROPERTY_FILE, jbossHome, appUsersStream, true); } InputStream appRolesStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(JBossUtil.APP_ROLES_PROPERTY_FILE); if (appRolesStream != null) { processPropertiesFile(JBossUtil.APP_ROLES_PROPERTY_FILE, jbossHome, appRolesStream, false); } InputStream mgmtUsersStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(JBossUtil.MGMT_USERS_PROPERTY_FILE); if (mgmtUsersStream != null) { processPropertiesFile(JBossUtil.MGMT_USERS_PROPERTY_FILE, jbossHome, mgmtUsersStream, true); } InputStream mgmtGroupsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(JBossUtil.MGMT_GROUPS_PROPERTY_FILE); if (mgmtGroupsStream != null) { processPropertiesFile(JBossUtil.MGMT_GROUPS_PROPERTY_FILE, jbossHome, mgmtGroupsStream, false); } } private void processPropertiesFile(final String propertyFileName, final String jbossHome, final InputStream newUsersStream, final boolean handleRealmToken) { final File propertyDir = new File(jbossHome, JBossUtil.STANDALONE_CONFIGURATION); final File propertyFile = new File(propertyDir, propertyFileName); try { String realmToken = ERRAI_PROPERTIES_REALM_TOKEN; boolean isErraiContent = false; final StringBuilder result = new StringBuilder(); List<String> lines = FileUtils.readLines(propertyFile); if (lines != null) { for (final String currentLine : lines) { String trimmed = currentLine.trim(); if(trimmed.startsWith(ERRAI_PROPERTIES_HINT_START.trim())) { isErraiContent = true; } else if(trimmed.startsWith(ERRAI_PROPERTIES_HINT_END.trim())) { isErraiContent = false; } else if(handleRealmToken && trimmed.startsWith("#") && trimmed.contains("$REALM_NAME=")) { realmToken = currentLine; } else if (!isErraiContent) { result.append(currentLine).append("\n"); } } final String newUsersStr = IOUtils.toString(newUsersStream, (String) null); result.append(ERRAI_PROPERTIES_HINT_START); result.append(newUsersStr).append("\n"); result.append(ERRAI_PROPERTIES_HINT_END); if (handleRealmToken) { result.append(realmToken).append("\n"); } try { FileUtils.write(propertyFile, result.toString()); } catch (IOException e) { throw new RuntimeException("Failed to write content for " + propertyFileName + " in " + propertyFile.getAbsolutePath(), e); } } } catch (IOException e) { throw new RuntimeException("Failed to parse content for " + propertyFileName + " in " + propertyFile.getAbsolutePath(), e); } } /** * Writes a new or updates an existing beans.xml file to add exclusions for * client-only classes. We do this to avoid class loading problems on * deployment. * * @param appRootDir * the root application directory (configured as <hostedWebapp> when * using the gwt-maven-plugin). * @throws IOException */ private void prepareBeansXml(File appRootDir) throws IOException { File webInfDir = new File(appRootDir, "WEB-INF"); File classesDir = new File(webInfDir, "classes"); StringBuilder exclusions = new StringBuilder(); exclusions.append(ERRAI_SCANNER_HINT_START); for (File clientLocalClass : FileUtils.listFiles(appRootDir, new ClientLocalFileFilter(), TrueFileFilter.INSTANCE)) { final String className = clientLocalClass.getAbsolutePath(); // Adding package-info exclusion makes beans.xml invalid. if (className.endsWith(".class") && !className.endsWith("package-info.class")) { final String exclusion = className .replace(classesDir.getAbsolutePath(), "") .replace(".class", "") .replace(File.separator, ".") .substring(1); exclusions.append(DEVMODE_BEANS_XML_EXCLUSION_TEMPLATE.replace("$CLASSNAME", exclusion)); } } exclusions.append(ERRAI_SCANNER_HINT_END); File beansXml = new File(webInfDir, "beans.xml"); String beansXmlContent; if (!beansXml.exists()) { beansXmlContent = DEVMODE_BEANS_XML_TEMPLATE.replace("$EXCLUSIONS", exclusions.toString()); } else { beansXmlContent = FileUtils.readFileToString(beansXml); beansXmlContent = removeExistingErraiExclusions(beansXmlContent); if (beansXmlContent.contains(exclusions)) return; if (beansXmlContent.contains("<scan>")) { beansXmlContent = beansXmlContent.replace("<scan>", "<scan>" + exclusions); } else { beansXmlContent = beansXmlContent.replace("</beans>", " <scan>" + exclusions + " </scan>\n</beans>"); } validateBeansXml(beansXmlContent); } FileUtils.write(beansXml, beansXmlContent); } private void validateBeansXml(String beansXmlContent) { if (beansXmlContent.contains("beans_1_0.xsd")) { logger.log(Type.WARN, "Your beans.xml file doesn't not allow for CDI 1.1! " + "Please remove the CDI 1.0 XML Schema."); } } private String removeExistingErraiExclusions(String beansXmlContent) { String oldExclusions = StringUtils.substringBetween(beansXmlContent, ERRAI_SCANNER_HINT_START, ERRAI_SCANNER_HINT_END); beansXmlContent = beansXmlContent.replace(ERRAI_SCANNER_HINT_START, ""); if (oldExclusions != null) { beansXmlContent = beansXmlContent.replace(oldExclusions, ""); } beansXmlContent = beansXmlContent.replace(ERRAI_SCANNER_HINT_END, ""); return beansXmlContent; } private class ClientLocalFileFilter implements IOFileFilter { final String clientLocalClassPatternString = System.getProperty(CLIENT_LOCAL_CLASS_PATTERN_PROPERTY, DEFAULT_CLIENT_LOCAL_CLASS_PATTERN); final Pattern clientLocalClassPattern = Pattern.compile(clientLocalClassPatternString); @Override public boolean accept(File pathName) { return accept(pathName.getAbsolutePath()); } @Override public boolean accept(File dir, String file) { String fullName = dir.getAbsolutePath() + File.separator + file; return accept(fullName); } private boolean accept(String fileName) { return clientLocalClassPattern.matcher(fileName).matches(); } } }