com.nextep.datadesigner.sqlgen.impl.SQLGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.nextep.datadesigner.sqlgen.impl.SQLGenerator.java

Source

/*******************************************************************************
 * Copyright (c) 2011 neXtep Software and contributors.
 * All rights reserved.
 *
 * This file is part of neXtep designer.
 *
 * NeXtep designer 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 any later version.
 *
 * NeXtep designer 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 Foobar.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Contributors:
 *     neXtep Softwares - initial API and implementation
 *******************************************************************************/
package com.nextep.datadesigner.sqlgen.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;

import com.nextep.datadesigner.dbgm.model.IDatabaseObject;
import com.nextep.datadesigner.exception.ErrorException;
import com.nextep.datadesigner.exception.InconsistentObjectException;
import com.nextep.datadesigner.impl.NameComparator;
import com.nextep.datadesigner.model.IElementType;
import com.nextep.datadesigner.model.IReferenceable;
import com.nextep.datadesigner.model.ITypedObject;
import com.nextep.datadesigner.sqlgen.model.DatabaseReference;
import com.nextep.datadesigner.sqlgen.model.ISQLGenerator;
import com.nextep.datadesigner.sqlgen.model.ISQLScript;
import com.nextep.datadesigner.sqlgen.model.ScriptType;
import com.nextep.datadesigner.vcs.impl.ComparisonAttribute;
import com.nextep.datadesigner.vcs.impl.Merger;
import com.nextep.datadesigner.vcs.services.MergeUtils;
import com.nextep.datadesigner.vcs.services.VersionHelper;
import com.nextep.designer.core.CorePlugin;
import com.nextep.designer.core.helpers.NameHelper;
import com.nextep.designer.core.model.DBVendor;
import com.nextep.designer.core.model.ICheckedObject;
import com.nextep.designer.core.model.ITypedObjectFactory;
import com.nextep.designer.sqlgen.SQLGenPlugin;
import com.nextep.designer.sqlgen.model.IGenerationResult;
import com.nextep.designer.sqlgen.model.ISQLCommandWriter;
import com.nextep.designer.sqlgen.model.ISQLParser;
import com.nextep.designer.sqlgen.model.impl.GenerationResult;
import com.nextep.designer.sqlgen.services.IGenerationService;
import com.nextep.designer.vcs.model.ComparedElement;
import com.nextep.designer.vcs.model.ComparisonScope;
import com.nextep.designer.vcs.model.DifferenceType;
import com.nextep.designer.vcs.model.IComparisonItem;
import com.nextep.designer.vcs.model.IVersionContainer;
import com.nextep.designer.vcs.model.IVersionable;

/**
 * Abstract class for generators which provides default generation behaviour for
 * incremental generation. It handles simple incremental scenarios resolution
 * and delegates the only DIFFER comparison case to an abstract dedicated
 * method.
 * 
 * @author Christophe Fondacci
 * @author Bruno Gautier
 */
public abstract class SQLGenerator implements ISQLGenerator {

    private static final Log LOGGER = LogFactory.getLog(SQLGenerator.class);
    protected static final String INDENTATION = "          "; //$NON-NLS-1$
    // private static final int MAX_LOOPS = 2000;

    private DBVendor vendor;
    private ISQLCommandWriter sqlCommandWriter;
    public String NEWLINE = "\r\n"; //$NON-NLS-1$

    public SQLGenerator() {
        NEWLINE = CorePlugin.getService(IGenerationService.class).getNewLine();
    }

