/******************************************************************************* * Copyright (c) 2011 GigaSpaces Technologies Ltd. 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.cloudifysource.dsl.internal; import groovy.lang.Binding; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyShell; import groovy.lang.GroovySystem; import groovy.lang.MissingMethodException; import groovy.lang.MissingPropertyException; import groovy.lang.Script; import groovy.util.ConfigObject; import groovy.util.ConfigSlurper; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.io.SequenceInputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Logger; import org.cloudifysource.domain.Service; import org.cloudifysource.domain.cloud.FileTransferModes; import org.cloudifysource.domain.cloud.RemoteExecutionModes; import org.cloudifysource.domain.cloud.ScriptLanguages; import org.cloudifysource.domain.context.BaseServiceContext; import org.cloudifysource.domain.context.ServiceContext; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; /******* * Generic Cloudify DSL Reader. * * @author barakme * */ public class DSLReader { public DSLReader() { } // Groovy DSL prefix, used for handling print and println correctly private static final String GROOVY_SERVICE_PREFIX = "Object.metaClass.println = {x->this.println(x)}; Object.metaClass.print = {x->this.print(x)};"; /***** * Name of the logger used to process dsl print/println statements. */ public static final String DSL_LOGGER_NAME = "dslLogger"; private static Logger logger = Logger.getLogger(DSLReader.class.getName()); private static Logger dslLogger = Logger.getLogger(DSL_LOGGER_NAME); private boolean loadUsmLib = true; private ServiceContext context; private String propertiesFileName; private boolean isRunningInGSC; private File dslFile; private File workDir; private String dslName; private String dslFileNamePrefix; private String dslFileNameSuffix; private File propertiesFile; private File overridesFile; private boolean createServiceContext = true; private final Map<String, Object> bindingProperties = new HashMap<String, Object>(); private final Map<String, Object> overrideProperties = new HashMap<String, Object>(); private final Map<String, Object> overrideFields = new HashMap<String, Object>(); private Map<String, Object> applicationProperties; private String dslContents; private boolean validateObjects = true; private String overridesScript = null; /******* * Variables injected into the context of a groovy compilation (binding) Used with the service extension mechanism * to pass defined properties, and the context, to the compilation of the parent service. */ private Map<Object, Object> variables; private GroovyClassLoader dslClassLoader; private final Object dslSingleton = new Object(); private static final String[] STAR_IMPORTS = new String[] { org.cloudifysource.domain.Service.class.getPackage().getName(), FileTransferModes.class.getName(), RemoteExecutionModes.class.getName(), ScriptLanguages.class.getName() }; private static final String[] CLASS_IMPORTS = new String[] { org.cloudifysource.dsl.utils.ServiceUtils.class.getName(), FileTransferModes.class.getName(), RemoteExecutionModes.class.getName(), ScriptLanguages.class.getName() // , // "org.cloudifysource.debug.DebugHook" }; /****** * Property name of injected dsl file path. */ public static final String DSL_FILE_PATH_PROPERTY_NAME = "dslFilePath"; /******* * Property name of injected validation activation flag. */ public static final String DSL_VALIDATE_OBJECTS_PROPERTY_NAME = "validateObjectsFlag"; private void initDslFile() throws FileNotFoundException { if (dslFile != null) { if (workDir == null) { workDir = dslFile.getParentFile(); } return; } if (dslContents != null) { return; } if (workDir == null) { throw new IllegalArgumentException("both dslFile and workDir are null"); } if (this.dslFileNameSuffix == null) { throw new IllegalArgumentException("dslFileName suffix has not been set"); } if (!workDir.exists()) { throw new FileNotFoundException("Cannot find " + workDir.getAbsolutePath()); } if (!workDir.isDirectory()) { throw new IllegalArgumentException(workDir.getAbsolutePath() + " must be a directory"); } dslFile = findDefaultDSLFile(dslFileNameSuffix, workDir); if (workDir == null) { workDir = dslFile.getParentFile(); } } /*********** * Search the directory for a file with the specified suffix. Assuming there is exactly one file with that suffix in * the directory. * * @param fileNameSuffix * The suffix. * @param dir * The directory. * @return the file. */ public static File findDefaultDSLFile(final String fileNameSuffix, final File dir) { final File[] files = findDefaultDSLFiles(fileNameSuffix, dir); if (files == null || files.length == 0) { throw new IllegalArgumentException("Cannot find configuration file in " + dir.getAbsolutePath() + "/*" + fileNameSuffix); } if (files.length > 1) { throw new IllegalArgumentException("Found multiple configuration files: " + Arrays.toString(files) + ". " + "Only one may be supplied in the folder."); } return files[0]; } /*********** * Search the directory for files with the specified suffix. * * @param fileNameSuffix * The suffix. * @param directory * The directory to search at. * @return The found files. Returns null if no file with the specified suffix was found. */ public static File[] findDefaultDSLFiles(final String fileNameSuffix, final File directory) { if (!directory.isDirectory()) { throw new IllegalArgumentException(directory.getAbsolutePath() + " is not a directory."); } final File[] files = directory.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return name.endsWith(fileNameSuffix); } }); if (files.length == 0) { return null; } return files; } /** * * @param fileNameSuffix * . * @param dir * . * @return The found file or null. */ public static File findDefaultDSLFileIfExists( final String fileNameSuffix, final File dir) { File found = null; try { found = findDefaultDSLFile(fileNameSuffix, dir); } catch (final IllegalArgumentException e) { if (e.getMessage().contains("Found multiple configuration files")) { throw e; } } return found; } private void init() throws IOException { initDslFile(); setDslName(); initPropertiesFile(); initOverridesFile(); } private void initOverridesFile() throws IOException { overridesFile = getFileIfExist(overridesFile, dslFileNamePrefix + DSLUtils.OVERRIDES_FILE_SUFFIX); } private static void createDSLOverrides(final File file, final String script, final Map<String, Object> overridesMap) throws IOException { if (file == null && script == null) { return; } if (file != null) { try { final ConfigObject parse = new ConfigSlurper().parse(file.toURI().toURL()); parse.flatten(overridesMap); } catch (final Exception e) { throw new IOException("Failed to read overrides file: " + file, e); } } if (script != null) { final ConfigObject parse = new ConfigSlurper().parse(script); parse.flatten(overridesMap); } } private Map<String, Object> createApplicationProperties() throws IOException { final File externalPropertiesFile = getFileIfExist(null, DSLUtils.APPLICATION_PROPERTIES_FILE_NAME); final Map<String, Object> externalProperties = new HashMap<String, Object>(); createDSLOverrides(externalPropertiesFile, null, externalProperties); final File externalOverridesFile = getFileIfExist(null, DSLUtils.APPLICATION_OVERRIDES_FILE_NAME); final Map<String, Object> externalOverrides = new HashMap<String, Object>(); createDSLOverrides(externalOverridesFile, null, externalOverrides); if (externalOverrides != null) { for (final Entry<String, Object> entry : externalOverrides.entrySet()) { externalProperties.put(entry.getKey(), entry.getValue()); } } return externalProperties; } /********* * Executes the current DSL reader, returning the required Object type. * * @param clazz * the expected class type returned from the DSL file. * @param <T> * The Class type returned from this type of DSL file. * @return the domain POJO. * @throws DSLException * in case there was a problem processing the DSL file. */ public <T> T readDslEntity(final Class<T> clazz) throws DSLException { final Object result = readDslObject(); if (result == null) { throw new IllegalStateException("The file " + dslFile + " evaluates to null, not to a DSL object"); } if (!clazz.isAssignableFrom(result.getClass())) { throw new IllegalStateException("The file: " + dslFile + " did not evaluate to the required object type"); } @SuppressWarnings("unchecked") final T resultObject = (T) result; return resultObject; } private Object readDslObject() throws DSLException { try { init(); } catch (final IOException e) { throw new DSLException("Failed to initialize DSL Reader: " + e.getMessage(), e); } LinkedHashMap<Object, Object> properties = null; try { properties = createDSLProperties(); createDSLOverrides(overridesFile, overridesScript, overrideProperties); overrideProperties(properties); addApplicationProperties(properties); } catch (final Exception e) { // catching exception here, as groovy config slurper may throw just // about anything String msg = null; if (propertiesFile != null) { msg = e.getMessage(); } else { msg = "Failed to load properties file: " + e.getMessage(); } throw new IllegalArgumentException(msg, e); } if (this.variables != null) { properties.putAll(this.variables); } // create an uninitialized service context if (this.createServiceContext) { String canonicalPath = null; try { canonicalPath = workDir.getCanonicalPath(); } catch (final IOException e) { throw new DSLException("Failed to get canonical path of work directory: " + workDir + ". Error was: " + e.getMessage(), e); } if (this.context == null) { this.context = new BaseServiceContext(canonicalPath); } } // create the groovy shell, loaded with our settings final GroovyShell gs = createGroovyShell(properties); final Object result = evaluateGroovyScript(gs); if (result == null) { throw new DSLException("The DSL evaluated to a null - check your syntax and try again"); } if (this.createServiceContext) { if (!(result instanceof Service)) { throw new IllegalArgumentException( "The DSL reader cannot create a service context to a DSL that does not evaluate to a Service. " + "Set the 'createServiceContext' option to false if you do not need a service conext"); } ((BaseServiceContext) this.context).init((Service) result); } this.dslClassLoader = gs.getClassLoader(); // The call below is required to clear cached class entries. Without it, a PermGen error will eventually occur. // A synchronized block may be required as this call MAY not be thread safe. // More info available here: http://jira.codehaus.org/browse/GROOVY-5121 synchronized (dslSingleton) { // Tell Groovy we don't need any meta // information about the generated DSL classes GroovySystem.getMetaClassRegistry().removeMetaClass(Object.class); // Tell the loader to clear out it's cache, // this ensures the classes will be GC'd gs.resetLoadedClasses(); } return result; } /** * * @param properties * the properties to add to * @throws IOException */ private void addApplicationProperties(final Map<Object, Object> properties) throws IOException { if (applicationProperties == null) { applicationProperties = createApplicationProperties(); } for (final Entry<String, Object> entry : applicationProperties.entrySet()) { properties.put(entry.getKey(), entry.getValue()); } } /** * * @param properties * the properties to override */ private void overrideProperties(final LinkedHashMap<Object, Object> properties) { for (final Entry<String, Object> entry : overrideProperties.entrySet()) { final String key = entry.getKey(); final Object propertyValue = entry.getValue(); // overrides existing property or add a new one. properties.put(key, propertyValue); } } @SuppressWarnings("deprecation") private Object evaluateGroovyScript(final GroovyShell gs) throws DSLValidationException { // Evaluate class using a FileReader, as the *-service files create a // class with an illegal name Object result = null; if (this.dslContents == null) { // FileReader reader = null; SequenceInputStream sis = null; FileInputStream fis = null; try { fis = new FileInputStream(dslFile); final ByteArrayInputStream bis = new ByteArrayInputStream(GROOVY_SERVICE_PREFIX.getBytes()); sis = new SequenceInputStream(bis, fis); // reader = new FileReader(dslFile); // using a deprecated method here as we do not have a multireader in the dependencies // and not really worth another jar just for this. result = gs.evaluate(sis, "dslEntity"); } catch (final IOException e) { throw new IllegalStateException("The file " + dslFile + " could not be read", e); } catch (final MissingMethodException e) { throw new IllegalArgumentException("Could not resolve DSL entry with name: " + e.getMethod(), e); } catch (final MissingPropertyException e) { throw new IllegalArgumentException("Could not resolve DSL entry with name: " + e.getProperty(), e); } catch (final DSLValidationRuntimeException e) { throw e.getDSLValidationException(); } catch (final CompilationFailedException e) { throw new IllegalArgumentException("Could not parse " + dslFile + ": " + e.getMessage(), e); } finally { if (sis != null) { try { sis.close(); } catch (final IOException e) { // ignore } } if (fis != null) { try { fis.close(); } catch (final IOException e) { // ignore } } } } else { try { result = gs.evaluate(this.dslContents, "dslEntity"); } catch (final CompilationFailedException e) { throw new IllegalArgumentException("The file " + dslFile + " could not be compiled", e); } } return result; } private void initPropertiesFile() throws IOException { if (this.propertiesFileName != null) { this.propertiesFile = new File(workDir, this.propertiesFileName); if (!propertiesFile.exists()) { throw new FileNotFoundException("Could not find properties file: " + propertiesFileName); } if (!propertiesFile.isFile()) { throw new FileNotFoundException(propertiesFileName + " is not a file!"); } return; } if (this.dslFile == null) { return; } // look for default properties file // using format <dsl file name>.properties final String defaultPropertiesFileName = dslFileNamePrefix + DSLUtils.PROPERTIES_FILE_SUFFIX; final File defaultPropertiesFile = new File(workDir, defaultPropertiesFileName); if (defaultPropertiesFile.exists()) { this.propertiesFileName = defaultPropertiesFileName; this.propertiesFile = defaultPropertiesFile; } } private File getFileIfExist(final File file, final String defaultFileName) throws IOException { if (file != null) { if (!file.exists()) { throw new FileNotFoundException("Could not find overrides file: " + file.getAbsolutePath()); } if (!file.isFile()) { throw new FileNotFoundException(this.overridesFile.getName() + " is not a file!"); } return file; } if (this.dslFile == null) { return null; } // look for default properties file // using format <dsl file name>.suffix final File defaultOverridesFile = new File(workDir, defaultFileName); if (defaultOverridesFile.exists()) { return defaultOverridesFile; } return null; } private void setDslName() { if (dslFile == null) { return; } final String baseFileName = dslFile.getName(); final int indexOfLastComma = baseFileName.lastIndexOf('.'); if (indexOfLastComma < 0) { dslName = baseFileName; } else { dslName = baseFileName.substring(0, indexOfLastComma); } dslFileNamePrefix = dslName; final int indexOfHyphen = dslName.indexOf('-'); if (indexOfHyphen >= 0) { dslName = dslName.substring(0, indexOfHyphen); } } @SuppressWarnings("unchecked") private LinkedHashMap<Object, Object> createDSLProperties() throws IOException { if (this.propertiesFile == null) { return new LinkedHashMap<Object, Object>(); } try { GroovyClassLoader gcl = new GroovyClassLoader(); Script script = (Script) gcl.parseClass(propertiesFile).newInstance(); ConfigObject config = new ConfigSlurper().parse(script); GroovySystem.getMetaClassRegistry().removeMetaClass(script.getClass()); return config; } catch (final Exception e) { throw new IOException("Failed to read properties file: " + propertiesFile + ": " + e.getMessage(), e); } } private GroovyShell createGroovyShell(final LinkedHashMap<Object, Object> properties) { final String baseClassName = BaseDslScript.class.getName(); final List<String> serviceJarFiles = createJarFileListForService(); String classpathDir = null; if (this.getWorkDir() != null) { classpathDir = this.getWorkDir().getAbsolutePath(); } else if (this.getDslFile() != null) { if (this.getDslFile().getParentFile() != null) { classpathDir = this.getDslFile().getParentFile().getAbsolutePath(); } } if (classpathDir != null) { serviceJarFiles.add(classpathDir); } final CompilerConfiguration cc = createCompilerConfiguration(baseClassName, serviceJarFiles); final Binding binding = createGroovyBinding(properties); final GroovyShell gs = new GroovyShell(ServiceReader.class.getClassLoader(), binding, cc); return gs; } private static CompilerConfiguration createCompilerConfiguration(final String baseClassName, final List<String> extraJarFileNames) { final CompilerConfiguration cc = new CompilerConfiguration(); final ImportCustomizer ic = new ImportCustomizer(); ic.addStarImports(STAR_IMPORTS); ic.addImports(CLASS_IMPORTS); ic.addStaticImport("Statistics", org.cloudifysource.domain.statistics.AbstractStatisticsDetails.class.getName(), "STATISTICS_FACTORY"); cc.addCompilationCustomizers(ic); // cc.addCompilationCustomizers(ic, new ASTTransformationCustomizer(new DebugHookTransformar())); cc.setScriptBaseClass(baseClassName); cc.setClasspathList(extraJarFileNames); return cc; } private Binding createGroovyBinding(final LinkedHashMap<Object, Object> properties) { final Binding binding = new Binding(); final Set<Entry<String, Object>> bindingPropertiesEntries = this.bindingProperties.entrySet(); for (final Entry<String, Object> entry : bindingPropertiesEntries) { binding.setVariable(entry.getKey(), entry.getValue()); } if (properties != null) { final Set<Entry<Object, Object>> entries = properties.entrySet(); for (final Entry<Object, Object> entry : entries) { binding.setVariable((String) entry.getKey(), entry.getValue()); } // add variable that contains all the properties // to distinguish between properties and other binding variables. // This will be used in loading application's service process // to transfer application properties to the service using the // application's binding. binding.setVariable(DSLUtils.DSL_PROPERTIES, properties); if (context != null) { binding.setVariable("context", context); } } binding.setVariable(DSLUtils.DSL_VALIDATE_OBJECTS_PROPERTY_NAME, validateObjects); binding.setVariable(DSLUtils.DSL_FILE_PATH_PROPERTY_NAME, dslFile == null ? null : dslFile.getPath()); binding.setVariable(DSLReader.DSL_LOGGER_NAME, dslLogger); // MethodClosure printlnClosure = new MethodClosure(this, "println"); // binding.setVariable("println", printlnClosure); return binding; } private List<String> createJarFileListForService() { logger.fine("Adding jar files to service compile path"); if (!this.isLoadUsmLib()) { logger.fine("Ignoring usmlib - external jar files will not be added to classpath!"); // when running in GSC, the usmlib jars are placed in the PU lib dir // automatically return new ArrayList<String>(0); } if (dslFile == null) { logger.fine("DSL file location not specified. Skipping usmlib jar loading!"); return new ArrayList<String>(0); } final File serviceDir = dslFile.getParentFile(); final File usmLibDir = new File(serviceDir, CloudifyConstants.USM_LIB_DIR); if (!usmLibDir.exists()) { logger.fine("No usmlib dir was found at: " + usmLibDir + " - no jars will be added to the classpath!"); return new ArrayList<String>(0); } if (usmLibDir.isFile()) { throw new IllegalArgumentException("The service includes a file called: " + CloudifyConstants.USM_LIB_DIR + ". This name may only be used for a directory containing service jar files"); } final File[] libFiles = usmLibDir.listFiles(); final List<String> result = new ArrayList<String>(libFiles.length); for (final File file : libFiles) { if (file.isFile() && file.getName().endsWith(".jar")) { result.add(file.getAbsolutePath()); } } logger.fine("Extra jar files list: " + result); return result; } // ////////////// // Accessors /// // ////////////// public ServiceContext getContext() { return context; } public void setContext(final ServiceContext context) { this.context = context; } public String getPropertiesFileName() { return propertiesFileName; } public void setPropertiesFileName(final String propertiesFileName) { this.propertiesFileName = propertiesFileName; } public boolean isRunningInGSC() { return isRunningInGSC; } public void setRunningInGSC(final boolean isRunningInGSC) { this.isRunningInGSC = isRunningInGSC; } public File getDslFile() { return dslFile; } public void setDslFile(final File dslFile) { this.dslFile = dslFile; } public File getWorkDir() { return workDir; } public void setWorkDir(final File workDir) { this.workDir = workDir; } public boolean isCreateServiceContext() { return createServiceContext; } public ClassLoader getDSLClassLoader() { return this.dslClassLoader; } public void setCreateServiceContext(final boolean createServiceContext) { this.createServiceContext = createServiceContext; } /********** * . * * @param key * . * @param value * . */ public void addProperty(final String key, final Object value) { bindingProperties.put(key, value); } public void setDslContents(final String dslContents) { this.dslContents = dslContents; } public String getDslFileNameSuffix() { return dslFileNameSuffix; } public void setDslFileNameSuffix(final String dslFileNameSuffix) { this.dslFileNameSuffix = dslFileNameSuffix; } public void setOverridesScript(final String script) { this.overridesScript = script; } public String getDslName() { return dslName; } public void setDslName(final String dslName) { this.dslName = dslName; } public boolean isLoadUsmLib() { return loadUsmLib; } public void setLoadUsmLib(final boolean loadUsmLib) { this.loadUsmLib = loadUsmLib; } public void setBindingVariables(final Map<Object, Object> variables) { this.variables = variables; } public boolean isValidateObjects() { return validateObjects; } public void setValidateObjects(final boolean isValidateObjects) { this.validateObjects = isValidateObjects; } public File getPropertiesFile() { return this.propertiesFile; } public File getOverridesFile() { return this.overridesFile; } public void setOverridesFile(final File overridesFile) { this.overridesFile = overridesFile; } public Map<String, Object> getOverrides() { return this.overrideProperties; } public Map<String, Object> getOverrideFields() { return this.overrideFields; } public Map<String, Object> getApplicationProperties() { return applicationProperties; } public void setApplicationProperties(final Map<String, Object> applicationProperties) { this.applicationProperties = applicationProperties; } }