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 == null) input = 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 (String) get(APPLICATION_VERSION);
169 }
170
171 /**
172 * @return The Griffon version used to build the application
173 */
174 public String getGriffonVersion() {
175 return (String) get(APPLICATION_GRIFFON_VERSION);
176 }
177
178 /**
179 * @return The environment the application expects to run in
180 */
181 public String getEnvironment() {
182 return (String) get(Environment.KEY);
183 }
184
185 /**
186 * @return The application name
187 */
188 public String getApplicationName() {
189 return (String) get(APPLICATION_NAME);
190 }
191
192 /**
193 * @return Supported toolkit by this application
194 */
195 public String getApplicationToolkit() {
196 return (String) get(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 = (String) get(GRIFFON_START_DIR);
241 if (griffonStartDir == null) {
242 griffonStartDir = System.getProperty(GRIFFON_START_DIR);
243 if (griffonStartDir != null && griffonStartDir.length() > 1 &&
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() > 1 &&
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 = (String) get(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 }
|