    @Override
    public final IGenerationResult generateIncrementalSQL(IComparisonItem result) {
        if (result == null)
            return null;
        IReferenceable r = result.getMergeInfo().getMergeProposal();

        // If all sub elements of the target are proposals, we got a target
        // proposal
        if (result.getDifferenceType() != DifferenceType.EQUALS
                && result.getScope() != ComparisonScope.REPOSITORY /*
                                                                   * &&
                                                                   * r==null
                                                                   * /*result
                                                                   * .
                                                                   * getTarget
                                                                   * ()
                                                                   */
                && result.getSource() != null) {
            if (MergeUtils.isSelected(result, ComparedElement.TARGET)) {
                r = result.getTarget();
            }
        }
        switch (result.getDifferenceType()) {
        case MISSING_TARGET:
            // We generate only if source is selected
            if (r == result.getSource()) {
                try {
                    checkConsistency(result.getSource());
                } catch (ErrorException e) {
                    // Error occurred while generating
                    LOGGER.error("Skipping generation of inconsistent element : " + e.getMessage(), e);
                    return null;
                }
                try {
                    return generateFullSQL(result.getSource());
                } catch (RuntimeException e) {
                    // Error occurred while generating
                    LOGGER.error("Generator generated runtime exception : " + e.getMessage(), e);
                    return null;
                }
            } else {
                return null;
            }
        case DIFFER:
            // TODO check if OK to replace comparison source
            // Checking proposal...
            if (result.getTarget() == r && result.getScope() != ComparisonScope.REPOSITORY) { // &&
                // (result.getScope()==ComparisonScope.DATABASE))
                // {
                // //
                // ||
                // result.getScope()==ComparisonScope.ALL))
                // {
                return null;
            }
            // else if(r!=result.getSource()) {
            // // Getting merger
            // IMerger m = MergerFactory.getMerger(result.getType(),
            // ComparisonScope.DATABASE);
            // if(m!=null) {
            // try {
            // Object mergeResult = m.buildMergedObject(result, null);
            // result.setSource((IReferenceable)mergeResult);
            // } catch( Exception e ) {
            // log.warn("Skipping user-defined merge of " +
            // result.getType().getName() + " '" +
            // ((INamedObject)result.getSource()).getName() +
            // "', attempting to generate from repository...");
            // }
            // }
            // }
            checkConsistency(result.getSource());
            try {
                return generateDiff(result);
            } catch (RuntimeException e) {
                // Error occurred while generating
                LOGGER.error("Generator generated runtime exception : " + e.getMessage(), e);
                return null;
            }
        case MISSING_SOURCE:
            // We drop only if selected
            if (r == result.getSource()) {
                try {
                    return generateDrop(result.getTarget());
                } catch (RuntimeException e) {
                    // Error occurred while generating
                    LOGGER.error("Generator generated runtime exception : " + e.getMessage(), e);
                    return null;
                }
            } else {
                return null;
            }
        case EQUALS:
        default:
            return null;
        }
    }

    /**
     * Method which generates the SQLScript to upgrade a target element which
     * differ from the source element. This abstract class assures implementors
     * that a difference has been found and must be generated.
     * 
     * @param result
     *            the comparison information containing merge data
     * @return the SQL script that upgrades the target element to the source
     *         element
     */
    public abstract IGenerationResult generateDiff(IComparisonItem result);

    @Override
    public IGenerationResult generateDrop(Object model) {
        final IGenerationService generationService = SQLGenPlugin.getService(IGenerationService.class);
        return generationService.getDropStrategy(((ITypedObject) model).getType(), getVendor()).generateDrop(this,
                model, getVendor());
    }

    // @SuppressWarnings("unused")
    // protected ISQLScript computeGenerationScript(IGenerationResult
    // currentGeneration,String name,
    // Collection<IGenerationResult> contentsGeneration) {
    // SQLWrapperScript s = new SQLWrapperScript(name,"");
    // // A set of all processed results
    // Set<IGenerationResult> processedResults = new
    // HashSet<IGenerationResult>();
    // // A copy of our input collection which we will modify (delete processed
    // entries
    // Collection<IGenerationResult> contents = new
    // ArrayList<IGenerationResult>(contentsGeneration);
    // int loopCount = 0;
    // while(!contents.isEmpty() && loopCount < MAX_LOOPS) {
    // IGenerationResult toProcess = null;
    // for(IGenerationResult c : contents) {
    // if(c.getPreconditions().isEmpty()) {
    // toProcess = c;
    // break;
    // } else {
    //
    // }
    // }
    // loopCount++;
    // }
    // return s;
    // }

    /**
     * This method is a helper which helps generating sub items incremental
     * scripts.
     * 
     * @param category
     *            the category of the child comparison item to generate
     * @param result
     *            the current comparison result for the generator
     * @param type
     *            the child item type
     * @param resolveDependencies
     *            indicates whether the integration will resolve dependencies
     */
    protected IGenerationResult generateTypedChildren(String category, IComparisonItem result, IElementType type,
            boolean resolveDependencies) {
        // Getting generator
        ISQLGenerator generator = getGenerator(type);
        // Delegating
        return generateChildren(category, result, generator, resolveDependencies);
    }

