View Javadoc

1   package org.andromda.core.cartridge;
2   
3   import java.io.File;
4   import java.io.StringWriter;
5   
6   import java.net.URL;
7   
8   import java.util.ArrayList;
9   import java.util.Collection;
10  import java.util.Iterator;
11  import java.util.LinkedHashMap;
12  import java.util.LinkedHashSet;
13  import java.util.List;
14  import java.util.Map;
15  
16  import org.andromda.core.cartridge.template.ModelElement;
17  import org.andromda.core.cartridge.template.ModelElements;
18  import org.andromda.core.cartridge.template.Template;
19  import org.andromda.core.cartridge.template.Type;
20  import org.andromda.core.common.AndroMDALogger;
21  import org.andromda.core.common.BasePlugin;
22  import org.andromda.core.common.ExceptionUtils;
23  import org.andromda.core.common.Introspector;
24  import org.andromda.core.common.PathMatcher;
25  import org.andromda.core.common.ResourceUtils;
26  import org.andromda.core.common.ResourceWriter;
27  import org.andromda.core.configuration.Namespaces;
28  import org.andromda.core.metafacade.MetafacadeFactory;
29  import org.andromda.core.metafacade.ModelAccessFacade;
30  import org.apache.commons.lang.BooleanUtils;
31  
32  
33  /***
34   * The AndroMDA Cartridge implementation of the Plugin. Cartridge instances are configured from
35   * <code>META-INF/andromda/cartridge.xml</code> files discovered on the classpath.
36   *
37   * @author <a href="http://www.mbohlen.de">Matthias Bohlen </a>
38   * @author Chad Brandon
39   */
40  public class Cartridge
41      extends BasePlugin
42  {
43      /***
44       * Processes all model elements with relevant stereotypes by retrieving the model elements from the model facade
45       * contained within the context.
46       *
47       * @param factory the metafacade factory (which is used to manage the lifecycle of metafacades).
48       */
49      public void processModelElements(final MetafacadeFactory factory)
50      {
51          ExceptionUtils.checkNull(
52              "factory",
53              factory);
54          final Collection resources = this.getResources();
55          if (resources != null && !resources.isEmpty())
56          {
57              for (final Iterator iterator = resources.iterator(); iterator.hasNext();)
58              {
59                  final Resource resource = (Resource)iterator.next();
60                  if (resource instanceof Template)
61                  {
62                      this.processTemplate(
63                          factory,
64                          (Template)resource);
65                  }
66                  else
67                  {
68                      this.processResource(resource);
69                  }
70              }
71          }
72      }
73  
74      /***
75       * Processes the given <code>template</code>.
76       *
77       * @param factory the metafacade factory instance.
78       * @param template the Template instance to process.
79       */
80      protected void processTemplate(
81          final MetafacadeFactory factory,
82          final Template template)
83      {
84          ExceptionUtils.checkNull(
85              "template",
86              template);
87          final ModelElements templateModelElements = template.getSupportedModeElements();
88  
89          // - handle the templates WITH model elements
90          if (templateModelElements != null && !templateModelElements.isEmpty())
91          {
92              for (final Iterator iterator = templateModelElements.getModelElements().iterator(); iterator.hasNext();)
93              {
94                  final ModelElement templateModelElement = (ModelElement)iterator.next();
95  
96                  // - if the template model element has a stereotype
97                  //   defined, then we filter the metafacades based
98                  //   on that stereotype, otherwise we get all metafacades
99                  //   and let the templateModelElement perform filtering on the
100                 //   metafacades by type and properties
101                 if (templateModelElement.hasStereotype())
102                 {
103                     templateModelElement.setMetafacades(
104                         factory.getMetafacadesByStereotype(templateModelElement.getStereotype()));
105                 }
106                 else if (templateModelElement.hasTypes())
107                 {
108                     templateModelElement.setMetafacades(factory.getAllMetafacades());
109                 }
110             }
111             this.processTemplateWithMetafacades(
112                 factory,
113                 template);
114         }
115         else
116         {
117             // - handle any templates WITHOUT metafacades.
118             this.processTemplateWithoutMetafacades(template);
119         }
120     }
121 
122     /***
123      * Processes all <code>modelElements</code> for this template.
124      *
125      * @param factory the metafacade factory
126      * @param template the template to process
127      */
128     protected void processTemplateWithMetafacades(
129         final MetafacadeFactory factory,
130         final Template template)
131     {
132         ExceptionUtils.checkNull(
133             "template",
134             template);
135         final ModelElements modelElements = template.getSupportedModeElements();
136         if (modelElements != null && !modelElements.isEmpty())
137         {
138             try
139             {
140                 final Collection allMetafacades = modelElements.getAllMetafacades();
141 
142                 // - if outputToSingleFile is true AND outputOnEmptyElements
143                 //   is true or we have at least one metafacade in the
144                 //   allMetafacades collection, then we collect the template
145                 //   model elements and place them into the template context
146                 //   by their variable names.
147                 if (template.isOutputToSingleFile() &&
148                     (template.isOutputOnEmptyElements() || !allMetafacades.isEmpty()))
149                 {
150                     final Map templateContext = new LinkedHashMap();
151 
152                     // - first place all relevant model elements by the
153                     //   <modelElements/> variable name. If the variable
154                     //   isn't defined (which is possible), ignore.
155                     final String modelElementsVariable = modelElements.getVariable();
156                     if (modelElementsVariable != null && modelElementsVariable.trim().length() > 0)
157                     {
158                         templateContext.put(
159                             modelElements.getVariable(),
160                             allMetafacades);
161                     }
162 
163                     // - now place the collections of elements by the given variable names. 
164                     //   (skip if the variable is NOT defined)
165                     for (final Iterator iterator = modelElements.getModelElements().iterator(); iterator.hasNext();)
166                     {
167                         final ModelElement modelElement = (ModelElement)iterator.next();
168                         final String modelElementVariable = modelElement.getVariable();
169                         if (modelElementVariable != null && modelElementVariable.trim().length() > 0)
170                         {
171                             // - if a modelElement has the same variable defined
172                             //   more than one time, then get the existing
173                             //   model elements added from the last iteration
174                             //   and add the new ones to that collection
175                             Collection metafacades = (Collection)templateContext.get(modelElementVariable);
176                             if (metafacades != null)
177                             {
178                                 metafacades.addAll(modelElement.getMetafacades());
179                             }
180                             else
181                             {
182                                 metafacades = modelElement.getMetafacades();
183                                 templateContext.put(
184                                     modelElementVariable,
185                                     new LinkedHashSet(metafacades));
186                             }
187                         }
188                     }
189                     this.processWithTemplate(
190                         template,
191                         templateContext,
192                         null,
193                         null);
194                 }
195                 else
196                 {
197                     // - if outputToSingleFile isn't true, then
198                     //   we just place the model element with the default
199                     //   variable defined on the <modelElements/> into the
200                     //   template.
201                     for (final Iterator iterator = allMetafacades.iterator(); iterator.hasNext();)
202                     {
203                         final Map templateContext = new LinkedHashMap();
204                         final Object metafacade = iterator.next();
205                         final ModelAccessFacade model = factory.getModel();
206                         for (final Iterator elements = modelElements.getModelElements().iterator(); elements.hasNext();)
207                         {
208                             final ModelElement modelElement = (ModelElement)elements.next();
209                             String variable = modelElement.getVariable();
210 
211                             // - if the variable isn't defined on the <modelElement/>, try
212                             //   the <modelElements/>
213                             if (variable == null || variable.trim().length() == 0)
214                             {
215                                 variable = modelElements.getVariable();
216                             }
217 
218                             // - only add the metafacade to the template context if the variable
219                             //   is defined (which is possible)
220                             if (variable != null && variable.trim().length() > 0)
221                             {
222                                 templateContext.put(
223                                     variable,
224                                     metafacade);
225                             }
226 
227                             // - now we process any property templates (if any 'variable' attributes are defined on one or
228                             //   more type's given properties), otherwise we process the single metafacade as usual
229                             if (!this.processPropertyTemplates(
230                                     template,
231                                     metafacade,
232                                     templateContext,
233                                     modelElement))
234                             {
235                                 this.processWithTemplate(
236                                     template,
237                                     templateContext,
238                                     model.getName(metafacade),
239                                     model.getPackageName(metafacade));
240                             }
241                         }
242                     }
243                 }
244             }
245             catch (final Throwable throwable)
246             {
247                 throw new CartridgeException(throwable);
248             }
249         }
250     }
251 
252     /***
253      * Determines if any property templates need to be processed (that is templates
254      * that are processed given related <em>properties</em> of a metafacade).
255      *
256      * @param template the template to use for processing.
257      * @param metafacade the metafacade instance (the property value is retrieved from this).
258      * @param templateContext the template context containing the instance to pass to the template.
259      * @param modelElement the model element from which we retrieve the corresponding types and then
260      *        properties to determine if any properties have been mapped for template processing.
261      * @return true if any property templates have been evaluated (false othewise).
262      */
263     private boolean processPropertyTemplates(
264         final Template template,
265         final Object metafacade,
266         final Map templateContext,
267         final ModelElement modelElement)
268     {
269         boolean propertyTemplatesEvaluated = false;
270         for (final Iterator types = modelElement.getTypes().iterator(); types.hasNext();)
271         {
272             final Type type = (Type)types.next();
273             for (final Iterator properties = type.getProperties().iterator(); properties.hasNext();)
274             {
275                 final Type.Property property = (Type.Property)properties.next();
276                 final String variable = property.getVariable();
277                 propertyTemplatesEvaluated = variable != null && variable.trim().length() > 0;
278                 if (propertyTemplatesEvaluated)
279                 {
280                     final Object value = Introspector.instance().getProperty(
281                             metafacade,
282                             property.getName());
283                     if (value instanceof Collection)
284                     {
285                         for (final Iterator values = ((Collection)value).iterator(); values.hasNext();)
286                         {
287                             templateContext.put(
288                                 variable,
289                                 values.next());
290                             this.processWithTemplate(
291                                 template,
292                                 templateContext,
293                                 null,
294                                 null);
295                         }
296                     }
297                     else
298                     {
299                         templateContext.put(
300                             variable,
301                             value);
302                         this.processWithTemplate(
303                             template,
304                             templateContext,
305                             null,
306                             null);
307                     }
308                 }
309             }
310         }
311         return propertyTemplatesEvaluated;
312     }
313 
314     /***
315      * Processes the <code>template</code> without metafacades. This is useful if you need to generate something that
316      * is part of your cartridge, however you only need to use a property passed in from a namespace or a template
317      * object defined in your cartridge descriptor.
318      *
319      * @param template the template to process.
320      */
321     protected void processTemplateWithoutMetafacades(final Template template)
322     {
323         ExceptionUtils.checkNull(
324             "template",
325             template);
326         final Map templateContext = new LinkedHashMap();
327         this.processWithTemplate(
328             template,
329             templateContext,
330             null,
331             null);
332     }
333 
334     /***
335      * <p>
336      * Perform processing with the <code>template</code>.
337      * </p>
338      *
339      * @param template the Template containing the template path to process.
340      * @param templateContext the context to which variables are added and made
341      *        available to the template engine for processing. This will contain
342      *        any model elements being made avaiable to the template(s) as well
343      *        as properties/template objects.
344      * @param metafacadeName the name of the model element (if we are
345      *        processing a single model element, otherwise this will be
346      *        ignored).
347      * @param metafacadePackage the name of the package (if we are processing
348      *        a single model element, otherwise this will be ignored).
349      */
350     private void processWithTemplate(
351         final Template template,
352         final Map templateContext,
353         final String metafacadeName,
354         final String metafacadePackage)
355     {
356         ExceptionUtils.checkNull(
357             "template",
358             template);
359         ExceptionUtils.checkNull(
360             "templateContext",
361             templateContext);
362 
363         File outputFile = null;
364         try
365         {
366             // - populate the template context with cartridge descriptor
367             //   properties and template objects
368             this.populateTemplateContext(templateContext);
369 
370             final StringWriter output = new StringWriter();
371 
372             // - process the template with the set TemplateEngine
373             this.getTemplateEngine().processTemplate(
374                 template.getPath(),
375                 templateContext,
376                 output);
377             
378             // - if we have an outputCondition defined make sure it evaluates to true before continuing
379             if (this.isValidOutputCondition(template.getOutputCondition(), templateContext))
380             {
381                 // - get the location and at the same time evaluate the outlet as a template engine variable (in case
382                 //   its defined as that).
383                 final String location =
384                     Namespaces.instance().getPropertyValue(
385                         this.getNamespace(),
386                         this.getTemplateEngine().getEvaluatedExpression(
387                             template.getOutlet(),
388                             templateContext));
389     
390                 if (location != null)
391                 {
392                     outputFile =
393                         template.getOutputLocation(
394                             metafacadeName,
395                             metafacadePackage,
396                             new File(location),
397                             this.getTemplateEngine().getEvaluatedExpression(
398                                 template.getOutputPattern(),
399                                 templateContext));
400                     if (outputFile != null)
401                     {
402                         // - only write files that do NOT exist, and
403                         //   those that have overwrite set to 'true'
404                         if (!outputFile.exists() || template.isOverwrite())
405                         {
406                             final String outputString = output.toString();
407                             AndroMDALogger.setSuffix(this.getNamespace());
408     
409                             // - check to see if generateEmptyFiles is true and if
410                             //   outString is not blank
411                             if ((outputString != null && outputString.trim().length() > 0) ||
412                                 template.isGenerateEmptyFiles())
413                             {
414                                 ResourceWriter.instance().writeStringToFile(
415                                     outputString,
416                                     outputFile,
417                                     this.getNamespace());
418                                 AndroMDALogger.info("Output: '" + outputFile.toURI() + "'");
419                             }
420                             else
421                             {
422                                 if (this.getLogger().isDebugEnabled())
423                                 {
424                                     this.getLogger().debug("Empty Output: '" + outputFile.toURI() + "' --> not writing");
425                                 }
426                             }
427                             AndroMDALogger.reset();
428                         }
429                     }
430                 }
431             }
432         }
433         catch (final Throwable throwable)
434         {
435             if (outputFile != null)
436             {
437                 outputFile.delete();
438                 this.getLogger().info("Removed: '" + outputFile + "'");
439             }
440             final String message =
441                 "Error processing template '" + template.getPath() + "' with template context '" + templateContext +
442                 "' using cartridge '" + this.getNamespace() + "'";
443             throw new CartridgeException(message, throwable);
444         }
445     }
446 
447     /***
448      * Processes the given <code>resource</code>
449      *
450      * @param resource the resource to process.
451      */
452     protected void processResource(final Resource resource)
453     {
454         ExceptionUtils.checkNull(
455             "resource",
456             resource);
457 
458         URL resourceUrl = ResourceUtils.getResource(
459                 resource.getPath(),
460                 this.getMergeLocation());
461         if (resourceUrl == null)
462         {
463             // - if the resourceUrl is null, the path is probably a regular
464             //   outputCondition pattern so we'll see if we can match it against
465             //   the contents of the plugin and write any contents that do match
466             final List contents = this.getContents();
467             if (contents != null)
468             {
469                 AndroMDALogger.setSuffix(this.getNamespace());
470                 for (final Iterator iterator = contents.iterator(); iterator.hasNext();)
471                 {
472                     final String content = (String)iterator.next();
473                     if (content != null && content.trim().length() > 0)
474                     {
475                         if (PathMatcher.wildcardMatch(
476                                 content,
477                                 resource.getPath()))
478                         {
479                             resourceUrl = ResourceUtils.getResource(
480                                     content,
481                                     this.getMergeLocation());
482                             // - don't attempt to write the directories within the resource
483                             if (!resourceUrl.toString().endsWith(FORWARD_SLASH))
484                             {
485                                 this.writeResource(
486                                     resource,
487                                     resourceUrl);                                
488                             }
489                         }
490                     }
491                 }
492                 AndroMDALogger.reset();
493             }
494         }
495         else
496         {
497             this.writeResource(
498                 resource,
499                 resourceUrl);
500         }
501     }
502 
503     /***
504      * The forward slash constant.
505      */
506     private static final String FORWARD_SLASH = "/";
507 
508     /***
509      * Writes the contents of <code>resourceUrl</code> to the outlet specified by <code>resource</code>.
510      *
511      * @param resource contains the outlet where the resource is written.
512      * @param resourceUrl the URL contents to write.
513      */
514     private void writeResource(
515         final Resource resource,
516         final URL resourceUrl)
517     {
518         File outputFile = null;
519         try
520         {
521             // - make sure we don't have any back slashes
522             final String resourceUri = ResourceUtils.normalizePath(resourceUrl.toString());
523             final String uriSuffix =
524                 resourceUri.substring(
525                     resourceUri.lastIndexOf(FORWARD_SLASH),
526                     resourceUri.length());
527 
528             final Map templateContext = new LinkedHashMap();
529             this.populateTemplateContext(templateContext);
530             
531             // - if we have an outputCondition defined make sure it evaluates to true before continuing
532             if (this.isValidOutputCondition(resource.getOutputCondition(), templateContext))
533             {
534                 // - get the location and at the same time evaluate the outlet as a template engine variable (in case
535                 //   its defined as that).
536                 final String location =
537                     Namespaces.instance().getPropertyValue(
538                         this.getNamespace(),
539                         this.getTemplateEngine().getEvaluatedExpression(
540                             resource.getOutlet(),
541                             templateContext));
542     
543                 if (location != null)
544                 {
545                     outputFile =
546                         resource.getOutputLocation(
547                             new String[] {uriSuffix},
548                             new File(location),
549                             this.getTemplateEngine().getEvaluatedExpression(
550                                 resource.getOutputPattern(),
551                                 templateContext));
552                     
553                     final boolean lastModifiedCheck = resource.isLastModifiedCheck();
554                     // - if we have the last modified check set, then make sure the last modified time is greater than the outputFile
555                     if (!lastModifiedCheck || (lastModifiedCheck && ResourceUtils.getLastModifiedTime(resourceUrl) > outputFile.lastModified()))
556                     {    
557                         // - only write files that do NOT exist, and
558                         //   those that have overwrite set to 'true'
559                         if (!outputFile.exists() || resource.isOverwrite())
560                         {
561                             ResourceWriter.instance().writeUrlToFile(
562                                 resourceUrl,
563                                 outputFile.toString());
564                             AndroMDALogger.info("Output: '" + outputFile.toURI() + "'");
565                         }
566                     }
567                 }
568             }
569         }
570         catch (final Throwable throwable)
571         {
572             if (outputFile != null)
573             {
574                 outputFile.delete();
575                 this.getLogger().info("Removed: '" + outputFile + "'");
576             }
577             throw new CartridgeException(throwable);
578         }
579     }
580 
581     /***
582      * Stores the loaded resources to be processed by this cartridge intance.
583      */
584     private final List resources = new ArrayList();
585 
586     /***
587      * Returns the list of templates configured in this cartridge.
588      *
589      * @return List the template list.
590      */
591     public List getResources()
592     {
593         return this.resources;
594     }
595 
596     /***
597      * Adds a resource to the list of defined resources.
598      *
599      * @param resource the new resource to add
600      */
601     public void addResource(final Resource resource)
602     {
603         ExceptionUtils.checkNull(
604             "resource",
605             resource);
606         resource.setCartridge(this);
607         resources.add(resource);
608     }
609     
610     /***
611      * Populates the <code>templateContext</code> with the properties and template objects defined in the
612      * <code>plugin</code>'s descriptor. If the <code>templateContext</code> is null, a new Map instance will be created
613      * before populating the context.
614      *
615      * @param templateContext the context of the template to populate.
616      */
617     protected void populateTemplateContext(Map templateContext)
618     {
619         super.populateTemplateContext(templateContext);
620         templateContext.putAll(this.getEvaluatedConditions(templateContext));
621     }
622     
623     /***
624      * Stores the global conditions.
625      */
626     private final Map conditions = new LinkedHashMap();
627     
628     /***
629      * Adds the outputCondition given the <code>name</code> and <code>value</code>
630      * to the outputConditions map.
631      * 
632      * @param name the name of the outputCondition.
633      * @param value the value of the outputCondition.
634      */
635     public void addCondition(final String name, final String value)
636     {
637         this.conditions.put(name, value != null ? value.trim() : "");
638     }
639     
640     /***
641      * Gets the current outputConditions defined within this cartridge
642      */
643     public Map getConditions()
644     {
645         return this.conditions;
646     }
647     
648     /***
649      * Indicates whether or not the global outputConditions have been evaluated.
650      */
651     private boolean conditionsEvaluated = false;
652     
653     /***
654      * Evaluates all conditions and stores the results in the <code>conditions</code>
655      * and returns that Map
656      * 
657      * @param templateContext the template context used to evaluate the conditions.
658      * @param the map containing the evaluated conditions.
659      */
660     private Map getEvaluatedConditions(final Map templateContext)
661     {
662         if (!this.conditionsEvaluated)
663         {
664             for (final Iterator iterator = this.conditions.keySet().iterator(); iterator.hasNext();)
665             {
666                 final String name = (String)iterator.next();
667                 final String value = (String)this.conditions.get(name);
668                 String evaluationResult = value != null ? value.trim() : null;
669                 if (evaluationResult != null && evaluationResult.trim().length() > 0)
670                 {
671                     evaluationResult = this.getTemplateEngine().getEvaluatedExpression(
672                         evaluationResult,
673                         templateContext);
674                 }
675                 this.conditions.put(name, Boolean.valueOf(BooleanUtils.toBoolean(evaluationResult)));
676             }
677             this.conditionsEvaluated = true;
678         }
679         return this.conditions;
680     }
681     
682     /***
683      * Gets the evaluted outputCondition result of a global outputCondition.
684      * 
685      * @param templateContext the current template context to pass the template engine if 
686      *        evaluation has yet to occurr.
687      * @return the evaluated outputCondition results.
688      */
689     private Boolean getGlobalConditionResult(final String outputCondition, final Map templateContext) 
690     {
691         return (Boolean)this.getEvaluatedConditions(templateContext).get(outputCondition);
692     }
693     
694     /***
695      * Indicates whether or not the given <code>outputCondition</code> is a valid
696      * outputCondition, that is, whether or not it return true.
697      * 
698      * @param outputCondition the outputCondition to evaluate.
699      * @param templateContext the template context containing the variables to use.
700      * @return true/false
701      */
702     private boolean isValidOutputCondition(final String outputCondition, final Map templateContext)
703     {
704         boolean validOutputCondition = true;
705         if (outputCondition != null && outputCondition.trim().length() > 0)
706         {
707             Boolean result = this.getGlobalConditionResult(outputCondition, templateContext);
708             if (result == null)
709             {
710                 final String outputConditionResult = this.getTemplateEngine().getEvaluatedExpression(
711                     outputCondition,
712                     templateContext);
713                 result = Boolean.valueOf(BooleanUtils.toBoolean(outputConditionResult != null ? outputConditionResult.trim() : null));
714             }
715             validOutputCondition = result != null ? result.booleanValue() : false;
716         }
717         return validOutputCondition;
718     }
719 
720     /***
721      * Override to provide cartridge specific shutdown (
722      *
723      * @see org.andromda.core.common.Plugin#shutdown()
724      */
725     public void shutdown()
726     {
727         super.shutdown();
728         this.conditions.clear();
729     }
730 }