Skip to content

Commit

Permalink
Enhanced the pipeline to propagate the watching exception to the wisd…
Browse files Browse the repository at this point in the history
…om server (#26)

Signed-off-by: Clement Escoffier <[email protected]>
  • Loading branch information
cescoffier committed Jun 1, 2014
1 parent 67d4375 commit 8819a0a
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private void invokeCoffeeScriptCompiler(File input, File out) throws WatchingExc
public WatchingException build(String message) {
String[] lines = message.split("\n");
for (String l : lines) {
if (! Strings.isNullOrEmpty(l)) {
if (!Strings.isNullOrEmpty(l)) {
message = l.trim();
break;
}
Expand All @@ -178,7 +178,8 @@ public WatchingException build(String message) {
String character = matcher.group(3);
String reason = matcher.group(4);
File file = new File(path);
return new WatchingException(reason, file, Integer.valueOf(line), Integer.valueOf(character), null);
return new WatchingException("CoffeeScript Compilation Error: " + reason, file,
Integer.valueOf(line), Integer.valueOf(character), null);
} else {
return new WatchingException("CoffeeScript Compilation Error : " + message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,26 @@
*/
package org.wisdom.maven.pipeline;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.apache.maven.plugin.Mojo;
import org.json.simple.JSONObject;
import org.wisdom.maven.Watcher;
import org.wisdom.maven.WatchingException;
import org.wisdom.maven.utils.DefensiveThreadFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* The pipeline is the spine of the watching system of Wisdom.
* Each Mojo, i.e. Maven Plugin, willing to become a watcher, will be plugged to the pipeline. Registration is made
* using org.wisdom.maven.pipeline.Watchers#add(org.apache.maven.execution.MavenSession, org.wisdom.maven.Watcher).
* <p/>
* <p>
* The pipeline is an internal class and should not be used directly. It just delegates the file events to the
* watchers. So this class holds the file alteration monitor that triggers the reactions of the different mojos.
*/
Expand All @@ -47,14 +50,20 @@ public class Pipeline {
private FileAlterationMonitor watcher;
private final File baseDir;

/**
* The file used to store a JSON representation of Watching Exceptions happening on a watched event.
*/
private final File error;

private final static String WATCHING_EXCEPTION_MESSAGE = "Watching exception: %s (check log for more details)";


/**
* Creates a new pipeline. Notice that the set of watchers cannot change.
* @param mojo the 'run' mojo
*
* @param mojo the 'run' mojo
* @param baseDir the base directory of the watched project
* @param list the set of watchers plugged on the pipeline, the order of the list will be the notification order.
* @param list the set of watchers plugged on the pipeline, the order of the list will be the notification order.
*/
public Pipeline(Mojo mojo, File baseDir, List<? extends Watcher> list) {
this.mojo = mojo;
Expand All @@ -64,6 +73,9 @@ public Pipeline(Mojo mojo, File baseDir, List<? extends Watcher> list) {
for (Object o : list) {
watchers.add(new WatcherDelegate(o));
}
File pipelineDirectory = new File(baseDir, "target/pipeline");
mojo.getLog().debug("Creating the target/pipeline directory : " + pipelineDirectory.mkdirs());
error = new File(pipelineDirectory, "error.json");
}

/**
Expand All @@ -80,6 +92,7 @@ public void shutdown() {

/**
* Starts the watching.
*
* @return the current pipeline.
*/
public Pipeline watch() {
Expand All @@ -100,9 +113,11 @@ public Pipeline watch() {

/**
* The FAM has detected a new file. It dispatches this event to the watchers plugged on the current pipeline.
*
* @param file the created file
*/
public void onFileCreate(File file) {
cleanupErrorFile();
mojo.getLog().info(EMPTY_STRING);
mojo.getLog().info("The watcher has detected a new file: " + file.getAbsolutePath());
mojo.getLog().info(EMPTY_STRING);
Expand All @@ -116,6 +131,7 @@ public void onFileCreate(File file) {
mojo.getLog().debug(watcher + " has thrown an exception while handling the " + file.getName() + EMPTY_STRING +
" creation", e);
mojo.getLog().error(String.format(WATCHING_EXCEPTION_MESSAGE, e.getMessage()));
createErrorFile(e);
continueProcessing = false;
}
if (!continueProcessing) {
Expand All @@ -127,12 +143,52 @@ public void onFileCreate(File file) {
mojo.getLog().info(EMPTY_STRING);
}

/**
* Creates the error file storing the information from the given exception in JSON. This file is consumed by the
* Wisdom server to generate an error page reporting the watching exception.
*
* @param e the exception
*/
@SuppressWarnings("unchecked")
private void createErrorFile(WatchingException e) {
mojo.getLog().debug("Creating error file for '" + e.getMessage() + "' happening at " + e.getLine() + ":" + e
.getCharacter() + " of " + e.getFile());
JSONObject obj = new JSONObject();
obj.put("message", e.getMessage());
if (e.getFile() != null) {
obj.put("file", e.getFile().getAbsolutePath());
}
if (e.getLine() != -1) {
obj.put("line", e.getLine());
}
if (e.getCharacter() != -1) {
obj.put("character", e.getCharacter());
}
if (e.getCause() != null) {
obj.put("cause", e.getCause().getMessage());
}
try {
FileUtils.writeStringToFile(error, obj.toJSONString(), false);
} catch (IOException e1) {
mojo.getLog().error("Cannot write the error file", e1);
}
}

/**
* Method called on each event before the processing, deleting the error file is this file exists.
*/
private void cleanupErrorFile() {
FileUtils.deleteQuietly(error);
}

/**
* The FAM has detected a change in a file. It dispatches this event to the watchers plugged on the current
* pipeline.
*
* @param file the updated file
*/
public void onFileChange(File file) {
cleanupErrorFile();
mojo.getLog().info(EMPTY_STRING);
mojo.getLog().info("The watcher has detected a change in " + file.getAbsolutePath());
mojo.getLog().info(EMPTY_STRING);
Expand All @@ -146,6 +202,7 @@ public void onFileChange(File file) {
mojo.getLog().debug(watcher + " has thrown an exception while handling the " + file.getName() + EMPTY_STRING +
" update", e);
mojo.getLog().error(String.format(WATCHING_EXCEPTION_MESSAGE, e.getMessage()));
createErrorFile(e);
continueProcessing = false;
}
if (!continueProcessing) {
Expand All @@ -160,9 +217,11 @@ public void onFileChange(File file) {
/**
* The FAM has detected a file deletion. It dispatches this event to the watchers plugged on the current
* pipeline.
*
* @param file the deleted file
*/
public void onFileDelete(File file) {
cleanupErrorFile();
mojo.getLog().info(EMPTY_STRING);
mojo.getLog().info("The watcher has detected a deleted file: " + file.getAbsolutePath());
mojo.getLog().info(EMPTY_STRING);
Expand All @@ -176,6 +235,7 @@ public void onFileDelete(File file) {
mojo.getLog().debug(watcher + " has thrown an exception while handling the " + file.getName() + EMPTY_STRING +
" deletion", e);
mojo.getLog().error(String.format(WATCHING_EXCEPTION_MESSAGE, e.getMessage()));
createErrorFile(e);
continueProcessing = false;
}
if (!continueProcessing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
/**
* An implementation of watcher using delegation.
* The invocation are delegate on an object by reflection. This is necessary because the retrieve watchers are not
* loaded with the same classloader as the run mojo. This comes form the forked lifecycle done by the run mojo.
* loaded with the same classloader as the run mojo. This comes from the forked lifecycle done by the run mojo.
*/
public class WatcherDelegate implements Watcher {

Expand All @@ -42,10 +42,10 @@ public class WatcherDelegate implements Watcher {
public WatcherDelegate(Object delegate) {
this.delegate = delegate;
try {
this.accept = delegate.getClass().getMethod("accept", new Class[] {File.class});
this.fileCreated = delegate.getClass().getMethod("fileCreated", new Class[] {File.class});
this.fileUpdated = delegate.getClass().getMethod("fileUpdated", new Class[] {File.class});
this.fileDeleted = delegate.getClass().getMethod("fileDeleted", new Class[] {File.class});
this.accept = delegate.getClass().getMethod("accept", new Class[]{File.class});
this.fileCreated = delegate.getClass().getMethod("fileCreated", new Class[]{File.class});
this.fileUpdated = delegate.getClass().getMethod("fileUpdated", new Class[]{File.class});
this.fileDeleted = delegate.getClass().getMethod("fileDeleted", new Class[]{File.class});
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
Expand All @@ -67,18 +67,52 @@ public boolean fileCreated(File file) throws WatchingException {
try {
return (Boolean) fileCreated.invoke(delegate, file);
} catch (InvocationTargetException e) { //NOSONAR
throw new WatchingException(e.getTargetException().getMessage(), file, e.getTargetException());
throw createWatchingException(e, file);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

/**
* Even {@link org.wisdom.maven.WatchingException} cannot be used directly, so we need to use reflection to
* recreate the exception with the same content.
* <p>
* This method checks if the cause of the {@link java.lang.reflect
* .InvocationTargetException} is a {@link org.wisdom.maven.WatchingException},
* in this case if create an exception with the same content. Otherwise it creates a new {@link org.wisdom.maven
* .WatchingException} from the given exception's cause.
*
* @param exception the invocation target exception caught by the delegate
* @param file the file having thrown the exception (the processed file).
* @return a Watching Exception containing the content from the given exception if possible or a new exception
* from the {@literal exception}'s cause.
*/
public static WatchingException createWatchingException(InvocationTargetException exception, File file) {
Throwable cause = exception.getTargetException();
if (WatchingException.class.getName().equals(exception.getTargetException().getClass().getName())) {
try {
Method line = cause.getClass().getMethod("getLine");
Method character = cause.getClass().getMethod("getCharacter");
Method source = cause.getClass().getMethod("getFile");

return new WatchingException(cause.getMessage(), (File) source.invoke(cause),
(Integer) line.invoke(cause),
(Integer) character.invoke(cause),
cause.getCause());
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
throw new IllegalArgumentException("Cannot create the watching exception from " + cause);
}
} else {
return new WatchingException(cause.getMessage(), file, cause);
}
}

@Override
public boolean fileUpdated(File file) throws WatchingException {
try {
return (Boolean) fileUpdated.invoke(delegate, file);
} catch (InvocationTargetException e) { //NOSONAR
throw new WatchingException(e.getTargetException().getMessage(), file, e.getTargetException());
throw createWatchingException(e, file);
} catch (Exception e) {
throw new RuntimeException(e);
}
Expand All @@ -89,7 +123,7 @@ public boolean fileDeleted(File file) throws WatchingException {
try {
return (Boolean) fileDeleted.invoke(delegate, file);
} catch (InvocationTargetException e) { //NOSONAR
throw new WatchingException(e.getTargetException().getMessage(), file, e.getTargetException());
throw createWatchingException(e, file);
} catch (Exception e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ public static WatchingException build(AbstractWisdomMojo mojo, Throwable excepti
String character = matcher.group(3);
String reason = matcher.group(4);
File file = new File(path);
return new WatchingException(reason, file, Integer.valueOf(line), Integer.valueOf(character), null);
return new WatchingException("Java Compilation Error: " + reason, file, Integer.valueOf(line),
Integer.valueOf(character), null);
} else {
return new WatchingException("Java Compilation Error", exception);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@ public class PipelineTest {
Pipeline pipeline;
private SpyWatcher textWatcher;
private SpyWatcher mdWatcher;
private Mojo mojo;

@Before
public void setUp() throws IOException {
FileUtils.forceMkdir(SOURCES);
textWatcher = new SpyWatcher(SOURCES, "txt");
mdWatcher = new SpyWatcher(SOURCES, "md");
Mojo mojo = mock(Mojo.class);
mojo = mock(Mojo.class);
Log log = mock(Log.class);
when(mojo.getLog()).thenReturn(log);
pipeline = new Pipeline(mojo, FAKE, Arrays.asList(textWatcher, mdWatcher));
Expand Down Expand Up @@ -112,6 +113,28 @@ public void testAdditionUpdateAndDeleteOfSubFiles() throws IOException {
assertThat(mdWatcher.deleted).containsExactly("touch.md");
}

@Test
public void testWatchingException() throws IOException {
pipeline.shutdown();

BadWatcher bad = new BadWatcher(SOURCES, "md");
pipeline = new Pipeline(mojo, FAKE, Arrays.asList(bad));
pipeline.watch();

File dir = new File(SOURCES, "foo");
dir.mkdirs();
File md = new File(SOURCES, "foo/touch.md");
md.createNewFile();
assertThat(md.isFile()).isTrue();
waitPullPeriod();

// Check that the error file was created.
File error = new File(FAKE, "target/pipeline/error.json");
assertThat(error).exists();
assertThat(FileUtils.readFileToString(error)).contains("10").contains("11").contains("touch.md").contains
("bad");
}

private void waitPullPeriod() {
try {
Thread.sleep(2500);
Expand Down Expand Up @@ -156,4 +179,21 @@ public boolean fileDeleted(File file) throws WatchingException {
return true;
}
}

private class BadWatcher extends SpyWatcher {

public BadWatcher(File root, String extension) {
super(root, extension);
}

@Override
public boolean fileCreated(File file) throws WatchingException {
throw new WatchingException("Bad bad bad", file, 10, 11, null);
}

@Override
public boolean fileUpdated(File file) throws WatchingException {
throw new WatchingException("Bad bad bad", file, 10, 11, null);
}
}
}

0 comments on commit 8819a0a

Please sign in to comment.