    /**
     * This method is a helper which helps generating sub items incremental
     * scripts.
     * 
     * @param category
     *            the category of the child comparison item to generate
     * @param result
     *            the current comparison result for the generator
     * @param generator
     *            the generator to use for child generation
     * @param resolveDependencies
     *            indicates whether the integration will resolve dependencies
     */
    protected IGenerationResult generateChildren(String category, IComparisonItem result, ISQLGenerator generator,
            boolean resolveDependencies) {
        List<IGenerationResult> childGenerations = new ArrayList<IGenerationResult>();
        // Getting sub items
        List<IComparisonItem> items = result.getSubItems(category);
        if (items == null) {
            return null;
            // // We try to look for an attribute instead of list
            // IComparisonItem attr = result.getAttribute(category);
            // if(attr == null) {
            // return null;
            // } else {
            // // If we find an attribute we convert it to a list of 1 element
            // items = new ArrayList<IComparisonItem>();
            // items.add(attr);
            // }
        }
        // Parsing comparison column items
        ISQLGenerator itemGenerator = generator;
        for (IComparisonItem colItem : items) {
            if (generator == null) {
                itemGenerator = getGenerator(colItem.getType());
            }
            // Generating child
            IGenerationResult childGeneration = itemGenerator.generateIncrementalSQL(colItem);
            childGenerations.add(childGeneration);
        }
        return integrateAll(childGenerations, resolveDependencies);
    }

    /**
     * Generates the list of children element using their default preconfigured
     * generator.
     * 
     * @param children
     *            list of children to generate
     * @param resolveDependencies
     *            should we resolve the dependencies of the resulting
     *            {@link IGenerationResult} when assembling everything
     *            altogether.
     * @return a generation result for this whold generation
     */
    public IGenerationResult generateChildren(Collection<? extends ITypedObject> children,
            boolean resolveDependencies) {
        return generateChildren(children, null, resolveDependencies);
    }

    /**
     * Generates the list of children element using the specified generator for
     * all children
     * 
     * @param children
     *            list of children to generate
     * @param g
     *            generator to use
     * @param resolveDependencies
     *            should we resolve the dependencies of the resulting
     *            {@link IGenerationResult} when assembling everything
     *            altogether.
     * @return a generation result for this whold generation
     * @see SQLGenerator#generateChildren(Collection, boolean)
     */
    public IGenerationResult generateChildren(Collection<? extends ITypedObject> children, ISQLGenerator g,
            boolean resolveDependencies) {
        List<IGenerationResult> childGenerations = new ArrayList<IGenerationResult>();

        for (ITypedObject child : children) {
            // Getting generator
            ISQLGenerator generator = (g == null ? getGenerator(child.getType()) : g);
            // Generating children
            if (child instanceof IComparisonItem) {
                // Generating incremental and adding if there is a non null
                // generation result
                IGenerationResult childGeneration = generator.generateIncrementalSQL((IComparisonItem) child);
                childGenerations.add(childGeneration);
            } else {
                // Generating incremental and adding to global result
                IGenerationResult childGeneration = generator.generateFullSQL(child);
                childGenerations.add(childGeneration);
            }
        }
        // Integrating
        return integrateAll(childGenerations, resolveDependencies);
    }

