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:
042: package org.apache.tools.ant.module.run;
043:
044: import java.awt.event.ActionEvent;
045: import java.io.File;
046: import java.io.IOException;
047: import java.io.InputStream;
048: import java.io.OutputStream;
049: import java.util.Arrays;
050: import java.util.HashMap;
051: import java.util.HashSet;
052: import java.util.Iterator;
053: import java.util.List;
054: import java.util.Map;
055: import java.util.Set;
056: import java.util.WeakHashMap;
057: import java.util.logging.Level;
058: import java.util.logging.Logger;
059: import javax.swing.AbstractAction;
060: import javax.swing.Action;
061: import javax.swing.ImageIcon;
062: import org.apache.tools.ant.module.AntModule;
063: import org.apache.tools.ant.module.AntSettings;
064: import org.apache.tools.ant.module.api.AntProjectCookie;
065: import org.apache.tools.ant.module.bridge.AntBridge;
066: import org.netbeans.api.progress.ProgressHandle;
067: import org.netbeans.api.progress.ProgressHandleFactory;
068: import org.openide.ErrorManager;
069: import org.openide.LifecycleManager;
070: import org.openide.awt.Actions;
071: import org.openide.execution.ExecutionEngine;
072: import org.openide.execution.ExecutorTask;
073: import org.openide.filesystems.FileAttributeEvent;
074: import org.openide.filesystems.FileChangeListener;
075: import org.openide.filesystems.FileEvent;
076: import org.openide.filesystems.FileObject;
077: import org.openide.filesystems.FileRenameEvent;
078: import org.openide.filesystems.FileUtil;
079: import org.openide.util.Cancellable;
080: import org.openide.util.NbBundle;
081: import org.openide.util.RequestProcessor;
082: import org.openide.util.io.ReaderInputStream;
083: import org.openide.windows.IOProvider;
084: import org.openide.windows.InputOutput;
085: import org.openide.windows.OutputWriter;
086: import org.w3c.dom.Element;
087:
088: /** Executes an Ant Target asynchronously in the IDE.
089: */
090: public final class TargetExecutor implements Runnable {
091:
092: /**
093: * All tabs which were used for some process which has now ended.
094: * These are closed when you start a fresh process.
095: * Map from tab to tab display name.
096: * @see "#43001"
097: */
098: private static final Map<InputOutput, String> freeTabs = new WeakHashMap<InputOutput, String>();
099:
100: /**
101: * Display names of currently active processes.
102: */
103: private static final Set<String> activeDisplayNames = new HashSet<String>();
104:
105: private AntProjectCookie pcookie;
106: private InputOutput io;
107: private OutputStream outputStream;
108: private boolean ok = false;
109: private int verbosity = AntSettings.getVerbosity();
110: private Map<String, String> properties = AntSettings
111: .getProperties();
112: private List<String> targetNames;
113: /** used for the tab etc. */
114: private String displayName;
115:
116: /** targets may be null to indicate default target */
117: public TargetExecutor(AntProjectCookie pcookie, String[] targets) {
118: this .pcookie = pcookie;
119: targetNames = ((targets == null) ? null : Arrays
120: .asList(targets));
121: }
122:
123: public void setVerbosity(int v) {
124: verbosity = v;
125: }
126:
127: public synchronized void setProperties(Map<String, String> p) {
128: properties = new HashMap<String, String>(p);
129: }
130:
131: static String getProcessDisplayName(AntProjectCookie pcookie,
132: List<String> targetNames) {
133: Element projel = pcookie.getProjectElement();
134: String projectName;
135: if (projel != null) {
136: // remove & if available.
137: projectName = Actions.cutAmpersand(projel
138: .getAttribute("name")); // NOI18N
139: } else {
140: projectName = NbBundle.getMessage(TargetExecutor.class,
141: "LBL_unparseable_proj_name");
142: }
143: String fileName;
144: if (pcookie.getFileObject() != null) {
145: fileName = pcookie.getFileObject().getNameExt();
146: } else if (pcookie.getFile() != null) {
147: fileName = pcookie.getFile().getName();
148: } else {
149: fileName = ""; // last resort for #84874
150: }
151: if (projectName.equals("")) { // NOI18N
152: // No name="..." given, so try the file name instead.
153: projectName = fileName;
154: }
155: if (targetNames != null) {
156: StringBuffer targetList = new StringBuffer();
157: Iterator<String> it = targetNames.iterator();
158: if (it.hasNext()) {
159: targetList.append(it.next());
160: }
161: while (it.hasNext()) {
162: targetList.append(NbBundle.getMessage(
163: TargetExecutor.class, "SEP_output_target"));
164: targetList.append(it.next());
165: }
166: return NbBundle.getMessage(TargetExecutor.class,
167: "TITLE_output_target", projectName, fileName,
168: targetList);
169: } else {
170: return NbBundle.getMessage(TargetExecutor.class,
171: "TITLE_output_notarget", projectName, fileName);
172: }
173: }
174:
175: private static final Map<InputOutput, StopAction> stopActions = new HashMap<InputOutput, StopAction>();
176: private static final Map<InputOutput, RerunAction> rerunActions = new HashMap<InputOutput, RerunAction>();
177:
178: private static final class StopAction extends AbstractAction {
179:
180: public Thread t;
181:
182: public StopAction() {
183: setEnabled(false); // initially, until ready
184: }
185:
186: @Override
187: public Object getValue(String key) {
188: if (key.equals(Action.SMALL_ICON)) {
189: return new ImageIcon(
190: TargetExecutor.class
191: .getResource("/org/apache/tools/ant/module/resources/stop.png"));
192: } else if (key.equals(Action.SHORT_DESCRIPTION)) {
193: return NbBundle.getMessage(TargetExecutor.class,
194: "TargetExecutor.StopAction.stop");
195: } else {
196: return super .getValue(key);
197: }
198: }
199:
200: public void actionPerformed(ActionEvent e) {
201: setEnabled(false); // discourage repeated clicking
202: if (t != null) { // #84688
203: stopProcess(t);
204: }
205: }
206:
207: }
208:
209: private static final class RerunAction extends AbstractAction
210: implements FileChangeListener {
211:
212: private final AntProjectCookie pcookie;
213: private final List<String> targetNames;
214: private final int verbosity;
215: private final Map<String, String> properties;
216:
217: public RerunAction(TargetExecutor prototype) {
218: pcookie = prototype.pcookie;
219: targetNames = prototype.targetNames;
220: verbosity = prototype.verbosity;
221: properties = prototype.properties;
222: setEnabled(false); // initially, until ready
223: FileObject script = pcookie.getFileObject();
224: if (script != null) {
225: script.addFileChangeListener(FileUtil
226: .weakFileChangeListener(this , script));
227: }
228: }
229:
230: @Override
231: public Object getValue(String key) {
232: if (key.equals(Action.SMALL_ICON)) {
233: return new ImageIcon(
234: TargetExecutor.class
235: .getResource("/org/apache/tools/ant/module/resources/rerun.png"));
236: } else if (key.equals(Action.SHORT_DESCRIPTION)) {
237: return NbBundle.getMessage(TargetExecutor.class,
238: "TargetExecutor.RerunAction.rerun");
239: } else {
240: return super .getValue(key);
241: }
242: }
243:
244: public void actionPerformed(ActionEvent e) {
245: setEnabled(false);
246: try {
247: TargetExecutor exec = new TargetExecutor(
248: pcookie,
249: targetNames != null ? targetNames
250: .toArray(new String[targetNames.size()])
251: : null);
252: exec.setVerbosity(verbosity);
253: exec.setProperties(properties);
254: exec.execute();
255: } catch (IOException x) {
256: Logger.getLogger(TargetExecutor.class.getName()).log(
257: Level.INFO, null, x);
258: }
259: }
260:
261: public void fileDeleted(FileEvent fe) {
262: firePropertyChange("enabled", null, false); // NOI18N
263: }
264:
265: public void fileFolderCreated(FileEvent fe) {
266: }
267:
268: public void fileDataCreated(FileEvent fe) {
269: }
270:
271: public void fileChanged(FileEvent fe) {
272: }
273:
274: public void fileRenamed(FileRenameEvent fe) {
275: }
276:
277: public void fileAttributeChanged(FileAttributeEvent fe) {
278: }
279:
280: public boolean isEnabled() {
281: // #84874: should be disabled in case the original Ant script is now gone.
282: return super .isEnabled() && pcookie.getFileObject() != null
283: && pcookie.getFileObject().isValid();
284: }
285:
286: }
287:
288: /**
289: * Actually start the process.
290: */
291: public ExecutorTask execute() throws IOException {
292: String dn = getProcessDisplayName(pcookie, targetNames);
293: if (activeDisplayNames.contains(dn)) {
294: // Uniquify: "prj (targ) #2", "prj (targ) #3", etc.
295: int i = 2;
296: String testdn;
297: do {
298: testdn = NbBundle.getMessage(TargetExecutor.class,
299: "TargetExecutor.uniquified", dn, i++);
300: } while (activeDisplayNames.contains(testdn));
301: dn = testdn;
302: }
303: assert !activeDisplayNames.contains(dn);
304: displayName = dn;
305: activeDisplayNames.add(displayName);
306:
307: final ExecutorTask task;
308: synchronized (this ) {
309: // OutputWindow
310: if (AntSettings.getAutoCloseTabs()) { // #47753
311: synchronized (freeTabs) {
312: for (Map.Entry<InputOutput, String> entry : freeTabs
313: .entrySet()) {
314: InputOutput free = entry.getKey();
315: String freeName = entry.getValue();
316: if (io == null && freeName.equals(displayName)) {
317: // Reuse it.
318: io = free;
319: io.getOut().reset();
320: // Apparently useless and just prints warning: io.getErr().reset();
321: // useless: io.flushReader();
322: } else {
323: // Discard it.
324: free.closeInputOutput();
325: stopActions.remove(free);
326: rerunActions.remove(free);
327: }
328: }
329: freeTabs.clear();
330: }
331: }
332: if (io == null) {
333: StopAction sa = new StopAction();
334: RerunAction ra = new RerunAction(this );
335: io = IOProvider.getDefault().getIO(displayName,
336: new Action[] { ra, sa });
337: stopActions.put(io, sa);
338: rerunActions.put(io, ra);
339: }
340: task = ExecutionEngine.getDefault().execute(null, this ,
341: InputOutput.NULL);
342: }
343: WrapperExecutorTask wrapper = new WrapperExecutorTask(task, io);
344: RequestProcessor.getDefault().post(wrapper);
345: return wrapper;
346: }
347:
348: public ExecutorTask execute(OutputStream outputStream)
349: throws IOException {
350: this .outputStream = outputStream;
351: ExecutorTask task = ExecutionEngine.getDefault().execute(null,
352: this , InputOutput.NULL);
353: return new WrapperExecutorTask(task, null);
354: }
355:
356: private class WrapperExecutorTask extends ExecutorTask {
357: private ExecutorTask task;
358: private InputOutput io;
359:
360: public WrapperExecutorTask(ExecutorTask task, InputOutput io) {
361: super (new WrapperRunnable(task));
362: this .task = task;
363: this .io = io;
364: }
365:
366: @Override
367: public void stop() {
368: StopAction sa = stopActions.get(io);
369: if (sa != null) {
370: sa.actionPerformed(null);
371: } else { // just in case
372: task.stop();
373: }
374: }
375:
376: @Override
377: public int result() {
378: return task.result() + (ok ? 0 : 1);
379: }
380:
381: @Override
382: public InputOutput getInputOutput() {
383: return io;
384: }
385: }
386:
387: private static class WrapperRunnable implements Runnable {
388: private final ExecutorTask task;
389:
390: public WrapperRunnable(ExecutorTask task) {
391: this .task = task;
392: }
393:
394: public void run() {
395: task.waitFinished();
396: }
397: }
398:
399: /** Call execute(), not this method directly!
400: */
401: synchronized public void run() {
402: final Thread[] this Process = new Thread[1];
403: final StopAction sa = stopActions.get(io);
404: assert sa != null;
405: RerunAction ra = rerunActions.get(io);
406: assert ra != null;
407: try {
408:
409: final boolean[] displayed = new boolean[] { AntSettings
410: .getAlwaysShowOutput() };
411:
412: if (outputStream == null) {
413: if (displayed[0]) {
414: io.select();
415: }
416: }
417:
418: if (AntSettings.getSaveAll()) {
419: LifecycleManager.getDefault().saveAll();
420: }
421:
422: OutputWriter out;
423: OutputWriter err;
424: if (outputStream == null) {
425: out = io.getOut();
426: err = io.getErr();
427: } else {
428: throw new RuntimeException(
429: "XXX No support for outputStream currently!"); // NOI18N
430: }
431:
432: File buildFile = pcookie.getFile();
433: if (buildFile == null) {
434: err.println(NbBundle.getMessage(TargetExecutor.class,
435: "EXC_non_local_proj_file"));
436: return;
437: }
438:
439: LastTargetExecuted.record(buildFile, verbosity,
440: targetNames != null ? targetNames
441: .toArray(new String[targetNames.size()])
442: : null, properties);
443:
444: // Don't hog the CPU, the build might take a while:
445: Thread.currentThread().setPriority(
446: (Thread.MIN_PRIORITY + Thread.NORM_PRIORITY) / 2);
447:
448: final Runnable interestingOutputCallback = new Runnable() {
449: public void run() {
450: // #58513: display output now.
451: if (!displayed[0]) {
452: displayed[0] = true;
453: io.select();
454: }
455: }
456: };
457:
458: InputStream in = null;
459: if (outputStream == null) { // #43043
460: try {
461: in = new ReaderInputStream(io.getIn()) {
462: // Show the output when an input field is displayed, if it hasn't already.
463: @Override
464: public int read() throws IOException {
465: interestingOutputCallback.run();
466: return super .read();
467: }
468:
469: @Override
470: public int read(byte[] b) throws IOException {
471: interestingOutputCallback.run();
472: return super .read(b);
473: }
474:
475: @Override
476: public int read(byte[] b, int off, int len)
477: throws IOException {
478: interestingOutputCallback.run();
479: return super .read(b, off, len);
480: }
481:
482: @Override
483: public long skip(long n) throws IOException {
484: interestingOutputCallback.run();
485: return super .skip(n);
486: }
487: };
488: } catch (IOException e) {
489: AntModule.err.notify(ErrorManager.INFORMATIONAL, e);
490: }
491: }
492:
493: this Process[0] = Thread.currentThread();
494: StopBuildingAction.registerProcess(this Process[0],
495: displayName);
496: sa.t = this Process[0];
497: // #58513, #87801: register a progress handle for the task too.
498: ProgressHandle handle = ProgressHandleFactory.createHandle(
499: displayName, new Cancellable() {
500: public boolean cancel() {
501: sa.actionPerformed(null);
502: return true;
503: }
504: }, new AbstractAction() {
505: public void actionPerformed(ActionEvent e) {
506: io.select();
507: }
508: });
509: handle.setInitialDelay(0); // #92436
510: handle.start();
511: sa.setEnabled(true);
512: ra.setEnabled(false);
513: ok = AntBridge.getInterface().run(buildFile, targetNames,
514: in, out, err, properties, verbosity, displayName,
515: interestingOutputCallback, handle);
516:
517: } finally {
518: if (io != null) {
519: synchronized (freeTabs) {
520: freeTabs.put(io, displayName);
521: }
522: }
523: if (this Process[0] != null) {
524: StopBuildingAction.unregisterProcess(this Process[0]);
525: }
526: sa.t = null;
527: sa.setEnabled(false);
528: ra.setEnabled(true);
529: activeDisplayNames.remove(displayName);
530: }
531: }
532:
533: /** Try to stop a build. */
534: static void stopProcess(Thread t) {
535: AntBridge.getInterface().stop(t);
536: }
537:
538: }
|