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.spi.project.support.ant;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeListener;
046: import java.io.File;
047: import java.lang.ref.Reference;
048: import java.lang.ref.WeakReference;
049: import java.util.Arrays;
050: import java.util.Map;
051: import java.util.WeakHashMap;
052: import java.util.logging.Level;
053: import java.util.logging.Logger;
054: import javax.swing.event.ChangeListener;
055: import org.netbeans.api.queries.FileBuiltQuery;
056: import org.netbeans.modules.project.ant.FileChangeSupport;
057: import org.netbeans.modules.project.ant.FileChangeSupportEvent;
058: import org.netbeans.modules.project.ant.FileChangeSupportListener;
059: import org.netbeans.spi.queries.FileBuiltQueryImplementation;
060: import org.openide.ErrorManager;
061: import org.openide.filesystems.FileAttributeEvent;
062: import org.openide.filesystems.FileChangeListener;
063: import org.openide.filesystems.FileEvent;
064: import org.openide.filesystems.FileObject;
065: import org.openide.filesystems.FileRenameEvent;
066: import org.openide.filesystems.FileUtil;
067: import org.openide.loaders.DataObject;
068: import org.openide.loaders.DataObjectNotFoundException;
069: import org.openide.util.ChangeSupport;
070: import org.openide.util.RequestProcessor;
071: import org.openide.util.Utilities;
072: import org.openide.util.WeakListeners;
073:
074: /**
075: * Simple file built query based on glob patterns.
076: * @see AntProjectHelper#createGlobFileBuiltQuery
077: * @author Jesse Glick
078: */
079: final class GlobFileBuiltQuery implements FileBuiltQueryImplementation {
080:
081: private static final ErrorManager err = ErrorManager
082: .getDefault()
083: .getInstance(
084: "org.netbeans.spi.project.support.ant.GlobFileBuiltQuery"); // NOI18N
085:
086: private final AntProjectHelper helper;
087: private final PropertyEvaluator eval;
088: private final String[] fromPrefixes;
089: private final String[] fromSuffixes;
090: private final String[] toPrefixes;
091: private final String[] toSuffixes;
092: private static final Reference<StatusImpl> NONE = new WeakReference<StatusImpl>(
093: null);
094: private final Map<FileObject, Reference<StatusImpl>> statuses = new WeakHashMap<FileObject, Reference<StatusImpl>>();
095:
096: /**
097: * Create a new query implementation based on an Ant-based project.
098: * @see AntProjectHelper#createGlobFileBuiltQuery
099: */
100: public GlobFileBuiltQuery(AntProjectHelper helper,
101: PropertyEvaluator eval, String[] from, String[] to)
102: throws IllegalArgumentException {
103: this .helper = helper;
104: this .eval = eval;
105: int l = from.length;
106: if (to.length != l) {
107: throw new IllegalArgumentException("Non-matching lengths"); // NOI18N
108: }
109: fromPrefixes = new String[l];
110: fromSuffixes = new String[l];
111: toPrefixes = new String[l];
112: toSuffixes = new String[l];
113: for (int i = 0; i < l; i++) {
114: int idx = from[i].indexOf('*');
115: if (idx == -1 || idx != from[i].lastIndexOf('*')) {
116: throw new IllegalArgumentException(
117: "Zero or multiple asterisks in " + from[i]); // NOI18N
118: }
119: fromPrefixes[i] = from[i].substring(0, idx);
120: fromSuffixes[i] = from[i].substring(idx + 1);
121: idx = to[i].indexOf('*');
122: if (idx == -1 || idx != to[i].lastIndexOf('*')) {
123: throw new IllegalArgumentException(
124: "Zero or multiple asterisks in " + to[i]); // NOI18N
125: }
126: toPrefixes[i] = to[i].substring(0, idx);
127: toSuffixes[i] = to[i].substring(idx + 1);
128: // XXX check that none of the pieces contain two slashes in a row, and
129: // the path does not start with or end with a slash, etc.
130: }
131: // XXX add properties listener to evaluator... if anything changes, refresh all
132: // status objects and clear the status cache; can then also keep a cache of
133: // evaluated path prefixes & suffixes
134: }
135:
136: public synchronized FileBuiltQuery.Status getStatus(FileObject file) {
137: Reference<StatusImpl> r = statuses.get(file);
138: if (r == NONE) {
139: return null;
140: }
141: StatusImpl status = (r != null) ? r.get() : null;
142: if (status == null) {
143: status = createStatus(file);
144: if (status != null) {
145: statuses.put(file,
146: new WeakReference<StatusImpl>(status));
147: } else {
148: statuses.put(file, NONE);
149: }
150: }
151: return status;
152: }
153:
154: private File findTarget(FileObject file) {
155: File sourceF = FileUtil.toFile(file);
156: if (sourceF == null) {
157: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
158: err.log("Not a disk file: " + file);
159: }
160: return null;
161: }
162: String source = sourceF.getAbsolutePath();
163: for (int i = 0; i < fromPrefixes.length; i++) {
164: String prefixEval = eval.evaluate(fromPrefixes[i]);
165: if (prefixEval == null) {
166: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
167: err.log(fromPrefixes[i] + " evaluates to null");
168: }
169: continue;
170: }
171: String suffixEval = eval.evaluate(fromSuffixes[i]);
172: if (suffixEval == null) {
173: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
174: err.log(fromSuffixes[i] + " evaluates to null");
175: }
176: continue;
177: }
178: boolean endsWithSlash = prefixEval.endsWith("/"); // NOI18N
179: String prefixF = helper.resolveFile(prefixEval)
180: .getAbsolutePath();
181: if (endsWithSlash && !prefixF.endsWith(File.separator)) {
182: prefixF += File.separatorChar;
183: }
184: if (!source.startsWith(prefixF)) {
185: continue;
186: }
187: String remainder = source.substring(prefixF.length());
188: if (!remainder.endsWith(suffixEval.replace('/',
189: File.separatorChar))) {
190: continue;
191: }
192: String particular = remainder.substring(0, remainder
193: .length()
194: - suffixEval.length());
195: String toPrefixEval = eval.evaluate(toPrefixes[i]);
196: if (toPrefixEval == null) {
197: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
198: err.log(toPrefixes[i] + " evaluates to null");
199: }
200: continue;
201: }
202: String toSuffixEval = eval.evaluate(toSuffixes[i]);
203: if (toSuffixEval == null) {
204: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
205: err.log(toSuffixes[i] + " evaluates to null");
206: }
207: continue;
208: }
209: File target = helper.resolveFile(toPrefixEval + particular
210: + toSuffixEval);
211: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
212: err.log("Found target for " + source + ": " + target);
213: }
214: return target;
215: }
216: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
217: err.log("No match for path " + source + " among "
218: + Arrays.asList(fromPrefixes) + " "
219: + Arrays.asList(fromSuffixes));
220: }
221: return null;
222: }
223:
224: private StatusImpl createStatus(FileObject file) {
225: File target = findTarget(file);
226: if (target != null) {
227: try {
228: DataObject source = DataObject.find(file);
229:
230: return new StatusImpl(source, file, target);
231: } catch (DataObjectNotFoundException e) {
232: Logger.getLogger(GlobFileBuiltQuery.class.getName())
233: .log(Level.FINE, null, e);
234: return null;
235: }
236: } else {
237: return null;
238: }
239: }
240:
241: private final class StatusImpl implements FileBuiltQuery.Status,
242: PropertyChangeListener/*<DataObject>*/,
243: FileChangeListener, FileChangeSupportListener, Runnable {
244:
245: private final ChangeSupport cs = new ChangeSupport(this );
246: private Boolean built = null;
247: private final DataObject source;
248: private File target;
249:
250: StatusImpl(DataObject source, FileObject sourceFO, File target) {
251: this .source = source;
252: this .source.addPropertyChangeListener(WeakListeners
253: .propertyChange(this , this .source));
254: sourceFO.addFileChangeListener(FileUtil
255: .weakFileChangeListener(this , sourceFO));
256: this .target = target;
257: FileChangeSupport.DEFAULT.addListener(this , target);
258: }
259:
260: // Side effect is to update its cache and maybe fire changes.
261: public boolean isBuilt() {
262: boolean doFire = false;
263: boolean b;
264: synchronized (GlobFileBuiltQuery.this ) {
265: b = isReallyBuilt();
266: if (built != null && built.booleanValue() != b) {
267: doFire = true;
268: }
269: built = Boolean.valueOf(b);
270: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
271: err.log("isBuilt: " + b + " from " + this );
272: }
273: }
274: if (doFire) {
275: cs.fireChange();
276: }
277: return b;
278: }
279:
280: private boolean isReallyBuilt() {
281: if (!source.isValid()) {
282: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
283: err.log("invalid: " + this );
284: }
285: return false; // whatever
286: }
287: if (source.isModified()) {
288: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
289: err.log("modified: " + this );
290: }
291: return false;
292: }
293: if (target == null) {
294: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
295: err.log("no target matching " + this );
296: }
297: return false;
298: }
299: long targetTime = target.lastModified();
300: long sourceTime = source.getPrimaryFile().lastModified()
301: .getTime();
302: if (targetTime >= sourceTime) {
303: return true;
304: } else {
305: if (err.isLoggable(ErrorManager.INFORMATIONAL)) {
306: err.log("out of date (target: " + targetTime
307: + " vs. source: " + sourceTime + "): "
308: + this );
309: }
310: return false;
311: }
312: }
313:
314: public void addChangeListener(ChangeListener l) {
315: cs.addChangeListener(l);
316: }
317:
318: public void removeChangeListener(ChangeListener l) {
319: cs.removeChangeListener(l);
320: }
321:
322: private void update() {
323: RequestProcessor.getDefault().post(this );
324: }
325:
326: public void run() {
327: isBuilt();
328: }
329:
330: public void propertyChange(PropertyChangeEvent evt) {
331: assert evt.getSource() instanceof DataObject;
332: if (DataObject.PROP_MODIFIED.equals(evt.getPropertyName())) {
333: update();
334: }
335: }
336:
337: public void fileChanged(FileEvent fe) {
338: update();
339: }
340:
341: public void fileDeleted(FileEvent fe) {
342: update();
343: }
344:
345: public void fileRenamed(FileRenameEvent fe) {
346: File target2 = findTarget(source.getPrimaryFile());
347: if (!Utilities.compareObjects(target, target2)) {
348: // #45694: source file moved, recalculate target.
349: if (target != null) {
350: FileChangeSupport.DEFAULT.removeListener(this ,
351: target);
352: }
353: if (target2 != null) {
354: FileChangeSupport.DEFAULT
355: .addListener(this , target2);
356: }
357: target = target2;
358: }
359: update();
360: }
361:
362: public void fileDataCreated(FileEvent fe) {
363: // ignore
364: }
365:
366: public void fileFolderCreated(FileEvent fe) {
367: // ignore
368: }
369:
370: public void fileAttributeChanged(FileAttributeEvent fe) {
371: // ignore
372: }
373:
374: public void fileCreated(FileChangeSupportEvent event) {
375: update();
376: }
377:
378: public void fileDeleted(FileChangeSupportEvent event) {
379: update();
380: }
381:
382: public void fileModified(FileChangeSupportEvent event) {
383: update();
384: }
385:
386: public String toString() {
387: return "GFBQ.StatusImpl[" + source.getPrimaryFile()
388: + " -> " + target + "]"; // NOI18N
389: }
390:
391: }
392:
393: }
|