    /**
     * Integrates a collection of generation results into a single generation
     * result. Dependencies may be resolved depending on the resolveDependency
     * flag. Null generation results will be filtered.
     * 
     * @param name
     *            name of the resulting generation result
     * @param generations
     *            a collection of all generation results to integrate
     * @param resolveDependencies
     *            a flag indicating if the integration should resolve
     *            dependencies. It consists in ordering the results depending on
     *            their dependencies (i.e. to ensure tables are created before
     *            indexes, etc.)
     * @return a single generation result integrating the collection of results
     */
    public static IGenerationResult integrateAll(String name, List<IGenerationResult> generations,
            boolean resolveDependencies) {
        // Initializing generation result
        IGenerationResult generation = new GenerationResult(name);
        // Removing null entries
        List<IGenerationResult> nonNullResults = new ArrayList<IGenerationResult>();
        for (IGenerationResult r : generations) {
            if (r != null) {
                nonNullResults.add(r);
            }
        }
        // Resolving by ordering generations (see the GenerationResult
        // comparator implementation)
        List<IGenerationResult> resolvedResults = new ArrayList<IGenerationResult>();
        if (resolveDependencies) {
            // Try / catch safe blocks added for DES-925 problems
            try {
                Collections.sort(nonNullResults, NameComparator.getInstance());
            } catch (RuntimeException e) {
                LOGGER.warn("Unable to sort generation results by name: " + e.getMessage(), e); //$NON-NLS-1$
            }
            try {
                Collections.sort(nonNullResults);
            } catch (RuntimeException e) {
                LOGGER.warn("Unable to sort generation results by natural comparison: " //$NON-NLS-1$
                        + e.getMessage(), e);
            }
            // Since comparator of IGenerationResult is not transitive
            // the sort might not resolve all dependencies
            // Hashing results by db ref
            Map<DatabaseReference, IGenerationResult> refMap = new HashMap<DatabaseReference, IGenerationResult>();
            for (IGenerationResult result : nonNullResults) {
                for (DatabaseReference generatedRef : result.getGeneratedReferences()) {
                    refMap.put(generatedRef, result);
                }
            }
            // Resolving
            for (IGenerationResult result : nonNullResults) {
                if (!resolvedResults.contains(result)) {
                    processPreconditions(result, resolvedResults, refMap, new ArrayList<IGenerationResult>());
                }
            }
        } else {
            resolvedResults = nonNullResults;
        }
        // Integrating children generations
        for (IGenerationResult r : resolvedResults) {
            generation.integrate(r);
        }
        return generation;
    }

    public static void processPreconditions(IGenerationResult result, List<IGenerationResult> resolvedResults,
            Map<DatabaseReference, IGenerationResult> refMap, Collection<IGenerationResult> stack) {
        if (result.getPreconditions().isEmpty()) {
            resolvedResults.add(result);
        } else {
            // We have a deadloop here, so we return
            if (stack.contains(result)) {
                LOGGER.warn("Circular dependencies found, generation order may not be accurate.");
                return;
            }
            stack.add(result);
            for (DatabaseReference ref : result.getPreconditions()) {
                IGenerationResult precondResult = refMap.get(ref);
                if (!resolvedResults.contains(precondResult) && precondResult != null && precondResult != result) {
                    try {
                        processPreconditions(precondResult, resolvedResults, refMap, stack);
                    } catch (StackOverflowError e) {
                        LOGGER.info(result.getName());
                        throw e;
                    }
                }
            }
            stack.remove(result);
            resolvedResults.add(result);
        }
    }

    /**
     * Performs an unnamed integration of all specified scrips
     * 
     * @see SQLGenerator#integrateAll(String, List, boolean)
     * @param generations
     *            collection of generation results to integrate
     * @param resolveDependencies
     *            indicates whether the integration will resolve dependencies
     * @return a single generation result integrating all specified generations
     */
    public static IGenerationResult integrateAll(List<IGenerationResult> generations, boolean resolveDependencies) {
        return integrateAll(null, generations, resolveDependencies);
    }

    /**
     * This method will simply check if the only difference in this comparison
     * item is the name. This could be used to generate the "ALTER ... RENAME"
     * statements
     * 
     * @param item
     *            the comparison item to check
     * @return <code>true</code> if the only difference is the name, else
     *         <code>false</code>
     */
    protected boolean isRenamedOnly(IComparisonItem item) {
        boolean renamedOnly = false;
        for (IComparisonItem i : item.getSubItems()) {
            if (i instanceof ComparisonAttribute && Merger.ATTR_NAME.equals(((ComparisonAttribute) i).getName())) {
                if (i.getDifferenceType() == DifferenceType.DIFFER) {
                    renamedOnly = true;
                }
            } else {
                if (i.getDifferenceType() != DifferenceType.EQUALS) {
                    return false;
                }
            }
        }
        return renamedOnly;
    }

    protected boolean isRenamed(IComparisonItem item) {
        IComparisonItem attrName = item.getAttribute(Merger.ATTR_NAME);
        if (attrName instanceof ComparisonAttribute) {
            return (attrName.getDifferenceType() == DifferenceType.DIFFER);
        }
        return false;
    }

