/*
documentr - Edit, maintain, and present software documentation on the web.
Copyright (C) 2012-2013 Maik Schreiber
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.blizzy.documentr.markdown.macro;
import groovy.lang.GroovyClassLoader;
import groovy.lang.Script;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.control.messages.Message;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import de.blizzy.documentr.DocumentrConstants;
import de.blizzy.documentr.Settings;
@Component
@Slf4j
class GroovyMacroScanner {
private static final class GroovyFileFilterImplementation implements FileFilter {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().endsWith(".groovy"); //$NON-NLS-1$
}
}
static final String MACROS_DIR_NAME = "macros"; //$NON-NLS-1$
@Autowired
private Settings settings;
@Autowired
private BeanFactory beanFactory;
private File macrosDir;
@PostConstruct
public void init() throws IOException {
macrosDir = new File(settings.getDocumentrDataDir(), MACROS_DIR_NAME);
if (!macrosDir.exists()) {
FileUtils.forceMkdir(macrosDir);
}
}
Set<IMacro> findGroovyMacros() {
log.info("registering macros from Groovy scripts in folder {}", macrosDir.getAbsolutePath()); //$NON-NLS-1$
GroovyClassLoader classLoader = getGroovyClassLoader();
Set<IMacro> macros = Sets.newHashSet();
for (File file : findGroovyMacroFiles()) {
IMacro macro = getMacro(file, classLoader);
if (macro != null) {
macros.add(macro);
}
}
return macros;
}
private Set<File> findGroovyMacroFiles() {
Set<File> result = Sets.newHashSet();
for (File file : macrosDir.listFiles(new GroovyFileFilterImplementation())) {
result.add(file);
}
return result;
}
private GroovyClassLoader getGroovyClassLoader() {
ImportCustomizer importCustomizer = new ImportCustomizer();
importCustomizer.addStarImports(DocumentrConstants.GROOVY_DEFAULT_IMPORTS.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
compilerConfiguration.addCompilationCustomizers(importCustomizer);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
GroovyClassLoader classLoader = new GroovyClassLoader(contextClassLoader, compilerConfiguration);
return classLoader;
}
private IMacro getMacro(File file, GroovyClassLoader classLoader) {
IMacro macro = null;
try {
Class<?> clazz = classLoader.parseClass(file);
if (IMacro.class.isAssignableFrom(clazz)) {
macro = (IMacro) clazz.newInstance();
} else {
Macro annotation = clazz.getAnnotation(Macro.class);
if (annotation != null) {
if (ISimpleMacro.class.isAssignableFrom(clazz)) {
ISimpleMacro simpleMacro = (ISimpleMacro) clazz.newInstance();
macro = new SimpleMacroMacro(simpleMacro, annotation, beanFactory);
} else if (IMacroRunnable.class.isAssignableFrom(clazz)) {
@SuppressWarnings("unchecked")
Class<? extends IMacroRunnable> c = (Class<? extends IMacroRunnable>) clazz;
macro = new MacroRunnableMacro(c, annotation, beanFactory);
} else {
log.warn("class {} not supported: {}", clazz.getName(), file.getName()); //$NON-NLS-1$
}
} else {
log.warn("class {} not supported, no @Macro annotation found: {}", clazz.getName(), file.getName()); //$NON-NLS-1$
}
}
} catch (IOException e) {
log.warn("error loading Groovy macro: " + file.getName(), e); //$NON-NLS-1$
} catch (InstantiationException e) {
log.warn("error loading Groovy macro: " + file.getName(), e); //$NON-NLS-1$
} catch (IllegalAccessException e) {
log.warn("error loading Groovy macro: " + file.getName(), e); //$NON-NLS-1$
} catch (RuntimeException e) {
log.warn("error loading Groovy macro: " + file.getName(), e); //$NON-NLS-1$
}
return macro;
}
List<String> listMacros() {
List<File> files = Lists.newArrayList(findGroovyMacroFiles());
Function<File, String> function = new Function<File, String>() {
@Override
public String apply(File file) {
return StringUtils.substringBeforeLast(file.getName(), ".groovy"); //$NON-NLS-1$
}
};
List<String> result = Lists.newArrayList(Lists.transform(files, function));
Collections.sort(result, new Comparator<String>() {
@Override
public int compare(String name1, String name2) {
return name1.compareToIgnoreCase(name2);
}
});
return result;
}
String getMacroCode(String name) throws IOException {
File file = new File(macrosDir, name + ".groovy"); //$NON-NLS-1$
return FileUtils.readFileToString(file, Charsets.UTF_8);
}
List<CompilationMessage> verifyMacro(String code) {
List<CompilationMessage> result = Lists.newArrayList();
if (StringUtils.isNotBlank(code)) {
GroovyClassLoader groovyClassLoader = getGroovyClassLoader();
try {
Class<?> clazz = groovyClassLoader.parseClass(code);
if (Script.class.isAssignableFrom(clazz)) {
result.add(new CompilationMessage(CompilationMessage.Type.ERROR, 1, 1, 1, 1,
"Script must represent a class implementing IMacro, ISimpleMacro, or " + //$NON-NLS-1$
"IMacroRunnable.")); //$NON-NLS-1$
} else if (!IMacro.class.isAssignableFrom(clazz)) {
Macro annotation = clazz.getAnnotation(Macro.class);
if (annotation != null) {
if (!ISimpleMacro.class.isAssignableFrom(clazz) &&
!IMacroRunnable.class.isAssignableFrom(clazz)) {
result.add(new CompilationMessage(CompilationMessage.Type.ERROR, 1, 1, 1, 1,
"Class " + clazz.getSimpleName() + " must implement IMacro or ISimpleMacro.")); //$NON-NLS-1$ //$NON-NLS-2$
}
} else {
result.add(new CompilationMessage(CompilationMessage.Type.ERROR, 1, 1, 1, 1,
"Class " + clazz.getSimpleName() + " not supported for macros: " + //$NON-NLS-1$ //$NON-NLS-2$
"No @Macro annotation found.")); //$NON-NLS-1$
}
}
} catch (MultipleCompilationErrorsException e) {
@SuppressWarnings("unchecked")
List<Message> errors = e.getErrorCollector().getErrors();
result.addAll(toCompilationMessages(errors));
} catch (RuntimeException e) {
// ignore
}
}
return result;
}
private List<CompilationMessage> toCompilationMessages(List<Message> errors) {
List<CompilationMessage> messages = Lists.newArrayList();
if (errors != null) {
for (Message error : errors) {
if (error instanceof SyntaxErrorMessage) {
SyntaxErrorMessage syntaxError = (SyntaxErrorMessage) error;
int startLine = syntaxError.getCause().getStartLine();
int startColumn = syntaxError.getCause().getStartColumn();
int endLine = syntaxError.getCause().getEndLine();
int endColumn = syntaxError.getCause().getEndColumn();
String message = syntaxError.getCause().getMessage();
messages.add(new CompilationMessage(CompilationMessage.Type.ERROR,
startLine, startColumn, endLine, endColumn, message));
}
}
}
return messages;
}
public void saveMacro(String name, String code) throws IOException {
File file = new File(macrosDir, name + ".groovy"); //$NON-NLS-1$
FileUtils.writeStringToFile(file, code, Charsets.UTF_8);
}
public void deleteMacro(String name) throws IOException {
File file = new File(macrosDir, name + ".groovy"); //$NON-NLS-1$
FileUtils.forceDelete(file);
}
}