/**
* Copyright 2007-2015, Kaazing Corporation. All rights reserved.
*
* 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.kaazing.k3po.driver.internal.control.handler;
import static java.lang.String.format;
import static java.lang.Thread.currentThread;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.FileSystems.newFileSystem;
import static org.kaazing.k3po.lang.internal.parser.ScriptParseStrategy.PROPERTY_NODE;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.logging.InternalLogger;
import org.jboss.netty.logging.InternalLoggerFactory;
import org.kaazing.k3po.driver.internal.Robot;
import org.kaazing.k3po.driver.internal.control.AwaitMessage;
import org.kaazing.k3po.driver.internal.control.DisposedMessage;
import org.kaazing.k3po.driver.internal.control.ErrorMessage;
import org.kaazing.k3po.driver.internal.control.FinishedMessage;
import org.kaazing.k3po.driver.internal.control.NotifiedMessage;
import org.kaazing.k3po.driver.internal.control.NotifyMessage;
import org.kaazing.k3po.driver.internal.control.PrepareMessage;
import org.kaazing.k3po.driver.internal.control.PreparedMessage;
import org.kaazing.k3po.driver.internal.control.StartedMessage;
import org.kaazing.k3po.lang.internal.parser.ScriptParseException;
import org.kaazing.k3po.lang.internal.parser.ScriptParserImpl;
public class ControlServerHandler extends ControlUpstreamHandler {
private static final Map<String, Object> EMPTY_ENVIRONMENT = Collections.<String, Object>emptyMap();
private static final InternalLogger logger = InternalLoggerFactory.getInstance(ControlServerHandler.class);
private Robot robot;
private ChannelFutureListener whenAbortedOrFinished;
private BlockingQueue<CountDownLatch> notifiedLatches = new LinkedBlockingQueue<CountDownLatch>();
private final ChannelFuture channelClosedFuture = Channels.future(null);
private ClassLoader scriptLoader;
public void setScriptLoader(ClassLoader scriptLoader) {
this.scriptLoader = scriptLoader;
}
// Note that this is more than just the channel close future. It's a future that means not only
// that this channel has closed but it is a future that tells us when this obj has processed the closed event.
public ChannelFuture getChannelClosedFuture() {
return channelClosedFuture;
}
@Override
public void channelClosed(final ChannelHandlerContext ctx, final ChannelStateEvent e) throws Exception {
if (robot != null) {
robot.dispose().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
channelClosedFuture.setSuccess();
ctx.sendUpstream(e);
}
});
}
}
@Override
public void prepareReceived(final ChannelHandlerContext ctx, MessageEvent evt) throws Exception {
final PrepareMessage prepare = (PrepareMessage) evt.getMessage();
// enforce control protocol version
String version = prepare.getVersion();
if (!"2.0".equals(version)) {
sendVersionError(ctx);
return;
}
List<String> scriptNames = prepare.getNames();
if (logger.isDebugEnabled()) {
logger.debug("preparing script(s) " + scriptNames);
}
robot = new Robot();
whenAbortedOrFinished = whenAbortedOrFinished(ctx);
String originScript = "";
String origin = prepare.getOrigin();
if (origin != null) {
try {
originScript = OriginScript.get(origin);
} catch (URISyntaxException e) {
throw new Exception("Could not find origin: ", e);
}
}
ChannelFuture prepareFuture;
try {
String aggregatedScript = originScript + aggregateScript(scriptNames, scriptLoader);
List<String> properyOverrides = prepare.getProperties();
// consider hard fail in the future, when test frameworks support
// override per test method
// Checks that it is a supported version
if (!"2.0".equals(version)) {
sendVersionError(ctx);
}
aggregatedScript = injectOverridenProperties(aggregatedScript, properyOverrides);
if (scriptLoader != null) {
Thread currentThread = currentThread();
ClassLoader contextClassLoader = currentThread.getContextClassLoader();
try {
currentThread.setContextClassLoader(scriptLoader);
prepareFuture = robot.prepare(aggregatedScript);
} finally {
currentThread.setContextClassLoader(contextClassLoader);
}
} else {
prepareFuture = robot.prepare(aggregatedScript);
}
final String scriptToRun = aggregatedScript;
prepareFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture f) {
PreparedMessage prepared = new PreparedMessage();
prepared.setScript(scriptToRun);
prepared.getBarriers().addAll(robot.getBarriersByName().keySet());
Channels.write(ctx, Channels.future(null), prepared);
}
});
} catch (Exception e) {
sendErrorMessage(ctx, e);
return;
}
}
private String injectOverridenProperties(String aggregatedScript, List<String> scriptProperties)
throws Exception, ScriptParseException {
ScriptParserImpl parser = new ScriptParserImpl();
for (String propertyToInject : scriptProperties) {
String propertyName = parser.parseWithStrategy(propertyToInject, PROPERTY_NODE).getPropertyName();
StringBuilder replacementScript = new StringBuilder();
Pattern pattern = Pattern.compile("property\\s+" + propertyName + "\\s+.+");
boolean matchFound = false;
for (String scriptLine : aggregatedScript.split("\\r?\\n")) {
if (pattern.matcher(scriptLine).matches()) {
matchFound = true;
replacementScript.append(propertyToInject + "\n");
} else {
replacementScript.append(scriptLine + "\n");
}
}
if (!matchFound) {
String errorMsg = "Received " + propertyToInject + " in PREPARE but found no where to substitute it";
logger.error(errorMsg);
throw new Exception(errorMsg);
}
aggregatedScript = replacementScript.toString();
}
return aggregatedScript;
}
/*
* Public static because it is used in test utils
*/
public static String aggregateScript(List<String> scriptNames, ClassLoader scriptLoader)
throws URISyntaxException, IOException {
final StringBuilder aggregatedScript = new StringBuilder();
for (String scriptName : scriptNames) {
String scriptNameWithExtension = format("%s.rpt", scriptName);
Path scriptPath = Paths.get(scriptNameWithExtension);
scriptNameWithExtension = URI.create(scriptNameWithExtension).normalize().getPath();
String script = null;
assert !scriptPath.isAbsolute();
// resolve relative scripts in local file system
if (scriptLoader != null) {
// resolve relative scripts from class loader to support
// separated specification projects that include Robot scripts only
URL resource = scriptLoader.getResource(scriptNameWithExtension);
if (resource != null) {
URI resourceURI = resource.toURI();
if ("file".equals(resourceURI.getScheme())) {
Path resourcePath = Paths.get(resourceURI);
script = readScript(resourcePath);
} else {
try (FileSystem fileSystem = newFileSystem(resourceURI, EMPTY_ENVIRONMENT)) {
Path resourcePath = Paths.get(resourceURI);
script = readScript(resourcePath);
}
}
}
}
if (script == null) {
throw new RuntimeException("Script not found: " + scriptPath);
}
aggregatedScript.append(script);
}
return aggregatedScript.toString();
}
private static String readScript(Path scriptPath) throws IOException {
List<String> lines = Files.readAllLines(scriptPath, UTF_8);
StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(line);
sb.append("\n");
}
String script = sb.toString();
return script;
}
@Override
public void startReceived(final ChannelHandlerContext ctx, MessageEvent evt) throws Exception {
try {
ChannelFuture startFuture = robot.start();
startFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture f) {
if (f.isSuccess()) {
final StartedMessage started = new StartedMessage();
Channels.write(ctx, Channels.future(null), started);
} else {
sendErrorMessage(ctx, f.getCause());
}
}
});
} catch (Exception e) {
sendErrorMessage(ctx, e);
return;
}
assert whenAbortedOrFinished != null;
robot.finish().addListener(whenAbortedOrFinished);
}
@Override
public void abortReceived(final ChannelHandlerContext ctx, MessageEvent evt) throws Exception {
if (logger.isInfoEnabled()) {
logger.info("ABORT");
}
assert whenAbortedOrFinished != null;
robot.abort().addListener(whenAbortedOrFinished);
}
@Override
public void notifyReceived(final ChannelHandlerContext ctx, MessageEvent evt) throws Exception {
NotifyMessage notifyMessage = (NotifyMessage) evt.getMessage();
final String barrier = notifyMessage.getBarrier();
if (logger.isDebugEnabled()) {
logger.debug("NOTIFY: " + barrier);
}
writeNotifiedOnBarrier(barrier, ctx);
robot.notifyBarrier(barrier);
}
@Override
public void awaitReceived(final ChannelHandlerContext ctx, MessageEvent evt) throws Exception {
AwaitMessage awaitMessage = (AwaitMessage) evt.getMessage();
final String barrier = awaitMessage.getBarrier();
if (logger.isDebugEnabled()) {
logger.debug("AWAIT: " + barrier);
}
writeNotifiedOnBarrier(barrier, ctx);
}
private void writeNotifiedOnBarrier(final String barrier, final ChannelHandlerContext ctx) throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
// Make sure finished message does not get sent before this notified message
notifiedLatches.add(latch);
robot.awaitBarrier(barrier).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
try {
if (future.isSuccess()) {
logger.debug("sending NOTIFIED: " + barrier);
final NotifiedMessage notified = new NotifiedMessage();
notified.setBarrier(barrier);
Channels.write(ctx, Channels.future(null), notified);
}
}
finally {
latch.countDown();
}
}
});
}
@Override
public void disposeReceived(final ChannelHandlerContext ctx, MessageEvent evt) throws Exception {
robot.dispose().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
writeDisposed(ctx);
}
});
}
private void writeDisposed(ChannelHandlerContext ctx) {
Channel channel = ctx.getChannel();
DisposedMessage disposedMessage = new DisposedMessage();
channel.write(disposedMessage);
}
private ChannelFutureListener whenAbortedOrFinished(final ChannelHandlerContext ctx) {
final AtomicBoolean oneTimeOnly = new AtomicBoolean();
return new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (oneTimeOnly.compareAndSet(false, true)) {
for (CountDownLatch latch : notifiedLatches) {
latch.await();
}
sendFinishedMessage(ctx);
}
}
};
}
private void sendFinishedMessage(ChannelHandlerContext ctx) {
Channel channel = ctx.getChannel();
String observedScript = robot.getObservedScript();
FinishedMessage finished = new FinishedMessage();
finished.setScript(observedScript);
channel.write(finished);
}
private void sendVersionError(ChannelHandlerContext ctx) {
Channel channel = ctx.getChannel();
ErrorMessage error = new ErrorMessage();
error.setSummary("Bad control protocol version");
error.setDescription("Robot requires control protocol version 2.0");
channel.write(error);
}
private void sendErrorMessage(ChannelHandlerContext ctx, Throwable throwable) {
ErrorMessage error = new ErrorMessage();
error.setDescription(throwable.getMessage());
if (throwable instanceof ScriptParseException) {
if (logger.isDebugEnabled()) {
logger.error("Caught exception trying to parse script. Sending error to client", throwable);
} else {
logger.error("Caught exception trying to parse script. Sending error to client. Due to " + throwable);
}
error.setSummary("Parse Error");
Channels.write(ctx, Channels.future(null), error);
} else {
logger.error("Internal Error. Sending error to client", throwable);
error.setSummary("Internal Error");
Channels.write(ctx, Channels.future(null), error);
}
}
}