001package org.andromda.core.configuration;
002
003import java.io.IOException;
004import java.io.Serializable;
005import java.net.URL;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.HashMap;
010import java.util.Iterator;
011import java.util.List;
012import java.util.Map;
013import org.andromda.core.common.ResourceUtils;
014import org.apache.commons.lang.StringUtils;
015
016/**
017 * Stores the model information for each model that AndroMDA will process.
018 *
019 * @author Chad Brandon
020 */
021public class Model
022    implements Serializable
023{
024    private static final long serialVersionUID = 34L;
025
026    /**
027     * Stores whether or not a last modified check
028     * should be performed.
029     */
030    private boolean lastModifiedCheck = false;
031
032    /**
033     * Whether or not to perform a last modified check on the model.
034     *
035     * @return Returns the lastModifiedCheck.
036     */
037    public boolean isLastModifiedCheck()
038    {
039        return lastModifiedCheck;
040    }
041
042    /**
043     * Sets whether or not to perform a last modified check when processing the model. If
044     * <code>true</code> the model will be checked for a timestamp before processing occurs.
045     *
046     * @param lastModifiedCheck true/false
047     */
048    public void setLastModifiedCheck(final boolean lastModifiedCheck)
049    {
050        this.lastModifiedCheck = lastModifiedCheck;
051    }
052
053    /**
054     * Stores the information about what packages should and shouldn't
055     * be processed.
056     */
057    private Filters packages = new Filters();
058
059    /**
060     * Sets the processAll flag on the internal model packages instance
061     * of this model.
062     *
063     * @param processAllPackages whether or not all packages should be processed by default.
064     */
065    public void setProcessAllPackages(final boolean processAllPackages)
066    {
067        packages.setApplyAll(processAllPackages);
068    }
069
070    /**
071     * Stores the information about what packages should/shouldn't be processed.
072     *
073     * @return Returns the packages.
074     */
075    public Filters getPackages()
076    {
077        return this.packages;
078    }
079
080    /**
081     * Sets the model packages for this model.  This indicates what
082     * packages should and should not be processed from this model.
083     *
084     * @param packages the packages to process.
085     */
086    public void setPackages(final Filters packages)
087    {
088        this.packages = packages;
089    }
090
091    /**
092     * Stores the information about what constraints should and shouldn't
093     * be enforced.
094     */
095    private Filters constraints = new Filters();
096
097    /**
098     * Sets the applyAll flag on the internal filters instance
099     * of this model.
100     *
101     * @param enforceAllConstraints whether or not all constraints should be enforced by default.
102     */
103    public void setEnforceAllConstraints(final boolean enforceAllConstraints)
104    {
105        this.constraints.setApplyAll(enforceAllConstraints);
106    }
107
108    /**
109     * Stores the information about what constraints should/shouldn't be enforced.
110     *
111     * @return Returns the constraints instance.
112     */
113    public Filters getConstraints()
114    {
115        return this.constraints;
116    }
117
118    /**
119     * Sets the constraints for this model.  This indicates what
120     * constraints should and should not be processed from this model.
121     *
122     * @param constraints the packages to process.
123     */
124    public void setConstraints(final Filters constraints)
125    {
126        this.constraints = constraints;
127    }
128
129    /**
130     * The URL to the model.
131     */
132    private List<URL> uris = new ArrayList<URL>();
133
134    /**
135     * Caches the urisAsStrings value (so we don't need
136     * to do the conversion more than once).
137     */
138    private String[] urisAsStrings = null;
139
140    /**
141     * All URIs that make up the model.
142     *
143     * @return Returns the uri.
144     */
145    public String[] getUris()
146    {
147        if (this.urisAsStrings == null)
148        {
149            final int uriNumber = uris.size();
150            this.urisAsStrings = new String[uriNumber];
151            for (int ctr = 0; ctr < uriNumber; ctr++)
152            {
153                urisAsStrings[ctr] = uris.get(ctr).toString();
154            }
155        }
156        return this.urisAsStrings;
157    }
158
159    /**
160     * Adds the location as a URI to one of the model files.
161     *
162     * @param uri the URI to the model.
163     */
164    public void addUri(final String uri)
165    {
166        try
167        {
168            final URL url = ResourceUtils.toURL(uri);
169            if (url == null)
170            {
171                throw new ConfigurationException("Model could not be loaded from invalid path --> '" + uri + '\'');
172            }
173            try
174            {
175                // - Get around the fact the URL won't be released until the JVM
176                //   has been terminated, when using the 'jar' uri protocol.
177                url.openConnection().setDefaultUseCaches(false);
178            }
179            catch (final IOException exception)
180            {
181                // - ignore the exception
182            }
183            this.uris.add(url);
184        }
185        catch (final Throwable throwable)
186        {
187            throw new ConfigurationException(throwable);
188        }
189    }
190
191    /**
192     * Stores the transformations for this Configuration instance.
193     */
194    private final Collection<Transformation> transformations = new ArrayList<Transformation>();
195
196    /**
197     * Adds a transformation to this configuration instance.
198     *
199     * @param transformation the transformation instance to add.
200     */
201    public void addTransformation(final Transformation transformation)
202    {
203        this.transformations.add(transformation);
204    }
205
206    /**
207     * Gets the transformations belonging to this configuration.
208     *
209     * @return the array of {@link Transformation} instances.
210     */
211    public Transformation[] getTransformations()
212    {
213        return this.transformations.toArray(new Transformation[this.transformations.size()]);
214    }
215
216    /**
217     * The locations in which to search for module.
218     */
219    private final Collection<Location> moduleSearchLocations = new ArrayList<Location>();
220
221    /**
222     * Adds a module search location (these are the locations
223     * in which a search for module is performed).
224     *
225     * @param location a location path.
226     * @see #addModuleSearchLocation(String)
227     */
228    public void addModuleSearchLocation(final Location location)
229    {
230        this.moduleSearchLocations.add(location);
231    }
232
233    /**
234     * Adds a module search location path (a location
235     * without a pattern defined).
236     *
237     * @param path a location path.
238     * @see #addModuleSearchLocation(Location)
239     */
240    public void addModuleSearchLocation(final String path)
241    {
242        if (path != null)
243        {
244            final Location location = new Location();
245            location.setPath(path);
246            this.moduleSearchLocations.add(location);
247        }
248    }
249
250    /**
251     * The type of model (i.e. uml-1.4, uml-2.0, etc).
252     */
253    private String type;
254
255    /**
256     * Gets the type of the model (i.e. the type of metamodel this
257     * model is based upon).
258     *
259     * @return Returns the type.
260     */
261    public String getType()
262    {
263        return this.type;
264    }
265
266    /**
267     * Sets the type of model (i.e. the type of metamodel this model
268     * is based upon).
269     *
270     * @param type The type to set.
271     */
272    public void setType(final String type)
273    {
274        this.type = type;
275    }
276
277    /**
278     * Gets the module search locations for this model instance.
279     *
280     * @return the module search locations.
281     * @see #getModuleSearchLocationPaths()
282     */
283    public Location[] getModuleSearchLocations()
284    {
285        return this.moduleSearchLocations.toArray(new Location[this.moduleSearchLocations.size()]);
286    }
287
288    /**
289     * Stores the path for each module search location in this configuration.
290     */
291    private String[] moduleSearchLocationPaths = null;
292
293    /**
294     * Gets all found module search location paths for this model instance.
295     *
296     * @return the module search location paths.
297     * @see #getModuleSearchLocations()
298     */
299    public String[] getModuleSearchLocationPaths()
300    {
301        if (this.moduleSearchLocationPaths == null)
302        {
303            final Collection<String> paths = new ArrayList<String>();
304            for (final Location location : this.moduleSearchLocations)
305            {
306                final URL[] resources = location.getResources();
307                final int resourceNumber = resources.length;
308                for (int ctr = 0; ctr < resourceNumber; ctr++)
309                {
310                    paths.add(resources[ctr].toString());
311                }
312                paths.add(location.getPath());
313            }
314            this.moduleSearchLocationPaths = paths.toArray(new String[paths.size()]);
315        }
316        return this.moduleSearchLocationPaths;
317    }
318
319    /**
320     * Stores all resources including all resources found within the module search locations
321     * as well as a resource for the {@link #getUris()}.
322     */
323    private URL[] moduleSearchLocationResources = null;
324
325    /**
326     * Gets the accumulation of all files found when combining the contents
327     * of all module search location paths and their patterns by which they
328     * are filtered as well as the model URI.
329     *
330     * @return all module search location files.
331     */
332    public URL[] getModuleSearchLocationResources()
333    {
334        if (this.moduleSearchLocationResources == null)
335        {
336            final Collection<URL> allResources = new ArrayList<URL>();
337            final Location[] locations = this.getModuleSearchLocations();
338            for (final Location location : locations)
339            {
340                final URL[] resources = location.getResources();
341                allResources.addAll(Arrays.asList(resources));
342            }
343            this.moduleSearchLocationResources = allResources.toArray(new URL[allResources.size()]);
344        }
345        return this.moduleSearchLocationResources;
346    }
347
348    /**
349     * Gets the time of the latest modified uri of the model as a <code>long</code>.
350     * If it can not be determined <code>0</code> is returned.
351     *
352     * @return the time this model was last modified
353     */
354    public long getLastModified()
355    {
356        long lastModifiedTime = 0;
357        for (final URL url : uris)
358        {
359            final long modifiedTime = ResourceUtils.getLastModifiedTime(url);
360            if (modifiedTime > lastModifiedTime)
361            {
362                lastModifiedTime = modifiedTime;
363            }
364        }
365        return lastModifiedTime;
366    }
367
368    /**
369     * @see Object#toString()
370     */
371    public String toString()
372    {
373        String toString = super.toString();
374        final String key = this.getKey();
375        if (StringUtils.isNotBlank(key))
376        {
377            toString = key;
378        }
379        return toString;
380    }
381
382    /**
383     * Stores the last modified times for each model at the time
384     * {@link #isChanged()} is called.
385     */
386    private static final Map<String, Map<String, Long>> modelModifiedTimes = new HashMap<String, Map<String, Long>>();
387
388    /**
389     * The unique key that identifies this model.
390     */
391    private String key = null;
392
393    /**
394     * Creates the unique key that identifies this model
395     * (its made up of a list of all the URIs for this model
396     * concatenated).
397     *
398     * @return the unique key
399     */
400    private String getKey()
401    {
402        if (StringUtils.isBlank(this.key))
403        {
404            final StringBuilder buffer = new StringBuilder();
405            for (final Iterator<URL> iterator = this.uris.iterator(); iterator.hasNext();)
406            {
407                final URL uri = iterator.next();
408                buffer.append(uri.getFile());
409                if (iterator.hasNext())
410                {
411                    buffer.append(", ");
412                }
413            }
414            this.key = buffer.toString();
415        }
416        return this.key;
417    }
418
419    /**
420     * The repository to which this model belongs.
421     */
422    private Repository repository;
423
424    /**
425     * Gets the repository to which this model belongs.
426     *
427     * @return the repository to which this model belongs.
428     */
429    public Repository getRepository()
430    {
431        return this.repository;
432    }
433
434    /**
435     * Sets the repository to which this model belongs.
436     *
437     * @param repository the repository configuration to which this model belongs.
438     */
439    void setRepository(final Repository repository)
440    {
441        this.repository = repository;
442    }
443
444    /**
445     * Indicates whether or not the given <code>model</code>
446     * has changed since the previous call to this method.
447     *
448     * @return true/false
449     */
450    public boolean isChanged()
451    {
452        boolean changed = this.getUris().length > 0;
453        if (changed)
454        {
455            final String modelKey = this.getKey();
456            Map<String, Long> lastModifiedTimes = modelModifiedTimes.get(modelKey);
457
458            // - load up the last modified times (from the model and all its modules)
459            //   if they haven't been loaded yet
460            if (lastModifiedTimes != null)
461            {
462                final long modelLastModified = lastModifiedTimes.get(modelKey);
463                changed = this.getLastModified() > modelLastModified;
464                if (!changed)
465                {
466                    // - check to see if any of the modules have changed if the model hasn't changed
467                    final URL[] resources = this.getModuleSearchLocationResources();
468                    for (final URL resource : resources)
469                    {
470                        final Long lastModified = lastModifiedTimes.get(resource.getFile());
471                        if (lastModified != null)
472                        {
473                            // - when we find the first modified module, break out
474                            if (ResourceUtils.getLastModifiedTime(resource) > lastModified)
475                            {
476                                changed = true;
477                                break;
478                            }
479                        }
480                    }
481                }
482            }
483
484            // - if our model (or modules) have changed re-load the last modified times
485            if (changed)
486            {
487                this.loadLastModifiedTimes();
488            }
489        }
490        return changed;
491    }
492
493    /**
494     * Loads (or re-loads) the last modified times from the
495     * {@link #getUris()} and the modules found on the module search path.
496     */
497    private void loadLastModifiedTimes()
498    {
499        final String modelKey = this.getKey();
500        Map<String, Long> lastModifiedTimes = modelModifiedTimes.get(modelKey);
501        if (lastModifiedTimes == null)
502        {
503            lastModifiedTimes = new HashMap<String, Long>();
504        }
505        else
506        {
507            lastModifiedTimes.clear();
508        }
509        final URL[] resources = this.getModuleSearchLocationResources();
510        for (final URL resource : resources)
511        {
512            lastModifiedTimes.put(
513                resource.getFile(),
514                    ResourceUtils.getLastModifiedTime(resource));
515        }
516
517        // - add the model key last so it overwrites any invalid ones
518        //   we might have picked up from adding the module search location files.
519        lastModifiedTimes.put(
520                modelKey,
521                this.getLastModified());
522
523        modelModifiedTimes.put(
524            modelKey,
525            lastModifiedTimes);
526    }
527
528    /**
529     * Clears out the current last modified times.
530     */
531    static void clearLastModifiedTimes()
532    {
533        modelModifiedTimes.clear();
534    }
535}