    /**
     * Checks the consistency of this object if it implements the
     * {@link ICheckedObject} interface. This method is called before
     * generating.
     * 
     * @param o
     */
    private void checkConsistency(Object o) {
        if (!(o instanceof ICheckedObject)) {
            return;
        }
        ICheckedObject c = (ICheckedObject) o;
        try {
            c.checkConsistency();
        } catch (InconsistentObjectException e) {
            throw new ErrorException(NameHelper.getQualifiedName(c)
                    + " appear to be inconsistent. Fix it before generating. Reason: " + e.getReason());
        }
    }

    /**
     * Adds all provided scripts to a source script, using commas to separate
     * one script content from another. A header and footer may be provided
     * which will encapsulate the script enumeration. This method is used to
     * append several child scripts to a global ALTER SQL command.
     * 
     * @param source
     *            source script which will be modified by appending the scripts
     *            list.
     * @param header
     *            header to append to the source script before appending the
     *            scripts list
     * @param footer
     *            footer to append to the source script after appending the
     *            scripts list
     * @param scripts
     *            scripts list to append to the source, using a comma to
     *            separate script contents
     */
    protected void addCommaSeparatedScripts(ISQLScript source, String header, String footer,
            List<ISQLScript> scripts) {
        if (scripts.size() == 0)
            return;
        boolean first = true;
        source.appendSQL(header);
        for (ISQLScript script : scripts) {
            source.appendSQL(NEWLINE).appendSQL(INDENTATION);
            // Adding format & comma on every item but the first
            if (!first) {
                source.appendSQL(","); //$NON-NLS-1$
            } else {
                source.appendSQL(" "); //$NON-NLS-1$
                first = false;
            }
            // Appending script
            source.appendSQL(script.getSql());
        }
        source.appendSQL(NEWLINE);
        if (footer != null && !"".equals(footer)) { //$NON-NLS-1$
            source.appendSQL(footer).appendSQL(NEWLINE);
        }
    }

    @Override
    public void setVendor(DBVendor vendor) {
        this.vendor = vendor;
        initSQLCommandWriter();
    }

    @Override
    public DBVendor getVendor() {
        return vendor;
    }

    /**
     * This method initialize the SQL command writer according to the database
     * vendor supplied to this SQL generator.
     */
    private void initSQLCommandWriter() {
        this.sqlCommandWriter = SQLGenPlugin.getService(IGenerationService.class).getSQLCommandWriter(getVendor());
    }

    /**
     * Returns the SQL command writer associated with this SQL generator.
     * 
     * @return a {@link ISQLCommandWriter}
     */
    protected ISQLCommandWriter getSQLCommandWriter() {
        if (sqlCommandWriter == null) {
            initSQLCommandWriter();
        }
        return sqlCommandWriter;
    }

    /**
     * A helper method which returns the desired {@link ISQLGenerator} for the
     * specified {@link IElementType}.
     * 
     * @param t
     *            the {@link IElementType} for which you want a SQL generator
     * @return the {@link ISQLGenerator}
     */
    protected ISQLGenerator getGenerator(IElementType t) {
        // TODO: Warning, this code retrieves the "global" vendor defined for
        // the view, instead of
        // local module vendor
        final ISQLGenerator generator = GeneratorFactory.getGenerator(t, getVendor());
        generator.setVendor(getVendor());
        return generator;
    }

    /**
     * Generates the SQL code which prompts the specified message to the user
     * during execution
     * 
     * @return the prompt command to use for this vendor (for generic
     *         generation)
     * @see ISQLCommandWriter#promptMessage(String)
     */
    protected String prompt(String message) {
        /*
         * For a quicker migration this method has been updated to use the
         * ISQLCommandWriter interface, but subclasses should call
         * getSQLCommandWriter().promptMessage(String) instead.
         */
        return getSQLCommandWriter().promptMessage(message);
    }

