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