1 19 20 package org.netbeans.modules.ruby.spi.project.support.rake; 21 22 import java.io.ByteArrayOutputStream ; 23 import java.io.File ; 24 import java.io.IOException ; 25 import java.io.OutputStream ; 26 import java.util.ArrayList ; 27 import java.util.HashSet ; 28 import java.util.Iterator ; 29 import java.util.List ; 30 import java.util.Set ; 31 import javax.xml.parsers.DocumentBuilder ; 32 import javax.xml.parsers.DocumentBuilderFactory ; 33 import javax.xml.parsers.ParserConfigurationException ; 34 import org.netbeans.api.project.Project; 35 import org.netbeans.api.project.ProjectManager; 36 import org.netbeans.modules.ruby.api.project.rake.RakeArtifact; 37 import org.netbeans.modules.ruby.modules.project.rake.RakeBasedProjectFactorySingleton; 38 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupport; 39 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupportEvent; 40 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupportListener; 41 import org.netbeans.modules.ruby.modules.project.rake.UserQuestionHandler; 42 import org.netbeans.modules.ruby.modules.project.rake.Util; 43 import org.netbeans.spi.project.AuxiliaryConfiguration; 44 import org.netbeans.spi.project.CacheDirectoryProvider; 45 import org.netbeans.spi.project.ProjectState; 46 import org.netbeans.spi.queries.FileBuiltQueryImplementation; 47 import org.netbeans.spi.queries.SharabilityQueryImplementation; 48 import org.openide.ErrorManager; 49 import org.openide.filesystems.FileLock; 50 import org.openide.filesystems.FileObject; 51 import org.openide.filesystems.FileSystem; 52 import org.openide.filesystems.FileUtil; 53 import org.openide.util.Mutex; 54 import org.openide.util.MutexException; 55 import org.openide.util.RequestProcessor; 56 import org.openide.util.UserQuestionException; 57 import org.openide.xml.XMLUtil; 58 import org.w3c.dom.Document ; 59 import org.w3c.dom.Element ; 60 import org.w3c.dom.Node ; 61 import org.w3c.dom.NodeList ; 62 import org.xml.sax.InputSource ; 63 import org.xml.sax.SAXException ; 64 65 69 public final class RakeProjectHelper { 70 71 74 public static final String PROJECT_PROPERTIES_PATH = "nbproject/project.properties"; 76 79 public static final String PRIVATE_PROPERTIES_PATH = "nbproject/private/private.properties"; 81 84 public static final String PROJECT_XML_PATH = RakeBasedProjectFactorySingleton.PROJECT_XML_PATH; 85 86 89 public static final String PRIVATE_XML_PATH = "nbproject/private/private.xml"; 91 94 static final String PROJECT_NS = RakeBasedProjectFactorySingleton.PROJECT_NS; 95 96 99 static final String PRIVATE_NS = "http://www.netbeans.org/ns/project-private/1"; 101 static { 102 RakeBasedProjectFactorySingleton.HELPER_CALLBACK = new RakeBasedProjectFactorySingleton.RakeProjectHelperCallback() { 103 public RakeProjectHelper createHelper(FileObject dir, Document projectXml, ProjectState state, RakeBasedProjectType type) { 104 return new RakeProjectHelper(dir, projectXml, state, type); 105 } 106 public void save(RakeProjectHelper helper) throws IOException { 107 helper.save(); 108 } 109 }; 110 } 111 112 private static final RequestProcessor RP = new RequestProcessor("RakeProjectHelper.RP"); 114 117 private final FileObject dir; 118 119 122 private final ProjectState state; 123 124 127 private final RakeBasedProjectType type; 128 129 133 private Document projectXml; 134 135 139 private Document privateXml; 140 141 147 private final Set <String > modifiedMetadataPaths = new HashSet <String >(); 148 149 153 private final List <RakeProjectListener> listeners = new ArrayList <RakeProjectListener>(); 154 155 158 private final ProjectProperties properties; 159 160 161 private final FileChangeSupportListener fileListener; 162 163 164 private boolean writingXML = false; 165 166 169 private ProjectXmlSavedHook pendingHook; 170 175 private int pendingHookCount; 176 177 180 private RakeProjectHelper(FileObject dir, Document projectXml, ProjectState state, RakeBasedProjectType type) { 181 this.dir = dir; 182 assert dir != null && FileUtil.toFile(dir) != null; 183 this.state = state; 184 assert state != null; 185 this.type = type; 186 assert type != null; 187 this.projectXml = projectXml; 188 assert projectXml != null; 189 properties = new ProjectProperties(this); 190 fileListener = new FileListener(); 191 FileChangeSupport.DEFAULT.addListener(fileListener, resolveFile(PROJECT_XML_PATH)); 192 FileChangeSupport.DEFAULT.addListener(fileListener, resolveFile(PRIVATE_XML_PATH)); 193 } 194 195 198 RakeBasedProjectType getType() { 199 return type; 200 } 201 202 206 private Document getConfigurationXml(boolean shared) { 207 assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess(); 208 assert Thread.holdsLock(modifiedMetadataPaths); 209 Document xml = shared ? projectXml : privateXml; 210 if (xml == null) { 211 String path = shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH; 212 xml = loadXml(path); 213 if (xml == null) { 214 String element = shared ? "project" : "project-private"; String ns = shared ? PROJECT_NS : PRIVATE_NS; 217 xml = XMLUtil.createDocument(element, ns, null, null); 218 if (shared) { 219 Element typeEl = xml.createElementNS(PROJECT_NS, "type"); typeEl.appendChild(xml.createTextNode(getType().getType())); 222 xml.getDocumentElement().appendChild(typeEl); 223 xml.getDocumentElement().appendChild(xml.createElementNS(PROJECT_NS, "configuration")); } 225 } 226 if (shared) { 227 projectXml = xml; 228 } else { 229 privateXml = xml; 230 } 231 } 232 assert xml != null; 233 return xml; 234 } 235 236 240 static boolean QUIETLY_SWALLOW_XML_LOAD_ERRORS = false; 241 242 246 private Document loadXml(String path) { 247 assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess(); 248 assert Thread.holdsLock(modifiedMetadataPaths); 249 FileObject xml = dir.getFileObject(path); 250 if (xml == null || !xml.isData()) { 251 return null; 252 } 253 File f = FileUtil.toFile(xml); 254 assert f != null; 255 try { 256 return XMLUtil.parse(new InputSource (f.toURI().toString()), false, true, Util.defaultErrorHandler(), null); 257 } catch (IOException e) { 258 if (!QUIETLY_SWALLOW_XML_LOAD_ERRORS) { 259 ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e); 260 } 261 } catch (SAXException e) { 262 if (!QUIETLY_SWALLOW_XML_LOAD_ERRORS) { 263 ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e); 264 } 265 } 266 return null; 267 } 268 269 273 private FileLock saveXml(final Document doc, final String path) throws IOException { 274 assert ProjectManager.mutex().isWriteAccess(); 275 assert !writingXML; 276 assert Thread.holdsLock(modifiedMetadataPaths); 277 final FileLock[] _lock = new FileLock[1]; 278 writingXML = true; 279 try { 280 dir.getFileSystem().runAtomicAction(new FileSystem.AtomicAction() { 281 public void run() throws IOException { 282 ByteArrayOutputStream baos = new ByteArrayOutputStream (); 284 XMLUtil.write(doc, baos, "UTF-8"); final byte[] data = baos.toByteArray(); 286 final FileObject xml = FileUtil.createData(dir, path); 287 try { 288 _lock[0] = xml.lock(); OutputStream os = xml.getOutputStream(_lock[0]); 290 try { 291 os.write(data); 292 } finally { 293 os.close(); 294 } 295 } catch (UserQuestionException uqe) { needPendingHook(); 297 UserQuestionHandler.handle(uqe, new UserQuestionHandler.Callback() { 298 public void accepted() { 299 assert !writingXML; 301 writingXML = true; 302 try { 303 FileLock lock = xml.lock(); 304 try { 305 OutputStream os = xml.getOutputStream(lock); 306 try { 307 os.write(data); 308 } finally { 309 os.close(); 310 } 311 } finally { 312 lock.releaseLock(); 313 } 314 maybeCallPendingHook(); 315 } catch (IOException e) { 316 ErrorManager.getDefault().notify(e); 318 reload(); 319 } finally { 320 writingXML = false; 321 } 322 } 323 public void denied() { 324 reload(); 325 } 326 public void error(IOException e) { 327 ErrorManager.getDefault().notify(e); 328 reload(); 329 } 330 private void reload() { 331 if (path.equals(PROJECT_XML_PATH)) { 333 synchronized (modifiedMetadataPaths) { 334 projectXml = null; 335 } 336 } else { 337 assert path.equals(PRIVATE_XML_PATH) : path; 338 synchronized (modifiedMetadataPaths) { 339 privateXml = null; 340 } 341 } 342 fireExternalChange(path); 343 cancelPendingHook(); 344 } 345 }); 346 } 347 } 348 }); 349 } finally { 350 writingXML = false; 351 } 352 return _lock[0]; 353 } 354 355 362 private Element getConfigurationDataRoot(boolean shared) { 363 assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess(); 364 assert Thread.holdsLock(modifiedMetadataPaths); 365 Document doc = getConfigurationXml(shared); 366 if (shared) { 367 Element project = doc.getDocumentElement(); 368 Element config = Util.findElement(project, "configuration", PROJECT_NS); assert config != null; 370 return config; 371 } else { 372 return doc.getDocumentElement(); 373 } 374 } 375 376 381 public void addRakeProjectListener(RakeProjectListener listener) { 382 synchronized (listeners) { 383 listeners.add(listener); 384 } 385 } 386 387 392 public void removeRakeProjectListener(RakeProjectListener listener) { 393 synchronized (listeners) { 394 listeners.remove(listener); 395 } 396 } 397 398 403 void fireExternalChange(final String path) { 404 final Mutex.Action<Void > action = new Mutex.Action<Void >() { 405 public Void run() { 406 fireChange(path, false); 407 return null; 408 } 409 }; 410 if (ProjectManager.mutex().isWriteAccess()) { 411 ProjectManager.mutex().readAccess(action); 413 } else if (ProjectManager.mutex().isReadAccess()) { 414 action.run(); 416 } else { 417 RP.post(new Runnable () { 419 public void run() { 420 ProjectManager.mutex().readAccess(action); 421 } 422 }); 423 } 424 } 425 426 432 private void fireChange(String path, boolean expected) { 433 assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess(); 434 final RakeProjectListener[] _listeners; 435 synchronized (listeners) { 436 if (listeners.isEmpty()) { 437 return; 438 } 439 _listeners = listeners.toArray(new RakeProjectListener[listeners.size()]); 440 } 441 final RakeProjectEvent ev = new RakeProjectEvent(this, path, expected); 442 final boolean xml = path.equals(PROJECT_XML_PATH) || path.equals(PRIVATE_XML_PATH); 443 ProjectManager.mutex().readAccess(new Mutex.Action<Void >() { 444 public Void run() { 445 for (int i = 0; i < _listeners.length; i++) { 446 try { 447 if (xml) { 448 _listeners[i].configurationXmlChanged(ev); 449 } else { 450 _listeners[i].propertiesChanged(ev); 451 } 452 } catch (RuntimeException e) { 453 ErrorManager.getDefault().notify(e); 455 } 456 } 457 return null; 458 } 459 }); 460 } 461 462 465 private void modifying(String path) { 466 assert ProjectManager.mutex().isWriteAccess(); 467 state.markModified(); 468 synchronized (modifiedMetadataPaths) { 469 modifiedMetadataPaths.add(path); 470 } 471 fireChange(path, true); 472 } 473 474 478 public FileObject getProjectDirectory() { 479 return dir; 480 } 481 482 487 public void notifyDeleted() { 488 state.notifyDeleted(); 489 } 490 491 492 496 void markModified() { 497 assert ProjectManager.mutex().isWriteAccess(); 498 state.markModified(); 499 synchronized (modifiedMetadataPaths) { 501 modifiedMetadataPaths.add(PROJECT_XML_PATH); 502 } 503 } 504 505 510 boolean isProjectXmlModified() { 511 assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess(); 512 return modifiedMetadataPaths.contains(PROJECT_XML_PATH); 513 } 514 515 521 private void save() throws IOException { 522 assert ProjectManager.mutex().isWriteAccess(); 523 Set <FileLock> locks = new HashSet <FileLock>(); 524 try { 525 synchronized (modifiedMetadataPaths) { 526 assert !modifiedMetadataPaths.isEmpty(); 527 assert pendingHook == null; 528 if (modifiedMetadataPaths.contains(PROJECT_XML_PATH)) { 529 Project p = RakeBasedProjectFactorySingleton.getProjectFor(this); 531 pendingHook = p.getLookup().lookup(ProjectXmlSavedHook.class); 532 } 534 Iterator it = modifiedMetadataPaths.iterator(); 535 while (it.hasNext()) { 536 String path = (String )it.next(); 537 if (path.equals(PROJECT_XML_PATH)) { 538 assert projectXml != null; 539 locks.add(saveXml(projectXml, path)); 540 } else if (path.equals(PRIVATE_XML_PATH)) { 541 assert privateXml != null; 542 locks.add(saveXml(privateXml, path)); 543 } else { 544 locks.add(properties.write(path)); 547 } 548 it.remove(); 550 } 551 if (pendingHook != null && pendingHookCount == 0) { 552 try { 553 pendingHook.projectXmlSaved(); 554 } catch (IOException e) { 555 modifiedMetadataPaths.add(PROJECT_XML_PATH); 557 throw e; 558 } 559 } 560 } 561 } finally { 562 locks.remove(null); 564 for (FileLock lock : locks) { 565 lock.releaseLock(); 566 } 567 if (pendingHookCount == 0) { 569 pendingHook = null; 570 } 571 } 572 } 573 574 575 void maybeCallPendingHook() { 576 assert pendingHookCount > 0; 578 pendingHookCount--; 579 if (pendingHookCount == 0 && pendingHook != null) { 582 try { 583 ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void >() { 584 public Void run() throws IOException { 585 pendingHook.projectXmlSaved(); 586 return null; 587 } 588 }); 589 } catch (MutexException e) { 590 ErrorManager.getDefault().notify(e); 592 } finally { 593 pendingHook = null; 594 } 595 } 596 } 597 void cancelPendingHook() { 598 assert pendingHookCount > 0; 599 pendingHookCount--; 600 if (pendingHookCount == 0) { 601 pendingHook = null; 602 } 603 } 604 void needPendingHook() { 605 pendingHookCount++; 606 } 607 608 618 public EditableProperties getProperties(final String path) { 619 if (path.equals(RakeProjectHelper.PROJECT_XML_PATH) || path.equals(RakeProjectHelper.PRIVATE_XML_PATH)) { 620 throw new IllegalArgumentException ("Attempt to load properties from a project XML file"); } 622 return ProjectManager.mutex().readAccess(new Mutex.Action<EditableProperties>() { 623 public EditableProperties run() { 624 return properties.getProperties(path); 625 } 626 }); 627 } 628 629 645 public void putProperties(final String path, final EditableProperties props) { 646 if (path.equals(RakeProjectHelper.PROJECT_XML_PATH) || path.equals(RakeProjectHelper.PRIVATE_XML_PATH)) { 647 throw new IllegalArgumentException ("Attempt to store properties from a project XML file"); } 649 ProjectManager.mutex().writeAccess(new Mutex.Action<Void >() { 650 public Void run() { 651 if (properties.putProperties(path, props)) { 652 modifying(path); 653 } 654 return null; 655 } 656 }); 657 } 658 659 667 public PropertyProvider getPropertyProvider(final String path) { 668 if (path.equals(RakeProjectHelper.PROJECT_XML_PATH) || path.equals(RakeProjectHelper.PRIVATE_XML_PATH)) { 669 throw new IllegalArgumentException ("Attempt to store properties from a project XML file"); } 671 return ProjectManager.mutex().readAccess(new Mutex.Action<PropertyProvider>() { 672 public PropertyProvider run() { 673 return properties.getPropertyProvider(path); 674 } 675 }); 676 } 677 678 691 public Element getPrimaryConfigurationData(final boolean shared) { 692 final String name = type.getPrimaryConfigurationDataElementName(shared); 693 assert name.indexOf(':') == -1; 694 final String namespace = type.getPrimaryConfigurationDataElementNamespace(shared); 695 assert namespace != null && namespace.length() > 0; 696 return ProjectManager.mutex().readAccess(new Mutex.Action<Element >() { 697 public Element run() { 698 synchronized (modifiedMetadataPaths) { 699 Element el = getConfigurationFragment(name, namespace, shared); 700 if (el != null) { 701 return el; 702 } else { 703 return cloneSafely(getConfigurationXml(shared).createElementNS(namespace, name)); 705 } 706 } 707 } 708 }); 709 } 710 711 727 public void putPrimaryConfigurationData(Element data, boolean shared) throws IllegalArgumentException { 728 String name = type.getPrimaryConfigurationDataElementName(shared); 729 assert name.indexOf(':') == -1; 730 String namespace = type.getPrimaryConfigurationDataElementNamespace(shared); 731 assert namespace != null && namespace.length() > 0; 732 if (!name.equals(data.getLocalName()) || !namespace.equals(data.getNamespaceURI())) { 733 throw new IllegalArgumentException ("Wrong name/namespace: expected {" + namespace + "}" + name + " but was {" + data.getNamespaceURI() + "}" + data.getLocalName()); } 735 putConfigurationFragment(data, shared); 736 } 737 738 private final class FileListener implements FileChangeSupportListener { 739 740 public FileListener() {} 741 742 private void change(File f) { 743 if (writingXML) { 744 return; 745 } 746 String path; 747 synchronized (modifiedMetadataPaths) { 748 if (f.equals(resolveFile(PROJECT_XML_PATH))) { 749 if (modifiedMetadataPaths.contains(PROJECT_XML_PATH)) { 750 return ; 752 } 753 path = PROJECT_XML_PATH; 754 projectXml = null; 755 } else if (f.equals(resolveFile(PRIVATE_XML_PATH))) { 756 if (modifiedMetadataPaths.contains(PRIVATE_XML_PATH)) { 757 return ; 759 } 760 path = PRIVATE_XML_PATH; 761 privateXml = null; 762 } else { 763 throw new AssertionError ("Unexpected file change in " + f); } 765 } 766 fireExternalChange(path); 767 } 768 769 public void fileCreated(FileChangeSupportEvent event) { 770 change(event.getPath()); 771 } 772 773 public void fileDeleted(FileChangeSupportEvent event) { 774 change(event.getPath()); 775 } 776 777 public void fileModified(FileChangeSupportEvent event) { 778 change(event.getPath()); 779 } 780 781 } 782 783 790 Element getConfigurationFragment(final String elementName, final String namespace, final boolean shared) { 791 return ProjectManager.mutex().readAccess(new Mutex.Action<Element >() { 792 public Element run() { 793 synchronized (modifiedMetadataPaths) { 794 Element root = getConfigurationDataRoot(shared); 795 Element data = Util.findElement(root, elementName, namespace); 796 if (data != null) { 797 return cloneSafely(data); 798 } else { 799 return null; 800 } 801 } 802 } 803 }); 804 } 805 806 private static final DocumentBuilder db; 807 static { 808 try { 809 db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 810 } catch (ParserConfigurationException e) { 811 throw new AssertionError (e); 812 } 813 } 814 private static Element cloneSafely(Element el) { 815 synchronized (db) { 818 Document dummy = db.newDocument(); 819 return (Element ) dummy.importNode(el, true); 820 } 821 } 822 823 828 void putConfigurationFragment(final Element fragment, final boolean shared) { 829 ProjectManager.mutex().writeAccess(new Mutex.Action<Void >() { 830 public Void run() { 831 synchronized (modifiedMetadataPaths) { 832 Element root = getConfigurationDataRoot(shared); 833 Element existing = Util.findElement(root, fragment.getLocalName(), fragment.getNamespaceURI()); 834 if (existing != null) { 836 root.removeChild(existing); 837 } 838 Node ref = null; 840 NodeList list = root.getChildNodes(); 841 for (int i=0; i<list.getLength(); i++) { 842 Node node = list.item(i); 843 if (node.getNodeType() != Node.ELEMENT_NODE) { 844 continue; 845 } 846 int comparison = node.getNodeName().compareTo(fragment.getNodeName()); 847 if (comparison == 0) { 848 comparison = node.getNamespaceURI().compareTo(fragment.getNamespaceURI()); 849 } 850 if (comparison > 0) { 851 ref = node; 852 break; 853 } 854 } 855 root.insertBefore(root.getOwnerDocument().importNode(fragment, true), ref); 856 modifying(shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH); 857 } 858 return null; 859 } 860 }); 861 } 862 863 870 boolean removeConfigurationFragment(final String elementName, final String namespace, final boolean shared) { 871 return ProjectManager.mutex().writeAccess(new Mutex.Action<Boolean >() { 872 public Boolean run() { 873 synchronized (modifiedMetadataPaths) { 874 Element root = getConfigurationDataRoot(shared); 875 Element data = Util.findElement(root, elementName, namespace); 876 if (data != null) { 877 root.removeChild(data); 878 modifying(shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH); 879 return true; 880 } else { 881 return false; 882 } 883 } 884 } 885 }); 886 } 887 888 893 public AuxiliaryConfiguration createAuxiliaryConfiguration() { 894 return new ExtensibleMetadataProviderImpl(this); 895 } 896 897 902 public CacheDirectoryProvider createCacheDirectoryProvider() { 903 return new ExtensibleMetadataProviderImpl(this); 904 } 905 906 919 public RakeArtifact createSimpleRakeArtifact(String type, String locationProperty, PropertyEvaluator eval, String targetName, String cleanTargetName) { 920 return new SimpleRakeArtifact(this, type, locationProperty, eval, targetName, cleanTargetName); 921 } 922 923 987 public SharabilityQueryImplementation createSharabilityQuery(PropertyEvaluator eval, String [] sourceRoots, String [] buildDirectories) { 988 String [] includes = new String [sourceRoots.length + 1]; 989 System.arraycopy(sourceRoots, 0, includes, 0, sourceRoots.length); 990 includes[sourceRoots.length] = ""; String [] excludes = new String [buildDirectories.length + 1]; 992 System.arraycopy(buildDirectories, 0, excludes, 0, buildDirectories.length); 993 excludes[buildDirectories.length] = "nbproject/private"; return new SharabilityQueryImpl(this, eval, includes, excludes); 995 } 996 997 1004 public PropertyProvider getStockPropertyPreprovider() { 1005 return properties.getStockPropertyPreprovider(); 1006 } 1007 1008 1018 public PropertyEvaluator getStandardPropertyEvaluator() { 1019 return properties.getStandardPropertyEvaluator(); 1020 } 1021 1022 1028 public File resolveFile(String filename) { 1029 if (filename == null) { 1030 throw new NullPointerException ("Attempted to pass a null filename to resolveFile"); } 1032 return PropertyUtils.resolveFile(FileUtil.toFile(dir), filename); 1033 } 1034 1035 1040 public FileObject resolveFileObject(String filename) { 1041 if (filename == null) { 1042 throw new NullPointerException ("Must pass a non-null filename"); } 1044 return PropertyUtils.resolveFileObject(dir, filename); 1045 } 1046 1047 1054 public String resolvePath(String path) { 1055 if (path == null) { 1056 throw new NullPointerException ("Must pass a non-null path"); } 1058 return PropertyUtils.resolvePath(FileUtil.toFile(dir), path); 1060 } 1061 1062 public String toString() { 1063 return "RakeProjectHelper[" + getProjectDirectory() + "]"; } 1065 1066} 1067 | Popular Tags |