001    package org.andromda.core.configuration;
002    
003    import java.io.IOException;
004    import java.io.Serializable;
005    import java.net.URL;
006    import java.util.ArrayList;
007    import java.util.Arrays;
008    import java.util.Collection;
009    import java.util.HashMap;
010    import java.util.Iterator;
011    import java.util.List;
012    import java.util.Map;
013    import org.andromda.core.common.ResourceUtils;
014    import 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     */
021    public 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    }