    /**
     * Helper method to append a statement delimiter followed by a new line
     * character to the last statement appended to the specified SQLScript. This
     * method does not check if the SQL script actually contains a statement to
     * close.
     * 
     * @param script
     *            a script with an unclosed statement at the end of the script.
     * @return the specified script with a statement delimiter appended at the
     *         end.
     * @see ISQLCommandWriter#closeStatement()
     */
    protected ISQLScript closeLastStatement(ISQLScript script) {
        /*
         * For a quicker migration this method has been updated to use the
         * ISQLCommandWriter interface, but subclasses should call
         * getSQLCommandWriter().closeStatement() instead.
         */
        return script.appendSQL(getSQLCommandWriter().closeStatement());
    }

    protected ISQLParser getParser() {
        return GeneratorFactory.getSQLParser(getVendor());
    }

    protected IProgressMonitor getMonitor() {
        return new NullProgressMonitor();
    }

    /**
     * Convenience method to get a new <code>ISQLScript</code> instance and set
     * its name, description and type attributes. This method uses the proper
     * {@link ITypedObjectFactory} to create a new <code>ISQLScript</code>
     * instance.
     * 
     * @param name
     *            the name of the script
     * @param description
     *            the description of the script
     * @param type
     *            the type of the script, must correspond to one of the values
     *            of the {@link ScriptType} enumeration
     * @return a new {@link ISQLScript} initialized with the specified
     *         attributes
     */
    protected ISQLScript getSqlScript(String name, String description, ScriptType type) {
        ISQLScript script = CorePlugin.getTypedObjectFactory().create(ISQLScript.class);
        script.setName(name);
        script.setDescription(description);
        script.setScriptType(type);
        return script;
    }

    /**
     * Convenience method for escaping names
     * 
     * @param name
     *            name to escape
     * @return the escaped name
     */
    protected String escape(String name) {
        return getSQLCommandWriter().escapeDbObjectName(name);
    }

    /**
     * Provides a name to use for SQL generation, which handles proper schema
     * location by performing a lookup in the container hierarchy.
     * 
     * @param dbObject
     *            the {@link IDatabaseObject} to generate a name for
     * @return the proper name which might be prefixed by a schema
     */
    protected String getName(IDatabaseObject<?> dbObject) {
        return getName(dbObject.getName(), dbObject);
    }

    /**
     * Provides a name to use for SQL generation using the given name as the
     * database object name and the given database object for schema location.
     * 
     * @param name
     *            name of the database object to generate
     * @param forObject
     *            the {@link IDatabaseObject} to use for locating the schema of
     *            this object
     * @return the SQL generation name of the object, optionally schema-prefixed
     */
    protected String getName(String name, IDatabaseObject<?> forObject) {
        // Check if we have a versionable
        IVersionable<?> versionable = VersionHelper.getVersionable(forObject);
        String schemaPrefix = "";
        // If yes, we check the owning container prefix
        if (versionable != null) {
            // Requesting the schema of this versionable
            final String schema = getSchema(versionable.getContainer());
            // If something has been defined we update the prefix
            if (schema != null) {
                schemaPrefix = schema + ".";
            }
        }
        return schemaPrefix + name;
    }

    private String getSchema(IVersionContainer v) {
        // If null we are on top of hierarchy and no schema was found
        if (v == null) {
            return null;
        } else {
            // Extracting defined schema
            final String schema = v.getSchemaName();
            // If something is defined
            if (schema != null && !"".equals(schema.trim())) {
                // We return it
                return schema;
            } else {
                // Otherwise we go up in the container hierarchy
                IVersionable<?> versionable = VersionHelper.getVersionable(v);
                // If we have a parent container
                if (versionable != null) {
                    // We return the schema for this
                    return getSchema(versionable.getContainer());
                } else {
                    return null;
                }
            }
        }
    }

    /**
     * Helper method to check emptiness of a script by safely checking that :<br>
     * - It is not null<br>
     * - It is not empty after trimming<br>
     * 
     * @param s
     *            string to check
     * @return <code>true</code> if empty or <code>null</code>, else
     *         <code>false</code>
     */
    protected boolean isEmpty(String s) {
        return s == null || "".equals(s.trim()); //$NON-NLS-1$
    }

    /**
     * Convenience method to return an empty string if the specified string is
     * <code>null</code>.
     * 
     * @param s
     *            a <code>String</code>
     * @return an empty string if the specified string is <code>null</code>, the
     *         same string otherwise.
     */
    protected String notNull(String s) {
        return (s == null ? "" : s); //$NON-NLS-1$
    }

}