Skip to content

Commit

Permalink
Fixed dynamic groovy scripts reload
Browse files Browse the repository at this point in the history
  • Loading branch information
Volodymyr Bodnar committed Aug 11, 2019
1 parent 885c1e1 commit 2f98d8e
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 69 deletions.
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Features**
* Application has two modes GUI/command line
* Allows communication with clients using OCPP protocol (Supports SSL/TLS)
* Allows dynamic change of OCPP server behavior
* Exposes REST API to control OCPP communication

**Requirements:**
Expand All @@ -20,6 +21,8 @@
**Download**
```
git clone https://github.com/v-bodnar/ocpp-server.git
git clone https://github.com/v-bodnar/GroovyOcppSupplier.git $GROOVY_PATH //@see section "Changing server behavior
using Groovy"
```

**Build:**
Expand Down Expand Up @@ -52,15 +55,23 @@ java -jar ocpp-server-0.1.jar <arguments>

## Changing server behavior using Groovy
**$GROOVY_PATH = $LITHOS_HOME/ocpp/groovy/**
Groovy files will be created under path: **$GROOVY_PATH**
By creating groovy classes inside **$GROOVY_PATH** that implement:
After the first start of ocpp-server app you will get exception:
```
xx:xx:xx.xxx [JavaFX Application Thread] ERROR com.omb.ocpp.groovy.GroovyService - Could not load groovy scripts
java.io.FileNotFoundException: Please execute "git clone https://github.com/v-bodnar/GroovyOcppSupplier.git
$GROOVY_PATH"
```
So just execute the command above. It will clone new gradle project into the **$GROOVY_PATH** folder.
After it is done restart the application or click "Reload groovy scripts" on the "Groovy" tab in GUI.

From this point in time you can make changes to the cloned gradle project and dynamically reload scripts using
"Reload groovy scripts" button on the "Groovy" tab in GUI or using REST api.

By implementing or changing classes that implement:
```
ConfirmationSupplier<REQUEST extends Request, RESPONSE extends Confirmation>
```
you can change responses that ocpp server sends to clients. Using GUI reload those classes on runtime.

Also you can upload .groovy files using REST API, it will replace files with the same names and automatically load
classes to classloader
you can change responses that ocpp server sends to clients dynamically on runtime.

## Secure connection using ssl
**SSL_PATH = $LITHOS_HOME/ocpp/ssl**
Expand Down
71 changes: 71 additions & 0 deletions src/main/java/com/omb/ocpp/groovy/BatchGroovyClassLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.omb.ocpp.groovy;

import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.ErrorCollector;
import org.codehaus.groovy.control.Phases;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.tools.GroovyClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.file.Path;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

public class BatchGroovyClassLoader extends GroovyClassLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(BatchGroovyClassLoader.class);
private final CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
private final ErrorCollector errorCollector = new ErrorCollector(compilerConfiguration);

public List<Class> parseClasses(List<Path> groovyFiles) {
if (groovyFiles == null || groovyFiles.isEmpty()) {
return new LinkedList<>();
} else {
return compileGroovyFiles(groovyFiles);
}
}

private List<Class> compileGroovyFiles(List<Path> groovyFiles) {
List<SourceUnit> sourceUnits = getGroovySources(groovyFiles);
if (sourceUnits.isEmpty()) {
LOGGER.error("No groovy sources found for compilation");
return new LinkedList<>();
}

InnerLoader innerLoader = new InnerLoader(this);
CompilationUnit compilationUnit = new CompilationUnit(innerLoader);
compilationUnit.setConfiguration(compilerConfiguration);
sourceUnits.forEach(compilationUnit::addSource);

try {
compilationUnit.compile(Phases.CLASS_GENERATION);
} catch (CompilationFailedException e) {
LOGGER.error("Failed to compile groovy files", e);
return new LinkedList<>();
}

List<Class> classes = (List<Class>) compilationUnit.getClasses().stream()
.map(groovyClass -> defineClass((GroovyClass) groovyClass))
.collect(Collectors.toList());
return classes;
}

private Class defineClass(GroovyClass groovyClass) {
Class clazz = this.defineClass(groovyClass.getName(), groovyClass.getBytes());
setClassCacheEntry(clazz);
return clazz;
}

private List<SourceUnit> getGroovySources(List<Path> groovyFiles) {
return groovyFiles.stream()
.map(path -> {
LOGGER.debug("Added file for compilation {} ", path);
return new SourceUnit(path.toFile(), compilerConfiguration, this, errorCollector);
})
.collect(Collectors.toList());
}
}
74 changes: 11 additions & 63 deletions src/main/java/com/omb/ocpp/groovy/GroovyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

import eu.chargetime.ocpp.model.Confirmation;
import eu.chargetime.ocpp.model.Request;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyRuntimeException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.tools.GroovyClass;
import org.jvnet.hk2.annotations.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -15,7 +10,6 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.nio.channels.Channels;
Expand All @@ -33,6 +27,7 @@
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.omb.ocpp.gui.Application.LITHOS_HOME;
Expand All @@ -48,13 +43,11 @@ public class GroovyService {
new HashMap<>();
private Consumer<Void> groovyCacheChangedListener = aVoid -> LOGGER.debug("No listeners attached");

private GroovyClassLoader groovyClassLoader = new GroovyClassLoader();

public void loadGroovyScripts() {
try {
if (!GROOVY_PROJECT_FOLDER.toFile().exists()) {
throw new FileNotFoundException(String.format("Please execute \"git clone https://github" +
".com/v-bodnar/GroovyOcppSupplier.git %s \"", GROOVY_PROJECT_FOLDER));
".com/v-bodnar/GroovyOcppSupplier.git %s\"", GROOVY_PROJECT_FOLDER));
}
reloadGroovyFiles();
} catch (IOException e) {
Expand All @@ -64,10 +57,8 @@ public void loadGroovyScripts() {

public synchronized void reloadGroovyFiles() {
confirmationSuppliers.clear();
groovyClassLoader.clearCache();

//this will compile all groovy files
List<Class> classes = compileGroovyFiles();
List<Class> classes = new BatchGroovyClassLoader().parseClasses(getGroovyFiles());
classes.stream()
.filter(aClass -> aClass.getGenericInterfaces().length != 0
&& aClass.getGenericInterfaces()[0] instanceof ParameterizedType
Expand All @@ -85,61 +76,18 @@ public void uploadGroovyScript(InputStream inputStream, String scriptName) throw
FileChannel dest = new FileOutputStream(destination.toFile()).getChannel()) {
dest.transferFrom(src, 0, Integer.MAX_VALUE);
}

Class uploadedClass = groovyClassLoader.parseClass(destination.toFile());
if (uploadedClass.getGenericInterfaces().length != 0 &&
uploadedClass.getGenericInterfaces()[0] instanceof ParameterizedType
&& ((ParameterizedType) uploadedClass.getGenericInterfaces()[0]).getRawType().equals(ConfirmationSupplier.class)) {
putToCache(uploadedClass);
groovyCacheChangedListener.accept(null);
} else {
throw new InvalidClassException(String.format("Could not load class from file %s, check that class implements " +
"ConfirmationSupplier<REQUEST extends Request, RESPONSE extends Confirmation>", destination));
}
reloadGroovyFiles();
}

private List<Class> compileGroovyFiles() {
List<Class> compiledClasses = new LinkedList<>();
List<SourceUnit> sourceUnits = new LinkedList<>();
CompilationUnit compileUnit = new CompilationUnit(groovyClassLoader);
private List<Path> getGroovyFiles() {
try (Stream<Path> stream = Files.walk(GROOVY_PROJECT_FOLDER)) {
stream.filter(path -> path.toString().endsWith(".groovy") && Files.isRegularFile(path))
.forEach(path -> {
LOGGER.debug("Adding file for compilation: {}", path);
sourceUnits.add(compileUnit.addSource(path.toFile()));
});
} catch (IOException | GroovyRuntimeException e) {
return stream
.filter(path -> path.toString().endsWith(".groovy") && Files.isRegularFile(path))
.collect(Collectors.toList());
} catch (IOException e) {
LOGGER.error(String.format("Can't walk path: %s", GROOVY_PROJECT_FOLDER), e);
return new LinkedList<>();
}
compileUnit.compile();

for (Object compileClass : compileUnit.getClasses()) {
GroovyClass groovyClass = (GroovyClass) compileClass;
byte[] compiledScriptBytes = groovyClass.getBytes();
try {
Class clazz = compileUnit.getClassLoader().loadClass(groovyClass.getName());
throw new FeatureNotSupportedException("Dynamic groovy reload is not supported yet"); //todo
// sourceUnits.stream()
// .filter(sourceUnit1 -> sourceUnit1.getName().endsWith(String.format("%s.groovy",
// clazz.getSimpleName())))
// .findFirst().ifPresentOrElse(sourceUnit1 -> {
// try {
// compiledClasses.add(compileUnit.getClassLoader().parseClass(sourceUnit1.getSource().getReader(),
// sourceUnit1.getSource().getURI().toString()));
// } catch (IOException e) {
// LOGGER.error(String.format("Can't reload class %s", groovyClass.getName()), e);
// }
// },
// () -> LOGGER.error("Can't reload class {}", groovyClass.getName()));

} catch (ClassNotFoundException e) {
compiledClasses.add(compileUnit.getClassLoader().defineClass(groovyClass.getName(), compiledScriptBytes));
} catch (FeatureNotSupportedException e) {
LOGGER.error(e.getMessage());
}
}

return compiledClasses;
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -182,7 +130,7 @@ public void setGroovyCacheChangedListener(Consumer<Void> groovyCacheChangedListe
this.groovyCacheChangedListener = groovyCacheChangedListener;
}

class FeatureNotSupportedException extends Exception{
class FeatureNotSupportedException extends Exception {
public FeatureNotSupportedException(String message) {
super(message);
}
Expand Down

0 comments on commit 2f98d8e

Please sign in to comment.