/*
* 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.as;
import java.io.File;
import java.io.IOException;
import org.jboss.as.cli.CliInitializationException;
import org.jboss.as.cli.CommandContext;
import org.jboss.as.cli.CommandContextFactory;
import org.jboss.as.cli.CommandLineException;
import org.jboss.as.controller.client.helpers.ClientConstants;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.dmr.ModelNode;
import org.jboss.errai.cdi.server.gwt.util.StackTreeLogger;
import com.google.gwt.core.ext.ServletContainer;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
/**
* Acts as a an adaptor between gwt's ServletContainer interface and a JBoss
* AS/WildFly instance.
*
* @author Max Barkley <mbarkley@redhat.com>
* @author Christian Sadilek <csadilek@redhat.com>
*/
public class JBossServletContainerAdaptor extends ServletContainer {
private final CommandContext ctx;
private final int port;
private final StackTreeLogger logger;
private final String context;
@SuppressWarnings("unused")
private final Process jbossProcess;
private String nativeControllerPath = "remote://localhost:9999";
private String httpControllerPath = "http-remoting://localhost:9990";
private static final int MAX_RETRIES = 9;
/**
* Initialize the command context for a remote JBoss AS instance.
*
* @param port
* The port to which the JBoss instance binds.
* @param appRootDir
* The exploded war directory to be deployed.
* @param context
* The deployment context for the app.
* @param treeLogger
* For logging events from this container.
* @throws UnableToCompleteException
* Thrown if this container cannot properly connect or deploy.
*/
public JBossServletContainerAdaptor(int port, File appRootDir, String context, TreeLogger treeLogger,
Process jbossProcess) throws UnableToCompleteException {
this(port,appRootDir,context,treeLogger,jbossProcess,null, null);
}
/**
* Initialize the command context for a remote JBoss AS instance.
*
* @param port
* The port to which the JBoss instance binds.
* @param appRootDir
* The exploded war directory to be deployed.
* @param context
* The deployment context for the app.
* @param treeLogger
* For logging events from this container.
* @param httpRemotingAddress
* If not null, overrides HTTP_CONTROLLER_PATH property with the specified one.
* @param nativeRemotingAddress
* If not null, overrides NATIVE_CONTROLLER_PATH property with the specified one.
*
* @throws UnableToCompleteException
* Thrown if this container cannot properly connect or deploy.
*/
public JBossServletContainerAdaptor(int port, File appRootDir, String context, TreeLogger treeLogger,
Process jbossProcess,String httpRemotingAddress, String nativeRemotingAddress ) throws UnableToCompleteException {
this.port = port;
logger = new StackTreeLogger(treeLogger);
this.jbossProcess = jbossProcess;
this.context = context;
logger.branch(Type.INFO, "Starting container initialization...");
// Overrides remoting address if and only if an ovverride is passed.
if (httpRemotingAddress != null && !httpRemotingAddress.trim().equalsIgnoreCase(httpControllerPath)) {
logger.branch(Type.INFO, "Changing default 'httpControllerPath' property from ["+httpControllerPath+"] to ["+httpRemotingAddress+"]");
httpControllerPath = httpRemotingAddress;
}
if (nativeRemotingAddress != null && !nativeRemotingAddress.trim().equalsIgnoreCase(nativeControllerPath)) {
logger.branch(Type.INFO, "Changing default 'nativeControllerPath' property from ["+nativeControllerPath+"] to ["+nativeRemotingAddress+"]");
nativeControllerPath = nativeRemotingAddress;
}
CommandContext ctx = null;
try {
// Create command context
try {
logger.branch(Type.INFO, "Creating new command context...");
ctx = CommandContextFactory.getInstance().newCommandContext();
this.ctx = ctx;
logger.log(Type.INFO, "Command context created");
logger.unbranch();
}
catch (CliInitializationException e) {
logger.branch(TreeLogger.Type.ERROR, "Could not initialize JBoss AS command context", e);
throw new UnableToCompleteException();
}
attemptCommandContextConnection(MAX_RETRIES);
try {
// Undeploy the app in case the container/devmode wasn't shutdown correctly which should
// have removed the deployment (see stop method).
removeDeployment();
/*
* Need to add deployment resource to specify exploded archive
*
* path : the absolute path the deployment file/directory archive : true
* iff the an archived file, false iff an exploded archive enabled :
* true iff war should be automatically scanned and deployed
*/
logger.branch(Type.INFO,
String.format("Adding deployment %s at %s...", getAppName(), appRootDir.getAbsolutePath()));
final ModelNode operation = getAddOperation(appRootDir.getAbsolutePath());
final ModelNode result = ctx.getModelControllerClient().execute(operation);
if (!Operations.isSuccessfulOutcome(result)) {
logger.log(Type.ERROR, String.format("Could not add deployment:\nInput:\n%s\nOutput:\n%s",
operation.toJSONString(false), result.toJSONString(false)));
throw new UnableToCompleteException();
}
logger.log(Type.INFO, "Deployment resource added");
logger.unbranch();
}
catch (IOException e) {
logger.branch(Type.ERROR, String.format("Could not add deployment %s", getAppName()), e);
throw new UnableToCompleteException();
}
attemptDeploy();
}
catch (UnableToCompleteException e) {
logger.branch(Type.INFO, "Attempting to stop container...");
stopHelper();
throw e;
}
}
@Override
public int getPort() {
return port;
}
@Override
public void refresh() throws UnableToCompleteException {
attemptDeploymentRelatedOp(ClientConstants.DEPLOYMENT_REDEPLOY_OPERATION);
}
@Override
public void stop() throws UnableToCompleteException {
try {
logger.branch(Type.INFO, String.format("Removing %s from deployments...", getAppName()));
ModelNode result = removeDeployment();
if (!Operations.isSuccessfulOutcome(result)) {
logger.log(
Type.ERROR,
String.format("Could not undeploy AS:\nInput:\n%s\nOutput:\n%s", getAppName(),
result.toJSONString(false)));
throw new UnableToCompleteException();
}
logger.log(Type.INFO, String.format("%s removed", getAppName()));
logger.unbranch();
}
catch (IOException e) {
logger.log(Type.ERROR, "Could not shutdown AS", e);
throw new UnableToCompleteException();
}
finally {
stopHelper();
}
}
private ModelNode removeDeployment() throws IOException {
final ModelNode operation = Operations.createRemoveOperation(
new ModelNode().add(ClientConstants.DEPLOYMENT, getAppName()));
return ctx.getModelControllerClient().execute(operation);
}
private void attemptCommandContextConnection(final int maxRetries)
throws UnableToCompleteException {
String[] controllers = new String[] { httpControllerPath, nativeControllerPath };
final String[] protocols = new String[controllers.length];
for (int i = 0; i < controllers.length; i++) {
protocols[i] = controllers[i].split(":", 2)[0];
}
for (int retry = 0; retry < maxRetries; retry++) {
for (int i = 0; i < controllers.length; i++) {
final String controller = controllers[i];
final String protocol = protocols[i];
try {
logger.branch(Type.INFO, String.format("Attempting to connect with %s protocol.", protocol));
ctx.connectController(controller);
logger.log(Type.INFO, "Connected to JBoss AS");
return;
}
catch (CommandLineException e) {
logger.log(
Type.INFO,
String.format("Attempt %d failed at connecting with %s protocol", retry + 1, protocol),
e);
}
finally {
logger.unbranch();
}
}
// No connection attempts have succeeded, so wait a bit before trying
// again.
if (retry < maxRetries) {
try {
Thread.sleep(1000);
}
catch (InterruptedException e1) {
logger.log(Type.WARN, "Thread was interrupted while waiting for AS to reload", e1);
}
}
}
logger.log(Type.ERROR, "Could not connect to AS");
throw new UnableToCompleteException();
}
private void stopHelper() {
logger.branch(Type.INFO, "Attempting to stop JBoss AS instance...");
/*
* There is a problem with Process#destroy where it will not reliably kill
* the JBoss instance. So instead we must try and send a shutdown signal. If
* that is not possible or does not work, we will log it's failure, advising
* the user to manually kill this process.
*/
try {
if (ctx.getControllerHost() == null) {
ctx.handle("connect localhost:9999");
}
ctx.handle(":shutdown");
logger.log(Type.INFO, "JBoss AS instance stopped");
logger.unbranch();
}
catch (CommandLineException e) {
logger.log(Type.ERROR, "Could not shutdown JBoss AS instance. "
+ "Restarting this container while a JBoss AS instance is still running will cause errors.");
}
logger.branch(Type.INFO, "Terminating command context...");
ctx.terminateSession();
logger.log(Type.INFO, "Command context terminated");
logger.unbranch();
}
private void attemptDeploy() throws UnableToCompleteException {
attemptDeploymentRelatedOp(ClientConstants.DEPLOYMENT_DEPLOY_OPERATION);
}
private void attemptDeploymentRelatedOp(final String opName) throws UnableToCompleteException {
try {
logger.branch(Type.INFO, String.format("Deploying %s...", getAppName()));
final ModelNode operation = Operations.createOperation(opName,
new ModelNode().add(ClientConstants.DEPLOYMENT, getAppName()));
final ModelNode result = ctx.getModelControllerClient().execute(operation);
if (!Operations.isSuccessfulOutcome(result)) {
logger.log(
Type.ERROR,
String.format("Could not %s %s:\nInput:\n%s\nOutput:\n%s", opName, getAppName(),
operation.toJSONString(false), result.toJSONString(false)));
throw new UnableToCompleteException();
}
logger.log(Type.INFO, String.format("%s %sed", getAppName(), opName));
logger.unbranch();
}
catch (IOException e) {
logger.branch(Type.ERROR, String.format("Could not %s %s", opName, getAppName()), e);
throw new UnableToCompleteException();
}
}
/**
* @return The runtime-name for the given deployment.
*/
private String getAppName() {
// Deployment names must end with .war
return context.endsWith(".war") ? context : context + ".war";
}
private ModelNode getAddOperation(String path) {
final ModelNode command = Operations.createAddOperation(new ModelNode().add(ClientConstants.DEPLOYMENT,
getAppName()));
final ModelNode content = new ModelNode();
final ModelNode contentObj = new ModelNode();
// Construct content list
contentObj.get("path").set(path);
contentObj.get("archive").set(false);
content.add(contentObj);
command.get("content").set(content);
command.get("enabled").set(false);
return command;
}
}