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-2006 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:
042: package org.netbeans.modules.apisupport.project.universe;
043:
044: import java.beans.PropertyChangeListener;
045: import java.beans.PropertyChangeSupport;
046: import java.io.File;
047: import java.io.IOException;
048: import java.io.InputStream;
049: import java.text.BreakIterator;
050: import java.util.ArrayList;
051: import java.util.List;
052: import java.util.Locale;
053: import org.netbeans.api.project.ProjectInformation;
054: import org.netbeans.modules.apisupport.project.Util;
055: import org.netbeans.spi.project.support.ant.EditableProperties;
056: import org.openide.ErrorManager;
057: import org.openide.filesystems.FileChangeAdapter;
058: import org.openide.filesystems.FileEvent;
059: import org.openide.filesystems.FileObject;
060: import org.openide.filesystems.FileUtil;
061: import org.openide.util.Utilities;
062:
063: /**
064: * Represents localized information for a NetBeans module usually loaded from a
065: * <em>Bundle.properties</em> specified in a module's manifest. It is actaully
066: * back up by {@link EditableProperties} so any changes to this instance will
067: * behave exactly as specified in {@link EditableProperties} javadoc during
068: * storing.
069: *
070: * @author Martin Krauskopf
071: */
072: public final class LocalizedBundleInfo {
073:
074: public static final String NAME = "OpenIDE-Module-Name"; // NOI18N
075: public static final String DISPLAY_CATEGORY = "OpenIDE-Module-Display-Category"; // NOI18N
076: public static final String SHORT_DESCRIPTION = "OpenIDE-Module-Short-Description"; // NOI18N
077: public static final String LONG_DESCRIPTION = "OpenIDE-Module-Long-Description"; // NOI18N
078:
079: static final LocalizedBundleInfo EMPTY = new LocalizedBundleInfo(
080: new EditableProperties[] { new EditableProperties(true) });
081:
082: private final EditableProperties[] props;
083: private final File[] paths;
084:
085: private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(
086: this );
087:
088: /** Whether this instance was modified since it was loaded or saved. */
089: private boolean modified;
090:
091: /**
092: * Returns instances initialized by data in the given {@link FileObject}.
093: * Note that instances created by this factory method are automatically
094: * storable (i.e. {@link #store} and {@link #reload} can be called) if the
095: * given object represents a regular {@link java.io.File}.
096: * @param bundleFO {@link FileObject} representing localizing bundle.
097: * Usually <em>bundle.properties</em> or its branded version.
098: * Given as an array so you can pass multiple locale variants (most specific last).
099: * @return instance representing data in the given bundle
100: */
101: public static LocalizedBundleInfo load(FileObject[] bundleFOs)
102: throws IOException {
103: return new LocalizedBundleInfo(bundleFOs);
104: }
105:
106: /**
107: * Returns instances initialized by data in the given {@link FileObject}.
108: * Instances created by this factory method are not storable (i.e. {@link
109: * #store} and {@link #reload} cannot be called) until the {@link #setPath}
110: * is called upon this object.
111: * @param bundleIS input stream representing localizing bundle. Usually
112: * <em>bundle.properties</em> or its branded version.
113: * @return instance representing data in the given bundle
114: */
115: public static LocalizedBundleInfo load(InputStream[] bundleISs)
116: throws IOException {
117: EditableProperties[] props = new EditableProperties[bundleISs.length];
118: for (int i = 0; i < props.length; i++) {
119: props[i] = new EditableProperties(true);
120: props[i].load(bundleISs[i]);
121: }
122: return new LocalizedBundleInfo(props);
123: }
124:
125: /** Use factory method instead. */
126: private LocalizedBundleInfo(EditableProperties[] props) {
127: this .props = props;
128: paths = new File[props.length];
129: }
130:
131: /** Use factory method instead. */
132: private LocalizedBundleInfo(FileObject[] bundleFOs)
133: throws IOException {
134: if (bundleFOs == null || bundleFOs.length == 0) {
135: throw new IllegalArgumentException();
136: }
137: props = new EditableProperties[bundleFOs.length];
138: paths = new File[bundleFOs.length];
139: for (int i = 0; i < bundleFOs.length; i++) {
140: InputStream bundleIS = bundleFOs[i].getInputStream();
141: try {
142: props[i] = new EditableProperties(true);
143: props[i].load(bundleIS);
144: } finally {
145: bundleIS.close();
146: }
147: paths[i] = FileUtil.toFile(bundleFOs[i]);
148: bundleFOs[i].addFileChangeListener(new FileChangeAdapter() {
149: public void fileChanged(FileEvent fe) {
150: try {
151: LocalizedBundleInfo.this .reload();
152: } catch (IOException e) {
153: Util.err.log(ErrorManager.WARNING,
154: "Cannot reload localized bundle info " + // NOI18N
155: FileUtil.getFileDisplayName(fe
156: .getFile()));
157: }
158: }
159: });
160: }
161: }
162:
163: /**
164: * Reload data of this localizing bundle info from the file represented by
165: * previously set path. Note that this instance already listens to the
166: * bundle properties file (or files if localized). So it just gives a
167: * possibility to force reloading in the case the properties were e.g.
168: * changed outside of IDE or using {@link java.io.File}.
169: */
170: public void reload() throws IOException {
171: String oldDisplayName = getDisplayName();
172: for (int i = 0; i < paths.length; i++) {
173: if (paths[i] == null) {
174: props[i] = new EditableProperties(true);
175: continue;
176: }
177: FileObject bundleFO = FileUtil.toFileObject(paths[i]);
178: props[i] = bundleFO != null ? Util.loadProperties(bundleFO)
179: : new EditableProperties(true);
180: }
181: modified = false;
182: firePropertyChange(ProjectInformation.PROP_DISPLAY_NAME,
183: oldDisplayName, getDisplayName());
184: }
185:
186: /**
187: * Reload this localizing bundle from the file specified by previously set
188: * path.
189: */
190: public void store() throws IOException {
191: for (int i = 0; i < paths.length; i++) {
192: if (paths[i] == null) {
193: continue;
194: }
195: FileObject bundleFO = FileUtil.toFileObject(paths[i]);
196: if (bundleFO == null) {
197: return;
198: }
199: Util.storeProperties(bundleFO, props[i]);
200: }
201: modified = false;
202: }
203:
204: /**
205: * Converts entries this instance represents into {@link
206: * EditableProperties}.
207: */
208: public EditableProperties toEditableProperties() {
209: return props[0];
210: }
211:
212: private String getProperty(String key) {
213: for (int i = props.length - 1; i >= 0; i--) {
214: if (props[i].containsKey(key)) {
215: return props[i].getProperty(key);
216: }
217: }
218: return null;
219: }
220:
221: /**
222: * Tells whether this instance was modified since it was loaded or saved.
223: * I.e. if it needs to be saved or not.
224: */
225: public boolean isModified() {
226: return modified;
227: }
228:
229: public String getDisplayName() {
230: return getProperty(NAME);
231: }
232:
233: public void setDisplayName(String name) {
234: String oldDisplayName = getDisplayName();
235: this .setProperty(NAME, name, false);
236: firePropertyChange(ProjectInformation.PROP_DISPLAY_NAME,
237: oldDisplayName, getDisplayName());
238: }
239:
240: public String getCategory() {
241: return getProperty(DISPLAY_CATEGORY);
242: }
243:
244: public void setCategory(String category) {
245: this .setProperty(DISPLAY_CATEGORY, category, false);
246: }
247:
248: public String getShortDescription() {
249: return getProperty(SHORT_DESCRIPTION);
250: }
251:
252: public void setShortDescription(String shortDescription) {
253: this .setProperty(SHORT_DESCRIPTION, shortDescription, false);
254: }
255:
256: public String getLongDescription() {
257: return getProperty(LONG_DESCRIPTION);
258: }
259:
260: public void setLongDescription(String longDescription) {
261: this .setProperty(LONG_DESCRIPTION, longDescription, true);
262: }
263:
264: public File[] getPaths() {
265: return paths;
266: }
267:
268: private void setProperty(String name, String value, boolean split) {
269: if (Utilities.compareObjects(value, getProperty(name))) {
270: return;
271: }
272: modified = true;
273: if (value != null) {
274: value = value.trim();
275: }
276: if (value != null && value.length() > 0) {
277: if (split) {
278: props[props.length - 1].setProperty(name,
279: splitBySentence(value));
280: } else {
281: props[props.length - 1].setProperty(name, value);
282: }
283: } else {
284: for (int i = 0; i < props.length; i++) {
285: props[i].remove(name);
286: }
287: }
288: }
289:
290: private static String[] splitBySentence(String text) {
291: List<String> sentences = new ArrayList<String>();
292: // Use Locale.US since the customizer is setting the default (US) locale text only:
293: BreakIterator it = BreakIterator.getSentenceInstance(Locale.US);
294: it.setText(text);
295: int start = it.first();
296: int end;
297: while ((end = it.next()) != BreakIterator.DONE) {
298: sentences.add(text.substring(start, end));
299: start = end;
300: }
301: return sentences.toArray(new String[sentences.size()]);
302: }
303:
304: public void addPropertyChangeListener(PropertyChangeListener pchl) {
305: changeSupport.addPropertyChangeListener(pchl);
306: }
307:
308: public void removePropertyChangeListener(PropertyChangeListener pchl) {
309: changeSupport.removePropertyChangeListener(pchl);
310: }
311:
312: private void firePropertyChange(String propName, Object oldValue,
313: Object newValue) {
314: changeSupport.firePropertyChange(propName, oldValue, newValue);
315: }
316:
317: public String toString() {
318: return "LocalizedBundleInfo[" + getDisplayName() + "; " + // NOI18N
319: getCategory() + "; " + // NOI18N
320: getShortDescription() + "; " + // NOI18N
321: getLongDescription() + "]"; // NOI18N
322: }
323:
324: public static interface Provider {
325: /** May return <code>null</code>. */
326: LocalizedBundleInfo getLocalizedBundleInfo();
327: }
328:
329: }
|