Author: fdesbois Date: 2010-03-24 00:29:29 +0100 (Wed, 24 Mar 2010) New Revision: 1853 Log: Evo #412 : template for final application -> generate abstract services with skeleton which contains transaction (topiaContext) managment (try/catch/finally, errors, ...) See javadoc Added: trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/BusinessTransformer.java Modified: trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/TopiaGeneratorUtil.java Added: trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/BusinessTransformer.java =================================================================== --- trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/BusinessTransformer.java (rev 0) +++ trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/BusinessTransformer.java 2010-03-23 23:29:29 UTC (rev 1853) @@ -0,0 +1,573 @@ + +package org.nuiton.topia.generator; + +import java.util.ArrayList; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.eugene.GeneratorUtil; +import org.nuiton.eugene.Template; +import org.nuiton.eugene.java.ObjectModelTransformerToJava; +import org.nuiton.eugene.models.object.ObjectModel; +import org.nuiton.eugene.models.object.ObjectModelClass; +import org.nuiton.eugene.models.object.ObjectModelInterface; +import org.nuiton.eugene.models.object.ObjectModelModifier; +import org.nuiton.eugene.models.object.ObjectModelOperation; +import org.nuiton.eugene.models.object.ObjectModelParameter; +import org.nuiton.i18n.I18n; +import org.nuiton.topia.TopiaContext; +import org.nuiton.topia.TopiaException; + +/*{generator option: parentheses = false}*/ + +/*{generator option: writeString = +}*/ + +/** + * This Template is used to create the skeleton of services for a final + * application which using Topia. + * <pre> + * Generation from a model named 'App' : + * + * - AppContext : empty super interface to used in application UI. Can + * be override in model if put in defaultPackage (ex : org.chorem.app) set + * in maven-eugene-plugin configuration. + * + * - AppContextImplementor : interface which extends AppContext to add + * technical methods for the application. Generation of methods : + * * doCatch : used to catch all exception from a service method. + * * doFinally : used to finally the try/catch of a service method + * * beginTransaction : start the transaction using rootContext. + * These three methods have to be implemented in a class which implements + * AppContextImplementor (ex : AppContextImpl). You can also add others + * methods to AppContextImplementor in the same way as AppContext. + * + * - AppException : exception class which extends RuntimeException for all + * technical exceptions which appears in service method. If you want to + * manage some specific exceptions, you have to managed them in doCatch + * implementation. + * + * Generation from interfaces with stereotype <<service>> : + * + * - Service : interface of the service defined in model. + * + * - ServiceAbstract : abstract class which contains : + * * constructor with AppContextImplementor in argument + * * for each method : the implementation of the method (skeleton with + * try/catch and beginTransaction call to open a new TopiaContext from + * AppContextImplementor). Usage of i18n keys for error messages in + * exception. + * * for each method : an abstract method used to execute the business + * code of the method : need to be implemented in subclass. + * + * Exemple of AppContextImpl : + * + * public class AppContextImpl implements AppContextImplementor { + * + * // properties for Topia configuration + * protected Properties properties; + * ... + * + * @Override + * public void doCatch(TopiaContext transaction, Exception eee, + * String message, Object... args) throws AppException { + * + * // Note that the message from service doesn't directly use _() for + * // i18 messages but n_(). In this log, the _() is used to translate + * // correctly the message. But the message must be translate when + * // catching the AppException in UI. + * if (log.isErrorEnabled()) { + * log.error(_(message, args), eee); + * } + * + * // rollback of current transaction + * if (transaction != null) { + * try { + * transaction.rollbackTransaction(); + * } catch (TopiaException ex) { + * if (log.isErrorEnabled()) { + * log.error(_("app.error.context.rollback"), ex); + * } + * } + * } + * // wrapping the exception in a AppException with message and + * // arguments for i18n translation + * throw new AppException(eee, message, args); + * } + * + * @Override + * public void doFinally(TopiaContext transaction) { + * if (transaction != null) { + * try { + * transaction.closeContext(); + * } catch (TopiaException eee) { + * if (log.isErrorEnabled()) { + * log.error(_("app.error.context.close"), eee); + * } + * } + * } + * } + * + * @Override + * public TopiaContext beginTransaction() throws TopiaException { + * TopiaContext rootContext = null; + * try { + * // You have to manage the properties using ApplicationConfig + * // or other lib to have configuration for Topia + * rootContext = TopiaContextFactory.getContext(properties); + * + * return getTopiaRootContext().beginTransaction(); + * + * // only catch exception for rootContext + * } catch (TopiaNotFoundException eee) { + * doCatch(eee, n_("app.error.context.getTopiaRootContext")); + * } + * return null; + * } + * ... + * } + * + * Exemple of ServiceImpl : + * + * public class ServiceImpl extends ServiceAbstract { + * + * public ServiceImpl(AppContextImplementor context) { + * super(context); + * } + * + * // Implementation of abstract method, the interface method is + * // called 'createMyEntity(MyEntity entity)' in this case. + * @Override + * public void executeCreateMyEntity(TopiaContext transaction, + * MyEntity entity) throws TopiaException { + * + * MyEntityDAO dao = AppDAOHelper.getMyEntityDAO(transaction); + * dao.create(entity); + * // That's it, no need to manage errors or transaction, the abstract + * // service will do this job. + * } + * } + * + * TAG_TRANSACTION + * --------------- + * + * You can use the tagValue 'transaction=false' to specify that a method doesn't + * need any TopiaContext, so no need to instantiate a new one. This tagValue + * can only be put directly in the model and not in properties file (because + * of multiple methods with same name problem). + * + * TAG_ERROR_ARGS + * -------------- + * + * You can use the tagValue 'errorArgs=false' to specify that a method doesn't + * need any arguments for error message. This tagValue can only be put directly + * in the model and not in properties file. + * + * It is smooth, isn't it :p ? + * + * TODO : may be refactor to integrate JTA or webservice or may be not in this + * transformer. + * + * TODO : find a good way to change log level + * + * </pre> + * + * Created: 23 mars 2010 + * + * @author fdesbois + * @version $Revision$ + * + * Mise a jour: $Date$ + * par : $Author$ + */ +public class BusinessTransformer extends ObjectModelTransformerToJava { + + protected String modelName; + + protected String defaultPackageName; + + protected String getContextInterfaceName() { + return modelName + "Context"; + } + + protected String getContextImplementorInterfaceName() { + return getContextInterfaceName() + "Implementor"; + } + + protected String getExceptionClassName() { + return modelName + "Exception"; + } + + protected String getServiceAbstractClassName(String serviceName) { + return serviceName + "Abstract"; + } + + @Override + public void transformFromModel(ObjectModel model) { + modelName = model.getName(); + defaultPackageName = getOutputProperties(). + getProperty(Template.PROP_DEFAULT_PACKAGE); + + ObjectModelInterface contextImplementor = + model.getInterface(defaultPackageName + "." + + getContextImplementorInterfaceName()); + + ObjectModelInterface context = + model.getInterface(defaultPackageName + "." + + getContextInterfaceName()); + + ObjectModelClass exception = createExceptionClass(); + + ObjectModelInterface newContextImplementor = + this.createInterface(getContextImplementorInterfaceName(), + defaultPackageName); + ObjectModelInterface newContext = + this.createInterface(getContextInterfaceName(), + defaultPackageName); + + this.addInterface(newContextImplementor, + newContext.getQualifiedName()); + + if (contextImplementor != null) { + // Copy of defined operations + // interfaces of contextImplementor are not copied + copyInterfaceOperations(contextImplementor, newContextImplementor); + } + + if (context != null) { + // Copy of defined operations + // interfaces of context are not copied + copyInterfaceOperations(context, newContext); + } + + ObjectModelOperation beginTransaction = + this.addOperation(newContextImplementor, + "beginTransaction", TopiaContext.class); + this.addException(beginTransaction, TopiaException.class); + + ObjectModelOperation doCatch1 = + this.addOperation(newContextImplementor, "doCatch", "void"); + this.addParameter(doCatch1, Exception.class, "eee"); + this.addParameter(doCatch1, String.class, "message"); + this.addParameter(doCatch1, "Object...", "args"); + this.addException(doCatch1, exception.getQualifiedName()); + + ObjectModelOperation doCatch2 = + this.addOperation(newContextImplementor, "doCatch", "void"); + this.addParameter(doCatch2, TopiaContext.class, "transaction"); + this.addParameter(doCatch2, Exception.class, "eee"); + this.addParameter(doCatch2, String.class, "message"); + this.addParameter(doCatch2, "Object...", "args"); + this.addException(doCatch2, exception.getQualifiedName()); + + ObjectModelOperation doFinally = + this.addOperation(newContextImplementor, "doFinally", "void"); + this.addParameter(doFinally, TopiaContext.class, "transaction"); + } + + protected ObjectModelClass createExceptionClass() { + + ObjectModelClass exception = + this.createClass(getExceptionClassName(), defaultPackageName); + + this.setSuperClass(exception, RuntimeException.class); + this.addAttribute(exception, "args", "Object[]", null, + ObjectModelModifier.PROTECTED); + + ObjectModelOperation constructor = + this.addConstructor(exception, ObjectModelModifier.PUBLIC); + + this.addParameter(constructor, Throwable.class, "eee"); + this.addParameter(constructor, String.class, "message"); + this.addParameter(constructor, "Object...", "args"); + + setOperationBody(constructor, "" + /*{ + super(message, eee); + this.args = args; + }*/ + ); + + ObjectModelOperation getArgs = + this.addOperation(exception, "getArgs", "Object[]", + ObjectModelModifier.PUBLIC); + + setOperationBody(getArgs, "" + /*{ + return args; + }*/ + ); + + ObjectModelOperation hasArgs = + this.addOperation(exception, "hasArgs", "boolean", + ObjectModelModifier.PUBLIC); + + setOperationBody(hasArgs, "" + /*{ + return args.length > 0; + }*/ + ); + + return exception; + } + + /** + * Used to simply copy the {@code source} interface signature to the + * {@code dest} interface. + * + * @param source interface + * @param dest interface + */ + protected void copyInterfaceOperations(ObjectModelInterface source, + ObjectModelInterface dest) { + for (ObjectModelOperation op : source.getOperations()) { + ObjectModelOperation newOp = + this.addOperation(dest, + op.getName(), op.getReturnType()); + setDocumentation(newOp.getReturnParameter(), + op.getReturnParameter().getDocumentation()); + for (ObjectModelParameter param : op.getParameters()) { + ObjectModelParameter newParam = + this.addParameter(newOp, param.getType(), + param.getName()); + setDocumentation(newParam, param.getDocumentation()); + } + for (String ex : op.getExceptions()) { + this.addException(newOp, ex); + } + setDocumentation(newOp, op.getDocumentation()); + } + } + + @Override + public void transformFromInterface(ObjectModelInterface interfacez) { + // skip ContextImplementor and Context interfaces + if (!interfacez.hasStereotype(TopiaGeneratorUtil.STEREOTYPE_SERVICE)) { + return; + } + + // Create INTERFACE + ObjectModelInterface serviceInterface = + this.createInterface(interfacez.getName(), + interfacez.getPackageName()); + + copyInterfaceOperations(interfacez, serviceInterface); + + + // Create ABSTRACT CLASS + ObjectModelClass service = this.createAbstractClass( + getServiceAbstractClassName(interfacez.getName()), + interfacez.getPackageName()); + + this.addInterface(service, serviceInterface.getQualifiedName()); + + // Add Logger + // FIXME in EUGene, we want the default value not to be managed + // for import. +// this.addAttribute(service, "log", +// Log.class, +// "LogFactory.getLog(" + interfacez.getName() + ".class)", +// ObjectModelModifier.PRIVATE, +// ObjectModelModifier.STATIC, +// ObjectModelModifier.FINAL); + this.addAttribute(service, "log", + Log.class, null, + ObjectModelModifier.PRIVATE, + ObjectModelModifier.FINAL); + + this.addImport(service, Log.class); + this.addImport(service, LogFactory.class); + + String contextFqn = defaultPackageName + "." + + getContextImplementorInterfaceName(); + + // Add Context Attribute + constructor + this.addAttribute(service, "context", contextFqn, null, + ObjectModelModifier.PROTECTED); + + // Constructor + ObjectModelOperation constructor = + this.addConstructor(service, ObjectModelModifier.PUBLIC); + this.addParameter(constructor, contextFqn, "context"); + setOperationBody(constructor, "" + /*{ + this.context = context; + // FIXME : must be fixed attribute value in EUGene + this.log = LogFactory.getLog(<%=interfacez.getName()%>.class); + }*/ + ); + + // Prepare operation generations + String first = modelName.substring(0, 1); + String serviceName = + GeneratorUtil.toLowerCaseFirstLetter(interfacez.getName()); + + this.addImport(service, TopiaContext.class); + this.addImport(service, I18n.class); + this.addImport(service, ArrayList.class); + + for (ObjectModelOperation op : interfacez.getOperations()) { + + // boolean to specify if the method need a transaction or not + // Default set to true but can be override by a tagvalue on the + // method + boolean needTransaction = true; + + String transactionTag = + op.getTagValue(TopiaGeneratorUtil.TAG_TRANSACTION); + + if (transactionTag != null) { + needTransaction = Boolean.parseBoolean(transactionTag); + } + + // boolean to specify if the method need error arguments or not + // Default set to true but can be override by a tagvalue on the + // method + boolean needErrorArgs = true; + + String errorArgsTag = + op.getTagValue(TopiaGeneratorUtil.TAG_ERROR_ARGS); + + if (errorArgsTag != null) { + needErrorArgs = Boolean.parseBoolean(errorArgsTag); + } + + // Implementation of interface operation + ObjectModelOperation implOp = + this.addOperation(service, + op.getName(), op.getReturnType(), + ObjectModelModifier.PUBLIC); + this.addAnnotation(service, implOp, Override.class.getSimpleName()); + + String opName = StringUtils.capitalize(op.getName()); + + // Abstract operation to execute method content + ObjectModelOperation abstOp = + this.addOperation(service, "execute" + opName, + op.getReturnType(), + ObjectModelModifier.ABSTRACT, + ObjectModelModifier.PROTECTED); + + if (needTransaction) { + this.addParameter(abstOp, TopiaContext.class, "transaction"); + this.addException(abstOp, TopiaException.class); + } + + // Copy exceptions + for (String ex : op.getExceptions()) { + this.addException(implOp, ex); + this.addException(abstOp, ex); + } + + String toStringAppend = ""; + String separatorLog = " : "; + // Prepare operation parameters + String opParams = ""; + String separatorParams = ""; + if (needErrorArgs) { + opParams += "errorArgs"; + separatorParams = ", "; + // Add errorArgs to abstract operation + this.addParameter(abstOp, + "java.util.List<Object>", "errorArgs"); + } + + // Copy other operation parameters + for (ObjectModelParameter param : op.getParameters()) { + String paramName = param.getName(); + this.addParameter(implOp, param.getType(), param.getName()); + this.addParameter(abstOp, param.getType(), param.getName()); + + // Prepare Log + toStringAppend += + "\n\t\t\t.append(\"" + separatorLog + paramName + " = \")" + + ".append(" + paramName + ")"; + separatorLog = " _ "; + + // Prepare Abstract method params + opParams += separatorParams + param.getName(); + separatorParams = ", "; + } + + // Use buffer for operation body + StringBuilder buffer = new StringBuilder(); + + // Error key for i18n + String errorKey = StringUtils.lowerCase(modelName) + ".error." + + serviceName + "." + op.getName(); + + String doCatchParams = "eee, I18n.n_(\"" + errorKey + "\")"; + doCatchParams += needErrorArgs ? ", errorArgs.toArray()" : ""; + + // Return managment + String opReturn = ""; + String finalReturn = ""; + if (!op.getReturnType().equals("void")) { + opReturn = "return "; + finalReturn = "return null;"; + } + + if (needErrorArgs) { + // Init errorArgs + buffer.append("" + /*{ + List<Object> errorArgs = new ArrayList<Object>(); + }*/ ); + } + + if (needTransaction) { + // Open the transaction + buffer.append("" + /*{ + TopiaContext transaction = null; + try { + transaction = context.beginTransaction(); + }*/ + ); + // Add transaction in the execute operation parameters + // and doCatch parameters + opParams = "transaction, " + opParams; + doCatchParams = "transaction, " + doCatchParams; + } else { + buffer.append("" + /*{ + try { + }*/ + ); + } + + buffer.append("" + /*{ + if (log.isDebugEnabled()) { + String message = new StringBuilder("<%=first%>:[ <%=opName%> ]")<%=toStringAppend%>. + toString(); + log.debug(message); + } + + <%=opReturn%>execute<%=opName%>(<%=opParams%>); + } catch (Exception eee) { + context.doCatch(<%=doCatchParams%>); + }*/ + ); + + if (needTransaction) { + // Finally block to close transaction + buffer.append("" + /*{ + } finally { + context.doFinally(transaction); + }*/ + ); + } + + buffer.append("" + /*{ + } + <%=finalReturn%> + }*/ + ); + + this.setOperationBody(implOp, buffer.toString()); + } + + } +} Property changes on: trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/BusinessTransformer.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL" Modified: trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/TopiaGeneratorUtil.java =================================================================== --- trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/TopiaGeneratorUtil.java 2010-03-23 13:04:37 UTC (rev 1852) +++ trunk/topia-persistence/src/main/java/org/nuiton/topia/generator/TopiaGeneratorUtil.java 2010-03-23 23:29:29 UTC (rev 1853) @@ -138,6 +138,16 @@ */ public static final String TAG_NATURAL_ID_MUTABLE = "naturalIdMutable"; /** + * Tag pour specifier si une methode a besoin d'une transaction + * (TopiaContext) ou non + */ + public static final String TAG_TRANSACTION = "transaction"; + /** + * Tag pour specifier si une methode de service a besoin d'arguments pour + * le message d'erreur ou non + */ + public static final String TAG_ERROR_ARGS = "errorArgs"; + /** * Tag pour spécifier la caractère lazy d'une association multiple */ public static final String TAG_LAZY = "lazy";