blob: 2eae23d7eee3d2b0bff4313098540ca58b953ece [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.gradle;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.spi.project.AuxiliaryConfiguration;
import org.netbeans.spi.project.ui.ProjectProblemsProvider;
import org.netbeans.spi.project.ui.ProjectProblemsProvider.ProjectProblem;
import org.openide.filesystems.FileChangeAdapter;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileSystem.AtomicAction;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle.Messages;
import org.openide.util.RequestProcessor;
import org.openide.xml.XMLUtil;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* implementation of AuxiliaryConfiguration that relies on FileObject's attributes
* for the non shared elements and on ${basedir}/nb-configuration file for share ones.
* @author mkleint
*/
public class GradleAuxiliaryConfigImpl implements AuxiliaryConfiguration {
public static final String BROKEN_NBCONFIG = "BROKENNBCONFIG"; //NOI18N
private static final String AUX_CONFIG = "AuxilaryConfiguration"; //NOI18N
public static final String CONFIG_FILE_NAME = "nb-configuration.xml"; //NOI18N
private static final Logger LOG = Logger.getLogger(GradleAuxiliaryConfigImpl.class.getName());
private static final RequestProcessor RP = new RequestProcessor(GradleAuxiliaryConfigImpl.class);
private static final int SAVING_DELAY = 100;
private RequestProcessor.Task savingTask;
private Document scheduledDocument;
private Document cachedDoc;
private static final Document DELETED_FILE_DOCUMENT = XMLUtil.createDocument(AUX_CONFIG, null, null, null);
private static final Document BROKEN_DOCUMENT = XMLUtil.createDocument(AUX_CONFIG, null, null, null);
private final Object configIOLock = new Object();
private final FileObject projectDirectory;
private ProblemProvider pp;
private final FileChangeAdapter fileChange;
private final AtomicBoolean fileChangeSet = new AtomicBoolean(false);
public GradleAuxiliaryConfigImpl(FileObject dir, boolean longtermInstance) {
this.projectDirectory = dir;
if (longtermInstance) {
pp = new ProblemProvider();
fileChange = new FileChangeAdapter() {
@Override
public void fileRenamed(FileRenameEvent fe) {
if (CONFIG_FILE_NAME.equals(fe.getName() + "." + fe.getExt())) {
resetCache();
}
}
@Override
public void fileDeleted(FileEvent fe) {
if (CONFIG_FILE_NAME.equals(fe.getFile().getNameExt())) {
resetCache();
}
}
@Override
public void fileChanged(FileEvent fe) {
if (CONFIG_FILE_NAME.equals(fe.getFile().getNameExt())) {
resetCache();
}
}
@Override
public void fileDataCreated(FileEvent fe) {
if (CONFIG_FILE_NAME.equals(fe.getFile().getNameExt())) {
resetCache();
}
}
};
savingTask = RP.create(new Runnable() {
public @Override void run() {
try {
projectDirectory.getFileSystem().runAtomicAction(new AtomicAction() {
public @Override void run() throws IOException {
Document doc;
synchronized (GradleAuxiliaryConfigImpl.this) {
doc = scheduledDocument;
if (doc == null) {
return;
}
scheduledDocument = null;
}
synchronized (configIOLock) {
FileObject config = projectDirectory.getFileObject(CONFIG_FILE_NAME);
if (doc.getDocumentElement().getElementsByTagName("*").getLength() > 0) {
OutputStream out = config == null ? projectDirectory.createAndOpen(CONFIG_FILE_NAME) : config.getOutputStream();
LOG.log(Level.FINEST, "Write configuration file for {0}", projectDirectory);
try {
XMLUtil.write(doc, out, "UTF-8"); //NOI18N
} finally {
out.close();
}
} else if (config != null) {
LOG.log(Level.FINEST, "Delete empty configuration file for {0}", projectDirectory);
config.delete();
}
}
}
});
} catch (IOException ex) {
LOG.log(Level.INFO, "IO Error while saving " + projectDirectory.getFileObject(CONFIG_FILE_NAME), ex);
}
}
});
} else {
fileChange = null;
fileChangeSet.set(true);
}
}
private synchronized void resetCache() {
cachedDoc = null;
}
public ProjectProblemsProvider getProblemProvider() {
return pp;
}
private Document loadConfig(FileObject config) throws IOException, SAXException {
synchronized (configIOLock) {
return XMLUtil.parse(new InputSource(config.toURL().toString()), false, true, null, null);
}
}
public @Override Element getConfigurationFragment(String elementName, String namespace, boolean shared) {
Element e = doGetConfigurationFragment(elementName, namespace, shared);
return e != null ? cloneSafely(e) : null;
}
// Copied from AntProjectHelper.
private static final DocumentBuilder db;
static {
try {
db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new AssertionError(e);
}
}
private static Element cloneSafely(Element el) { // #190845
// #50198: for thread safety, use a separate document.
// Using XMLUtil.createDocument is much too slow.
synchronized (db) {
Document dummy = db.newDocument();
return (Element) dummy.importNode(el, true);
}
}
@Messages({
"TXT_Problem_Broken_Config=Broken nb-configuration.xml file.",
"# {0} - parser error message",
"DESC_Problem_Broken_Config=The $project_basedir/nb-configuration.xml file cannot be parsed. "
+ "The information contained in the file will be ignored until fixed. "
+ "This affects several features in the IDE that will not work properly as a result.\n\n "
+ "The parsing exception follows:\n{0}",
"TXT_Problem_Broken_Config2=Duplicate entries found in nb-configuration.xml file.",
"DESC_Problem_Broken_Config2=The $project_basedir/nb-configuration.xml file contains some elements multiple times. "
+ "That can happen when concurrent changes get merged by version control for example. The IDE however cannot decide which one to use. "
+ "So until the problem is resolved manually, the affected configuration will be ignored."
})
private synchronized Element doGetConfigurationFragment(final String elementName, final String namespace, boolean shared) {
lazyAttachListener();
if (shared) {
//first check the document schedule for persistence
if (scheduledDocument != null) {
try {
Element el = XMLUtil.findElement(scheduledDocument.getDocumentElement(), elementName, namespace);
if (el != null) {
el = (Element) el.cloneNode(true);
}
return el;
} catch (IllegalArgumentException iae) {
//thrown from XmlUtil.findElement when more than 1 equal elements are present.
LOG.log(Level.INFO, iae.getMessage(), iae);
}
}
if (cachedDoc == null) {
final FileObject config = projectDirectory.getFileObject(CONFIG_FILE_NAME);
if (config != null) {
// we need to re-read the config file..
try {
Document doc = loadConfig(config);
cachedDoc = doc;
if (pp != null) {
pp.setProblem(null);
findDuplicateElements(doc.getDocumentElement(), pp, config);
}
return XMLUtil.findElement(doc.getDocumentElement(), elementName, namespace);
} catch (final SAXException ex) {
if (pp != null) {
RP.post(new Runnable() {
@Override
public void run() {
//TODO Report the problem
// pp.setProblem(ProjectProblem.createWarning(
// TXT_Problem_Broken_Config(),
// DESC_Problem_Broken_Config(ex.getMessage()),
// new ProblemReporterImpl.MavenProblemResolver(ProblemReporterImpl.createOpenFileAction(config), BROKEN_NBCONFIG)));
}
});
}
LOG.log(Level.INFO, ex.getMessage(), ex);
cachedDoc = BROKEN_DOCUMENT;
} catch (IOException ex) {
LOG.log(Level.INFO, "IO Error while loading " + config.getPath(), ex);
cachedDoc = BROKEN_DOCUMENT;
} catch (IllegalArgumentException iae) {
//thrown from XmlUtil.findElement when more than 1 equal elements are present.
LOG.log(Level.INFO, iae.getMessage(), iae);
}
return null;
} else {
// no file.. remove possible cache
cachedDoc = DELETED_FILE_DOCUMENT;
return null;
}
} else {
if (cachedDoc == DELETED_FILE_DOCUMENT || cachedDoc == BROKEN_DOCUMENT) {
return null;
}
//reuse cached value if available;
try {
return XMLUtil.findElement(cachedDoc.getDocumentElement(), elementName, namespace);
} catch (IllegalArgumentException iae) {
//thrown from XmlUtil.findElement when more than 1 equal elements are present.
LOG.log(Level.INFO, iae.getMessage(), iae);
}
}
return null;
} else {
String str = (String) projectDirectory.getAttribute(AUX_CONFIG);
if (str != null) {
Document doc;
try {
doc = XMLUtil.parse(new InputSource(new StringReader(str)), false, true, null, null);
return XMLUtil.findElement(doc.getDocumentElement(), elementName, namespace);
} catch (SAXException ex) {
LOG.log(Level.FINE, "cannot parse", ex);
} catch (IOException ex) {
LOG.log(Level.FINE, "error reading private auxiliary configuration", ex);
}
}
return null;
}
}
private void lazyAttachListener() {
if (fileChangeSet.compareAndSet(false, true)) {
projectDirectory.addFileChangeListener(FileUtil.weakFileChangeListener(fileChange, projectDirectory));
}
}
public @Override synchronized void putConfigurationFragment(final Element fragment, final boolean shared) throws IllegalArgumentException {
lazyAttachListener();
Document doc = null;
if (shared) {
if (scheduledDocument != null) {
doc = scheduledDocument;
} else {
FileObject config = projectDirectory.getFileObject(CONFIG_FILE_NAME);
if (config != null) {
try {
doc = loadConfig(config);
} catch (SAXException ex) {
LOG.log(Level.INFO, "Cannot parse file " + config.getPath(), ex);
if (config.getSize() == 0) {
//something got wrong in the past..
doc = createNewSharedDocument();
}
} catch (IOException ex) {
LOG.log(Level.INFO, "IO Error with " + config.getPath(), ex);
}
} else {
doc = createNewSharedDocument();
}
}
} else {
String str = (String) projectDirectory.getAttribute(AUX_CONFIG);
if (str != null) {
try {
doc = XMLUtil.parse(new InputSource(new StringReader(str)), false, true, null, null);
} catch (SAXException ex) {
LOG.log(Level.FINE, "cannot parse", ex);
} catch (IOException ex) {
LOG.log(Level.FINE, "error reading private auxiliary configuration", ex);
}
}
if (doc == null) {
String element = "project-private"; // NOI18N
doc = XMLUtil.createDocument(element, null, null, null);
}
}
if (doc != null) {
Element el = XMLUtil.findElement(doc.getDocumentElement(), fragment.getNodeName(), fragment.getNamespaceURI());
if (el != null) {
doc.getDocumentElement().removeChild(el);
}
doc.getDocumentElement().appendChild(doc.importNode(fragment, true));
if (shared) {
if (scheduledDocument == null) {
scheduledDocument = doc;
}
LOG.log(Level.FINEST, "Schedule saving of configuration fragment for " + projectDirectory, new Exception());
savingTask.schedule(SAVING_DELAY);
} else {
try {
ByteArrayOutputStream wr = new ByteArrayOutputStream();
XMLUtil.write(doc, wr, "UTF-8"); //NOI18N
projectDirectory.setAttribute(AUX_CONFIG, wr.toString("UTF-8"));
} catch (IOException ex) {
LOG.log(Level.FINE, "error writing private auxiliary configuration", ex);
}
}
}
}
public @Override synchronized boolean removeConfigurationFragment(final String elementName, final String namespace, final boolean shared) throws IllegalArgumentException {
lazyAttachListener();
Document doc = null;
FileObject config = projectDirectory.getFileObject(CONFIG_FILE_NAME);
if (shared) {
if (scheduledDocument != null) {
doc = scheduledDocument;
} else {
if (config != null) {
try {
try {
doc = loadConfig(config);
} catch (SAXException ex) {
LOG.log(Level.INFO, "Cannot parse file " + config.getPath(), ex);
if (config.getSize() == 0) {
//just delete the empty file, something got wrong a while back..
config.delete();
}
return true;
}
} catch (IOException ex) {
LOG.log(Level.INFO, "IO Error with " + config.getPath(), ex);
}
} else {
return false;
}
}
} else {
String str = (String) projectDirectory.getAttribute(AUX_CONFIG);
if (str != null) {
try {
doc = XMLUtil.parse(new InputSource(new StringReader(str)), false, true, null, null);
} catch (SAXException | IOException ex) {
Exceptions.printStackTrace(ex);
}
} else {
return false;
}
}
if (doc != null) {
Element el = XMLUtil.findElement(doc.getDocumentElement(), elementName, namespace);
if (el != null) {
doc.getDocumentElement().removeChild(el);
}
if (shared) {
if (scheduledDocument == null) {
scheduledDocument = doc;
}
LOG.log(Level.FINEST, "Schedule saving of configuration fragment for " + projectDirectory, new Exception());
savingTask.schedule(SAVING_DELAY);
} else {
try {
ByteArrayOutputStream wr = new ByteArrayOutputStream();
XMLUtil.write(doc, wr, "UTF-8"); //NOI18N
projectDirectory.setAttribute(AUX_CONFIG, wr.toString("UTF-8"));
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
}
return true;
}
private Document createNewSharedDocument() throws DOMException {
String element = "project-shared-configuration";
Document doc = XMLUtil.createDocument(element, null, null, null);
doc.getDocumentElement().appendChild(doc.createComment(
"\nThis file contains additional configuration written by modules in the NetBeans IDE.\n" +
"The configuration is intended to be shared among all the users of project and\n" +
"therefore it is assumed to be part of version control checkout.\n" +
"Without this configuration present, some functionality in the IDE may be limited or fail altogether.\n"));
return doc;
}
static void findDuplicateElements(@NonNull Element parent, @NonNull ProblemProvider pp, FileObject config) {
NodeList l = parent.getChildNodes();
int nodeCount = l.getLength();
Set<String> known = new HashSet<>();
for (int i = 0; i < nodeCount; i++) {
if (l.item(i).getNodeType() == Node.ELEMENT_NODE) {
Node node = l.item(i);
String localName = node.getLocalName();
localName = localName == null ? node.getNodeName() : localName;
String id = localName + "|" + node.getNamespaceURI();
if (!known.add(id)) {
//we have a duplicate;
//TODO: Report the problem
// pp.setProblem(ProjectProblem.createWarning(
// TXT_Problem_Broken_Config2(),
// DESC_Problem_Broken_Config2(),
// new ProblemReporterImpl.MavenProblemResolver(ProblemReporterImpl.createOpenFileAction(config), BROKEN_NBCONFIG)));
}
}
}
}
private class ProblemProvider implements ProjectProblemsProvider {
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private ProjectProblem pp;
public ProblemProvider() {
}
void setProblem(ProjectProblem pp) {
this.pp = pp;
if (pp == null && this.pp == null) {
return; //ignore this case, dont' fire change..
}
pcs.firePropertyChange(ProjectProblemsProvider.PROP_PROBLEMS, null, null);
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
@Override
public Collection<? extends ProjectProblem> getProblems() {
if (pp != null) {
return Collections.singleton(pp);
} else {
return Collections.emptyList();
}
}
}
}