001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.visualweb.jsfsupport.designtime;
042:
043: import com.sun.rave.designtime.DesignBean;
044: import org.netbeans.modules.visualweb.xhtml.F_LoadBundle;
045: import com.sun.rave.designtime.DesignContext;
046: import com.sun.rave.designtime.DesignProject;
047: import com.sun.rave.designtime.event.DesignProjectAdapter;
048: import com.sun.rave.designtime.event.DesignProjectListener;
049: import com.sun.rave.designtime.faces.FacesDesignProject;
050: import org.netbeans.modules.visualweb.jsfsupport.container.RaveFacesContext;
051: import java.io.File;
052: import java.io.FileInputStream;
053: import java.io.IOException;
054: import java.net.URI;
055: import java.util.ArrayList;
056: import java.util.Enumeration;
057: import java.util.HashMap;
058: import java.util.List;
059: import java.util.Locale;
060: import java.util.Map;
061: import java.util.PropertyResourceBundle;
062: import java.util.ResourceBundle;
063: import javax.faces.context.FacesContext;
064: import javax.faces.el.VariableResolver;
065: import org.openide.filesystems.FileChangeAdapter;
066: import org.openide.filesystems.FileChangeListener;
067: import org.openide.filesystems.FileEvent;
068: import org.openide.filesystems.FileObject;
069: import org.openide.filesystems.FileRenameEvent;
070: import org.openide.filesystems.FileUtil;
071:
072: /**
073: * A design-time variable resolver that mimics the JSF loadBundle tag. The resolver
074: * attempts to determine if the requested variable corresponds to a property bundle
075: * file in the project source directory. If a property bundle file is found, the
076: * resolver creates a map of all key-value pairs found in the file, and returns it
077: * as the resolution of the variable. The map is cached, to expedite future requests
078: * for the same variable.
079: *
080: * <p>Each bundle map listens for changes in its underlying file. If the file is
081: * modified, the map keys are refreshed. Note that this only happens when the user
082: * explicitly saves the bundle file in the IDE.
083: *
084: * <p>If the underyling file is renamed or deleted, the map is destroyed.
085: *
086: * @author gjmurphy
087: */
088: public class LoadBundleVariableResolver extends VariableResolver {
089:
090: private static final String VAR_PROPERTY = "var"; //NOI18N
091: private static final String BASENAME_PROPERTY = "basename"; //NOI18N
092:
093: // Listener for context close event on the design project
094: private DesignProjectListener projectListener;
095: // Parent of this variable resolver, to which requests are delegated
096: private VariableResolver parentVariableResolver;
097: // A map of resource bundle base names to maps containing the key/value
098: // pairs defined by the corresponding property resource bundle file
099: private Map<String, Map> propertyResourceMapMap = new HashMap<String, Map>();
100:
101: public LoadBundleVariableResolver(
102: VariableResolver parentVariableResolver) {
103: this .parentVariableResolver = parentVariableResolver;
104: }
105:
106: // @Override
107: public Object resolveVariable(FacesContext context, String name) {
108: DesignContext designContext = ((RaveFacesContext) context)
109: .getDesignContext();
110: DesignBean loadBundleDesignBean = null;
111: if (name != null && designContext != null) {
112: // Search for a LoadBundle design bean that corresponds to the variable name
113: DesignProject designProject = designContext.getProject();
114: if (designProject instanceof FacesDesignProject) {
115: DesignContext candidateDesignContext = ((FacesDesignProject) designProject)
116: .findDesignContext(name);
117: if (candidateDesignContext == null) {
118: DesignBean[] loadBundleBeans = designContext
119: .getBeansOfType(F_LoadBundle.class);
120: for (DesignBean designBean : loadBundleBeans) {
121: if (name.equals(designBean.getProperty(
122: VAR_PROPERTY).getValue())) {
123: loadBundleDesignBean = designBean;
124: break;
125: }
126: }
127: }
128: }
129: }
130: if (loadBundleDesignBean != null) {
131: // If a LoadBundle design bean was found, fetch the corresponding properties map
132: // from cache (or create a new map if not found in the cache)
133: String bundleBaseName = (String) loadBundleDesignBean
134: .getProperty(BASENAME_PROPERTY).getValue();
135: if (bundleBaseName != null && bundleBaseName.length() > 0) {
136: // Lazy initialization of design context listener
137: if (this .projectListener == null) {
138: this .projectListener = new LoadBundleProjectListener(
139: designContext);
140: designContext.getProject()
141: .addDesignProjectListener(
142: this .projectListener);
143: }
144: Map propertyResourceMap = this .propertyResourceMapMap
145: .get(bundleBaseName);
146: if (propertyResourceMap == null) {
147: List<String> candidateBundleNames = generateCandidateBundleNames(
148: bundleBaseName, Locale.getDefault());
149: File sourceDir = this
150: .getSourceDir(loadBundleDesignBean);
151: for (String candidateBundleName : candidateBundleNames) {
152: try {
153: File resourceBundleFile = new File(
154: sourceDir, candidateBundleName);
155: if (resourceBundleFile.exists()) {
156: propertyResourceMap = new PropertyResourceMap(
157: bundleBaseName,
158: resourceBundleFile);
159: this .propertyResourceMapMap.put(
160: bundleBaseName,
161: propertyResourceMap);
162: break;
163: }
164: } catch (IOException e) {
165: e.printStackTrace();
166: }
167: }
168: }
169: if (propertyResourceMap != null)
170: return propertyResourceMap;
171: }
172: }
173: return parentVariableResolver.resolveVariable(context, name);
174: }
175:
176: private static final String SOURCE_DIR = "src/";
177: private static final URI SOURCE_URI = URI.create(SOURCE_DIR);
178:
179: private File sourceDir;
180:
181: /**
182: * A utility method that returns a file object which represents the root
183: * source directory of the current project.
184: */
185: private File getSourceDir(DesignBean designBean) {
186: if (sourceDir == null)
187: sourceDir = designBean.getDesignContext().getProject()
188: .getResourceFile(SOURCE_URI);
189: // Depending on the project type, the source directory will be either ./src or
190: // ./src/java
191: File javaSourceDir = new File(sourceDir, "java");
192: if (javaSourceDir.exists())
193: sourceDir = javaSourceDir;
194: return sourceDir;
195: }
196:
197: private static final String BUNDLE_SUFFIX = ".properties";
198:
199: /**
200: * A utility method that generates a list of all candidate bundle file names
201: * for the base name and locale specified.
202: */
203: private static List<String> generateCandidateBundleNames(
204: String baseName, Locale locale) {
205: List<String> candidateBundleNames = new ArrayList<String>(8);
206: String language = locale.getLanguage();
207: int languageLength = language.length();
208: String country = locale.getCountry();
209: int countryLength = country.length();
210: String variant = locale.getVariant();
211: int variantLength = variant.length();
212:
213: if (baseName == null)
214: return candidateBundleNames;
215: StringBuffer buffer = new StringBuffer(baseName.replace('.',
216: '/'));
217: int bufferLength = buffer.length();
218: buffer.append(BUNDLE_SUFFIX);
219: candidateBundleNames.add(buffer.toString());
220: buffer.setLength(bufferLength);
221:
222: if (languageLength + countryLength + variantLength == 0)
223: return candidateBundleNames;
224:
225: buffer.append('_');
226: buffer.append(language);
227: if (languageLength > 0) {
228: bufferLength = buffer.length();
229: buffer.append(BUNDLE_SUFFIX);
230: candidateBundleNames.add(0, buffer.toString());
231: buffer.setLength(bufferLength);
232: }
233:
234: if (countryLength + variantLength == 0)
235: return candidateBundleNames;
236: buffer.append('_');
237: buffer.append(country);
238: if (countryLength > 0) {
239: bufferLength = buffer.length();
240: buffer.append(BUNDLE_SUFFIX);
241: candidateBundleNames.add(0, buffer.toString());
242: buffer.setLength(bufferLength);
243: }
244:
245: if (variantLength == 0)
246: return candidateBundleNames;
247: buffer.append('_');
248: buffer.append(variant);
249: buffer.append(BUNDLE_SUFFIX);
250: candidateBundleNames.add(0, buffer.toString());
251:
252: return candidateBundleNames;
253: }
254:
255: void clearPropertyResourceMap(String bundleBaseName) {
256: Map propertyResourceMap = LoadBundleVariableResolver.this .propertyResourceMapMap
257: .get(bundleBaseName);
258: propertyResourceMap.clear();
259: LoadBundleVariableResolver.this .propertyResourceMapMap
260: .remove(bundleBaseName);
261: }
262:
263: /**
264: * A design context listener that removes all file object listeners when this
265: * property resolver's design context is deactivated.
266: */
267: class LoadBundleProjectListener extends DesignProjectAdapter {
268:
269: DesignContext context;
270:
271: LoadBundleProjectListener(DesignContext context) {
272: this .context = context;
273: }
274:
275: public void contextClosed(DesignContext context) {
276: if (this .context == context) {
277: // System.err.println("CONTEXT DEACTIVATED");
278: context.getProject().removeDesignProjectListener(this );
279: LoadBundleVariableResolver.this .projectListener = null;
280: for (String bundleBaseName : LoadBundleVariableResolver.this .propertyResourceMapMap
281: .keySet()) {
282: clearPropertyResourceMap(bundleBaseName);
283: }
284: }
285: }
286:
287: }
288:
289: /**
290: * A property resource bundle that listens for changes to its underlying
291: * property file, and refreshes all of its keys in response to changes in the
292: * file. The bundle is implemented as a {@link java.util.Map} so that it can
293: * be returned directly in response to requests to resolve variables that
294: * reference load bundle components.
295: */
296: class PropertyResourceMap extends HashMap {
297:
298: FileChangeListener fileChangeListener;
299:
300: PropertyResourceMap(String bundleBaseName,
301: File propertyResourceFile) throws IOException {
302: // System.err.println("FILE CREATED");
303: this .setFileObject(FileUtil
304: .toFileObject(propertyResourceFile));
305: this .setBundleBaseName(bundleBaseName);
306: this .fileChangeListener = new FileChangeAdapter() {
307: public void fileRenamed(FileRenameEvent fileRenameEvent) {
308: // System.err.println("FILE RENAMED");
309: setFileObject(fileRenameEvent.getFile());
310: LoadBundleVariableResolver.this
311: .clearPropertyResourceMap(getBundleBaseName());
312: }
313:
314: public void fileDeleted(FileEvent fileEvent) {
315: // System.err.println("FILE DELETED");
316: setFileObject(fileEvent.getFile());
317: LoadBundleVariableResolver.this
318: .clearPropertyResourceMap(getBundleBaseName());
319: }
320:
321: public void fileChanged(FileEvent fileEvent) {
322: // System.err.println("FILE CHANGED");
323: setFileObject(fileEvent.getFile());
324: try {
325: update();
326: } catch (IOException e) {
327: e.printStackTrace();
328: }
329: }
330: };
331: FileObject fileObject = this .getFileObject();
332: fileObject.addFileChangeListener(this .fileChangeListener);
333: update();
334: }
335:
336: public void clear() {
337: // System.err.println("REMOVING LISTENER FOR " + this.getBundleBaseName());
338: FileObject fileObject = this .getFileObject();
339: fileObject
340: .removeFileChangeListener(this .fileChangeListener);
341: super .clear();
342: }
343:
344: private void update() throws IOException {
345: File file = FileUtil.toFile(this .getFileObject());
346: ResourceBundle resourceBundle = new PropertyResourceBundle(
347: new FileInputStream(file));
348: Enumeration<String> keysEnumeration = resourceBundle
349: .getKeys();
350: super .clear();
351: while (keysEnumeration.hasMoreElements()) {
352: String key = keysEnumeration.nextElement();
353: this .put(key, resourceBundle.getString(key));
354: }
355: resourceBundle = null;
356: }
357:
358: private String bundleBaseName;
359:
360: String getBundleBaseName() {
361: return this .bundleBaseName;
362: }
363:
364: void setBundleBaseName(String bundleBaseName) {
365: this .bundleBaseName = bundleBaseName;
366: }
367:
368: private FileObject fileObject;
369:
370: public FileObject getFileObject() {
371: return this .fileObject;
372: }
373:
374: public void setFileObject(FileObject fileObject) {
375: this .fileObject = fileObject;
376: }
377:
378: public Object get(Object key) {
379: Object value = super .get(key);
380: if (value == null && key instanceof String)
381: value = "??? " + (String) key + " ???";
382: return value;
383: }
384:
385: }
386:
387: }
|