Metadata.java
001 /*
002  * Copyright 2004-2013 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *      http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package griffon.util;
017 
018 import java.io.*;
019 import java.lang.ref.Reference;
020 import java.lang.ref.SoftReference;
021 import java.net.URL;
022 import java.util.*;
023 import java.util.regex.Pattern;
024 
025 /**
026  * Represents the application Metadata and loading mechanics
027  *
028  @author Graeme Rocher (Grails 1.1)
029  */
030 
031 public class Metadata extends Properties {
032     public static final String FILE = "application.properties";
033     public static final String APPLICATION_VERSION = "app.version";
034     public static final String APPLICATION_NAME = "app.name";
035     public static final String APPLICATION_GRIFFON_VERSION = "app.griffon.version";
036     public static final String GRIFFON_START_DIR = "griffon.start.dir";
037     public static final String GRIFFON_WORKING_DIR = "griffon.working.dir";
038     public static final String APPLICATION_TOOLKIT = "app.toolkit";
039 
040     private static final Pattern SKIP_PATTERN = Pattern.compile("^.*\\/griffon-.*.jar!\\/application.properties$");
041     private static Reference<Metadata> metadata = new SoftReference<Metadata>(new Metadata());
042 
043     private boolean initialized;
044     private File metadataFile;
045     private boolean dirty;
046 
047     private Metadata() {
048         super();
049     }
050 
051     private Metadata(File f) {
052         this.metadataFile = f;
053     }
054 
055     /**
056      * Resets the current state of the Metadata so it is re-read
057      */
058     public static void reset() {
059         Metadata m = metadata.get();
060         if (m != null) {
061             m.clear();
062             m.initialized = false;
063             m.dirty = false;
064         }
065     }
066 
067     /**
068      @return Returns the metadata for the current application
069      */
070     public static Metadata getCurrent() {
071         Metadata m = metadata.get();
072         if (m == null) {
073             metadata = new SoftReference<Metadata>(new Metadata());
074             m = metadata.get();
075         }
076         if (!m.initialized) {
077             InputStream input = null;
078             try {
079                 // GRIFFON-108 enable reading metadata from a local file IF AND ONLY IF
080                 // current environment == 'dev'.
081                 // must read environment directly from System.properties to avoid a
082                 // circular problem
083                 // if(Environment.getEnvironment(System.getProperty(Environment.KEY)) == Environment.DEVELOPMENT) {
084                 //     input = new FileInputStream(FILE);
085                 // }
086 
087                 // GRIFFON-255 there may be multiple versions of "application.properties" in the classpath
088                 // due to addon packaging. Avoid any URLS that look like plugin dirs or addon jars
089                 input = fetchApplicationProperties(ApplicationClassLoader.get());
090                 if (input == nullinput = fetchApplicationProperties(Metadata.class.getClassLoader());
091 
092                 if (input != null) {
093                     m.load(input);
094                 }
095             catch (Exception e) {
096                 throw new RuntimeException("Cannot load application metadata:" + e.getMessage(), e);
097             finally {
098                 closeQuietly(input);
099                 m.initialized = true;
100             }
101         }
102 
103         return m;
104     }
105 
106     /**
107      * Loads a Metadata instance from a Reader
108      *
109      @param inputStream The InputStream
110      @return a Metadata instance
111      */
112     public static Metadata getInstance(InputStream inputStream) {
113         Metadata m = new Metadata();
114         metadata = new FinalReference<Metadata>(m);
115 
116         try {
117             m.load(inputStream);
118             m.initialized = true;
119         catch (IOException e) {
120             throw new RuntimeException("Cannot load application metadata:" + e.getMessage(), e);
121         }
122         return m;
123     }
124 
125     /**
126      * Loads and returns a new Metadata object for the given File
127      *
128      @param file The File
129      @return A Metadata object
130      */
131     public static Metadata getInstance(File file) {
132         Metadata m = new Metadata(file);
133         metadata = new FinalReference<Metadata>(m);
134 
135         if (file != null && file.exists()) {
136             FileInputStream input = null;
137             try {
138                 input = new FileInputStream(file);
139                 m.load(input);
140                 m.initialized = true;
141             catch (Exception e) {
142                 throw new RuntimeException("Cannot load application metadata:" + e.getMessage(), e);
143             finally {
144                 closeQuietly(input);
145             }
146         }
147         return m;
148     }
149 
150     /**
151      * Reloads the application metadata
152      *
153      @return The metadata object
154      */
155     public static Metadata reload() {
156         File f = getCurrent().metadataFile;
157 
158         if (f != null) {
159             return getInstance(f);
160         }
161         return getCurrent();
162     }
163 
164     /**
165      @return The application version
166      */
167     public String getApplicationVersion() {
168         return (Stringget(APPLICATION_VERSION);
169     }
170 
171     /**
172      @return The Griffon version used to build the application
173      */
174     public String getGriffonVersion() {
175         return (Stringget(APPLICATION_GRIFFON_VERSION);
176     }
177 
178     /**
179      @return The environment the application expects to run in
180      */
181     public String getEnvironment() {
182         return (Stringget(Environment.KEY);
183     }
184 
185     /**
186      @return The application name
187      */
188     public String getApplicationName() {
189         return (Stringget(APPLICATION_NAME);
190     }
191 
192     /**
193      @return Supported toolkit by this application
194      */
195     public String getApplicationToolkit() {
196         return (Stringget(APPLICATION_TOOLKIT);
197     }
198 
199     /**
200      * Obtains a map (name->version) of installed plugins specified in the project metadata
201      *
202      @return A map of installed plugins
203      */
204     public Map<String, String> getInstalledPlugins() {
205         Map<String, String> newMap = new LinkedHashMap<String, String>();
206 
207         for (Map.Entry<Object, Object> entry : entrySet()) {
208             String key = entry.getKey().toString();
209             Object val = entry.getValue();
210             if (key.startsWith("plugins."&& val != null) {
211                 newMap.put(key.substring(8), val.toString());
212             }
213         }
214         return newMap;
215     }
216 
217     public Map<String, String> getArchetype() {
218         Map<String, String> newMap = new LinkedHashMap<String, String>();
219 
220         for (Map.Entry<Object, Object> entry : entrySet()) {
221             String key = entry.getKey().toString();
222             Object val = entry.getValue();
223             if (key.startsWith("archetype."&& val != null) {
224                 newMap.put("name", key.substring(10));
225                 newMap.put("version", val.toString());
226                 break;
227             }
228         }
229         return newMap;
230     }
231 
232     /**
233      * Returns the application's starting directory.<p>
234      * The value comes from the System property 'griffon.start.dir'
235      * if set. Result may be null.
236      *
237      @return The application start directory path
238      */
239     public String getGriffonStartDir() {
240         String griffonStartDir = (Stringget(GRIFFON_START_DIR);
241         if (griffonStartDir == null) {
242             griffonStartDir = System.getProperty(GRIFFON_START_DIR);
243             if (griffonStartDir != null && griffonStartDir.length() &&
244                     griffonStartDir.startsWith("\""&& griffonStartDir.endsWith("\"")) {
245                 // normalize without double quotes
246                 griffonStartDir = griffonStartDir.substring(1, griffonStartDir.length() 1);
247                 System.setProperty(GRIFFON_START_DIR, griffonStartDir);
248             }
249             if (griffonStartDir != null && griffonStartDir.length() &&
250                     griffonStartDir.startsWith("'"&& griffonStartDir.endsWith("'")) {
251                 // normalize without single quotes
252                 griffonStartDir = griffonStartDir.substring(1, griffonStartDir.length() 1);
253                 System.setProperty(GRIFFON_START_DIR, griffonStartDir);
254             }
255             if (griffonStartDir != null) {
256                 put(GRIFFON_START_DIR, griffonStartDir);
257             }
258         }
259         return griffonStartDir;
260     }
261 
262     /**
263      * Returns ia non-null value for the application's starting directory.<p>
264      * the path to new File(".") if that path is writable, returns
265      * the value of 'user.dir' otherwise.
266      *
267      @return The application start directory path
268      */
269     public String getGriffonStartDirSafe() {
270         String griffonStartDir = getGriffonStartDir();
271         if (griffonStartDir == null) {
272             File path = new File(".");
273             if (path.canWrite()) {
274                 return path.getAbsolutePath();
275             }
276             return System.getProperty("user.dir");
277         }
278         return griffonStartDir;
279     }
280 
281     /**
282      @return The application working directory
283      */
284     public File getGriffonWorkingDir() {
285         String griffonWorkingDir = (Stringget(GRIFFON_WORKING_DIR);
286         if (griffonWorkingDir == null) {
287             String griffonStartDir = getGriffonStartDirSafe();
288             File workDir = new File(griffonStartDir);
289             if (workDir.canWrite()) {
290                 put(GRIFFON_WORKING_DIR, griffonStartDir);
291                 return workDir;
292             else {
293                 try {
294                     File temp = File.createTempFile("griffon"".tmp");
295                     temp.deleteOnExit();
296                     workDir = new File(temp.getParent(), getApplicationName());
297                     put(GRIFFON_WORKING_DIR, workDir.getAbsolutePath());
298                     return workDir;
299                 catch (IOException ioe) {
300                     // ignore ??
301                     // should not happen
302                 }
303             }
304         }
305 
306         return new File(griffonWorkingDir);
307     }
308 
309     /**
310      * Saves the current state of the Metadata object
311      */
312     public void persist() {
313         if (dirty && metadataFile != null) {
314             FileOutputStream out = null;
315 
316             try {
317                 out = new FileOutputStream(metadataFile);
318                 store(out, "Griffon Metadata file");
319                 dirty = false;
320             catch (Exception e) {
321                 throw new RuntimeException("Error persisting metadata to file [" + metadataFile + "]: " + e.getMessage(), e);
322             finally {
323                 closeQuietly(out);
324             }
325         }
326     }
327 
328     /**
329      @return Returns true if these properties have not changed since they were loaded
330      */
331     public boolean propertiesHaveNotChanged() {
332         return dirty;
333     }
334 
335     @Override
336     public synchronized Object setProperty(String name, String value) {
337         if(containsKey(name)) {
338             Object oldValue = getProperty(name);
339             dirty = oldValue != null && !oldValue.equals(value);
340         else {
341             dirty = true;
342         }
343         return super.setProperty(name, value);
344     }
345 
346     @Override
347     public synchronized Object put(Object key, Object value) {
348         if(containsKey(key)) {
349             Object oldValue = get(key);
350             dirty = oldValue != null && !oldValue.equals(value);
351         else {
352             dirty = true;
353         }
354         return super.put(key, value);
355     }
356 
357     @Override
358     public synchronized void putAll(Map<? extends Object, ? extends Object> map) {
359         dirty = true;
360         super.putAll(map);
361     }
362 
363     /**
364      * Overrides, called by the store method.
365      */
366     @SuppressWarnings("unchecked")
367     public synchronized Enumeration keys() {
368         Enumeration keysEnum = super.keys();
369         Vector keyList = new Vector();
370         while (keysEnum.hasMoreElements()) {
371             keyList.add(keysEnum.nextElement());
372         }
373         Collections.sort(keyList);
374         return keyList.elements();
375     }
376 
377     private static void closeQuietly(Closeable c) {
378         if (c != null) {
379             try {
380                 c.close();
381             catch (Exception ignored) {
382                 // ignored
383             }
384         }
385     }
386 
387     static class FinalReference<T> extends SoftReference<T> {
388         private T ref;
389 
390         public FinalReference(T t) {
391             super(t);
392             this.ref = t;
393         }
394 
395         @Override
396         public T get() {
397             return ref;
398         }
399     }
400 
401     private static InputStream fetchApplicationProperties(ClassLoader classLoader) {
402         Enumeration<URL> urls = null;
403 
404         try {
405             urls = classLoader.getResources(FILE);
406         catch (IOException ioe) {
407             throw new RuntimeException(ioe);
408         }
409 
410         while (urls.hasMoreElements()) {
411             try {
412                 URL url = urls.nextElement();
413                 if (SKIP_PATTERN.matcher(url.toString()).matches()) continue;
414                 return url.openStream();
415             catch (IOException ioe) {
416                 // skip
417             }
418         }
419 
420         return null;
421     }
422 }