blob: 3cdee9e22a7fd3f44ffeda41fb3ed099b8d835f9 [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.actions;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.xml.parsers.ParserConfigurationException;
import org.netbeans.api.project.Project;
import org.netbeans.modules.gradle.api.GradleBaseProject;
import static org.netbeans.modules.gradle.api.NbGradleProject.PROP_PROJECT_INFO;
import org.netbeans.modules.gradle.api.execute.ActionMapping;
import org.netbeans.modules.gradle.api.execute.GradleExecConfiguration;
import org.netbeans.modules.gradle.execute.ConfigurableActionProvider;
import org.netbeans.modules.gradle.execute.GradleExecAccessor;
import org.netbeans.modules.gradle.execute.ProjectConfigurationSupport;
import org.netbeans.modules.gradle.spi.GradleFiles;
import org.netbeans.modules.gradle.spi.actions.GradleActionsProvider;
import org.netbeans.modules.gradle.spi.actions.ProjectActionMappingProvider;
import org.netbeans.spi.project.ProjectConfigurationProvider;
import org.netbeans.spi.project.ProjectServiceProvider;
import org.openide.filesystems.FileChangeAdapter;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.util.BaseUtilities;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
import org.openide.util.lookup.ProxyLookup;
import org.xml.sax.SAXException;
/**
* Extended implementation of action provider. This service is quite tangled with {@link ProjectConfigurationProvider}: individual participating
* {@link GradleActionProvider}s may provide builtin {@link GradleExecConfiguration}s - this information is served from this impl to GradleProjectConfigProvider.
* And action definition files are loaded for defined configurations, especially the user-defined, which is managed by the {@link ProjectConfigurationProvider} implementation.
* So these two impls listen for each other firing their own change events + guard against loops.
* <p/>
* Caching here is designed as follows: Once data is loaded, it is valid until a refresh. Customized actions are loaded lazily, on query for a specific configuration, or
* during the refresh. Once the customizations are loaded, the service listens on the file - if the content changes, customizations are invalidated and loaded during next query.
* Refresh is caused by a change to the set of configurations and to the set of {@link GradleActionProvider}s - which may occur e.g. when the project is reloaded and
* new set of plugins is detected.
*
* @author sdedic
*/
@ProjectServiceProvider(service = { ConfigurableActionProvider.class, ProjectActionMappingProvider.class }, projectType = "org-netbeans-modules-gradle")
public class ConfigurableActionsProviderImpl implements ProjectActionMappingProvider, ConfigurableActionProvider {
private static final Logger LOG = Logger.getLogger(ConfigurableActionsProviderImpl.class.getName());
private static final RequestProcessor ACTIONS_REFRESH_RP = new RequestProcessor(ConfigurableActionsProviderImpl.class); // NOI18N
private final Project project;
private final FileObject projectDirectory;
/**
* Listener for project reloads. Will reload the actions if the project information change.
*/
final PropertyChangeListener pcl = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (PROP_PROJECT_INFO.equals(evt.getPropertyName())) {
reload();
}
}
};
/**
* Listener for individual action files and the project directory.
*/
final FileChangeListener fcl = new FileChangeAdapter() {
@Override
public void fileRenamed(FileRenameEvent fe) {
actionFileChanged(fe.getFile(), fe.getName(), false);
}
@Override
public void fileDeleted(FileEvent fe) {
actionFileChanged(fe.getFile(), null, true);
}
@Override
public void fileChanged(FileEvent fe) {
actionFileChanged(fe.getFile(), null, false);
}
@Override
public void fileDataCreated(FileEvent fe) {
actionFileChanged(fe.getFile(), null, false);
}
};
/**
* Configuration provider. Initialized on the first read by {@link #conf()}.
*/
private ProjectConfigurationProvider<GradleExecConfiguration> confProvider;
/**
* Changeable list of providers. Collected from the project (first) and the default
* Lookup (last).
*/
// @GuardedBy(this)
private Lookup.Result<GradleActionsProvider> providers;
/**
* Cached actions.
*/
// @GuardedBy(this)
private Map<String, ActionData> cache = null;
// @GuardedBy(this)
private final List<ChangeListener> listeners = new ArrayList<>();
// @GuardedBy(this)
private Map<String, GradleExecConfiguration> configurations = new HashMap<>();
// @GuardedBy(this)
private Set<String> actionIDs = new HashSet<>();
// @GuardedBy(this)
private Set<String> plugins;
public ConfigurableActionsProviderImpl(Project project, Lookup l) {
this.project = project;
this.projectDirectory = project.getProjectDirectory();
FileChangeListener wl = WeakListeners.create(FileChangeListener.class, fcl, this.projectDirectory);
projectDirectory.addFileChangeListener(wl);
LOG.log(Level.FINER, "Initializing ConfigurableAP for {0}", project);
}
void setConfigurationProvider(ProjectConfigurationProvider p) {
this.confProvider = p;
p.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
configurationsChanged();
}
});
}
private ProjectConfigurationProvider<GradleExecConfiguration> conf() {
if (confProvider != null) {
return confProvider;
}
synchronized (this) {
confProvider = project.getLookup().lookup(ProjectConfigurationProvider.class);
setConfigurationProvider(confProvider);
return confProvider;
}
}
private String effectiveConfig() {
return ProjectConfigurationSupport.getEffectiveConfiguration(project, Lookup.EMPTY).getId();
}
ActionMapping findMapping(String action, String cfgId) {
ActionData ad = getActionData(cfgId);
ActionMapping m = null;
if (ad != null) {
m = ad.getAction(action);
}
if (m == null && !GradleExecConfiguration.DEFAULT.equals(cfgId)) {
m = getActionData(GradleExecConfiguration.DEFAULT).getAction(action);
}
return m;
}
Set<String> customizedActions(String cfgId) {
ActionData ad = getActionData(cfgId);
if (ad == null) {
return Collections.emptySet();
}
return new HashSet<>(ad.customizedMappings.keySet());
}
@Override
public ActionMapping findMapping(String action) {
return findMapping(action, effectiveConfig());
}
@Override
public Set<String> customizedActions() {
return customizedActions(effectiveConfig());
}
@Override
public void addChangeListener(ChangeListener l) {
synchronized (this) {
listeners.add(l);
}
}
@Override
public void removeChangeListener(ChangeListener l) {
synchronized (this) {
listeners.remove(l);
}
}
@Override
public List<GradleExecConfiguration> findConfigurations() {
Set<String> confIds;
synchronized (this) {
if (cache != null) {
return new ArrayList<>(configurations.values());
}
confIds = cache == null ? null : cache.keySet();
}
LOG.log(Level.FINER, "Reloading configuration; old IDs: {0}", confIds);
Map<String, ActionData> map = updateCache(serial.incrementAndGet(), confIds);
List<GradleExecConfiguration> lst = new ArrayList<>(map.size());
for (ActionData ad : map.values()) {
// do not reflect back user-custom configurations.
if (ad.fromProvider) {
lst.add(ad.cfg);
}
}
return lst;
}
@Override
public ProjectActionMappingProvider findActionProvider(String configurationId) {
return new ProjectActionMappingProvider() {
@Override
public ActionMapping findMapping(String action) {
return ConfigurableActionsProviderImpl.this.findMapping(action, configurationId);
}
@Override
public Set<String> customizedActions() {
return ConfigurableActionsProviderImpl.this.customizedActions(configurationId);
}
};
}
@Override
public ActionMapping findDefaultMapping(String configurationId, String action) {
Map<String, ActionData> snap = null;
synchronized (this) {
snap = cache;
}
if (snap == null) {
LOG.log(Level.FINER, "Reloading configuration");
snap = updateCache(serial.incrementAndGet(), null);
}
ActionData ad = snap.get(configurationId);
if (ad != null) {
ActionMapping result = ad.getDefaultAction(action);
if (result != null) {
return result;
}
}
ActionData def = snap.get(GradleExecConfiguration.DEFAULT);
if (def != ad && def != null) {
return def.getDefaultAction(action);
}
return null;
}
private synchronized Set<String> getPlugins() {
synchronized(this) {
if (plugins == null) {
GradleBaseProject gbp = GradleBaseProject.get(project);
plugins = gbp.getPlugins();
}
return plugins;
}
}
private Collection<? extends GradleActionsProvider> providers() {
if (providers != null) {
return providers.allInstances();
}
LOG.log(Level.FINER, "Initializing providers lookup for: {0}", project);
Lookup combined = new ProxyLookup(Lookup.getDefault(), project.getLookup());
Lookup.Result<GradleActionsProvider> result = combined.lookupResult(GradleActionsProvider.class);
Collection<? extends GradleActionsProvider> lst;
synchronized (this) {
if (providers != null) {
return providers.allInstances();
} else {
LOG.log(Level.FINER, "Attaching provider listener");
result.addLookupListener(new LookupListener() {
@Override
public void resultChanged(LookupEvent ev) {
synchronized (ConfigurableActionsProviderImpl.this) {
if (providers != result) {
return;
}
}
LOG.log(Level.FINER, "Action providers change for {0}, refreshing", project);
refresh();
}
});
lst = result.allInstances();
providers = result;
}
}
return lst;
}
// @GuardedBy(this)
private RequestProcessor.Task pendingTask;
/**
* Stamp guarding against paralel obsolete overwrites.
*/
private final AtomicInteger serial = new AtomicInteger(0);
private void configurationsChanged() {
Collection<? extends GradleExecConfiguration> confs = conf().getConfigurations();
Set<String> ids = new HashSet<>();
confs.forEach(c -> ids.add(c.getId()));
synchronized (this) {
if (cache != null && cache.keySet().equals(ids)) {
LOG.log(Level.FINER, "Configuration set did not change - stop.");
return;
}
}
refresh();
LOG.log(Level.FINER, "Different configuraitons set, reloading");
}
private void refresh() {
Set<String> confIds;
synchronized (this) {
plugins = null;
confIds = cache == null ? null : cache.keySet();
cache = null;
if (pendingTask != null) {
pendingTask.cancel();
}
pendingTask = ACTIONS_REFRESH_RP.post(() -> {
synchronized (this) {
pendingTask = null;
}
updateCache(serial.incrementAndGet(), confIds);
});
}
}
private ActionData getActionData(String id) {
Collection<? extends GradleExecConfiguration> configs = conf().getConfigurations();
Map<String, ActionData> snap;
GradleExecConfiguration foundConfig = null;
synchronized (this) {
snap = cache;
if (snap != null) {
ActionData ad = cache.get(id);
if (ad != null) {
if (ad.customizedMappings != null) {
return ad;
}
} else {
foundConfig = configs.stream().filter(c -> c.getId().equals(id)).findAny().orElse(null);
if (foundConfig == null) {
return null;
}
}
// may need a refresh ... some custom config is not in yet:
}
}
if (snap != null) {
// try to load customized actions now
Map<String, ActionMapping> map = loadCustomActions(id);
synchronized (this) {
if (snap == cache) {
ActionData ad = cache.get(id);
if (ad != null) {
if (ad.customizedMappings != null) {
return ad;
} else {
ad.customizedMappings = map;
}
} else {
ad = new ActionData(false, foundConfig);
cache.put(id, ad);
}
ad.customizedMappings = map;
FileObject f = ActionPersistenceUtils.findActionsFile(projectDirectory, id);
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "Adding listener for: {0} - {1}", new Object[] { id, f });
}
ad.attach(projectDirectory, f, fcl);
return ad;
} else {
return cache.get(id);
}
}
} else {
return updateCache(serial.incrementAndGet(), null).get(id);
}
}
/**
* Refreshes using Refresher, then atomically updates the cache and updates file listeners.
* @return new configuration Map.
*/
private Map<String, ActionData> updateCache(int serial, Set<String> oldConfigs) {
LOG.log(Level.FINER, "Updating action cache for {0}, serial {1}, oldConfigs {2}", new Object[] { project, serial, oldConfigs });
Refresher r = new Refresher();
r.run();
List<ChangeListener> ll;
Map<String, ActionData> oldCache;
Map<String, ActionData> newCache;
synchronized (this) {
if (serial != this.serial.get()) {
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "Reloaded serial {0}-{1} does not match current {2}, stop.", new Object[] { project, serial, this.serial.get() });
}
return r.actionMap;
}
oldCache = this.cache;
this.cache = newCache = r.actionMap;
Map<String, GradleExecConfiguration> confs = new HashMap<>();
for (String id : newCache.keySet()) {
confs.put(id, newCache.get(id).cfg);
}
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "Project {0} got configurations: {1}", new Object[] { project, confs.keySet() });
}
this.configurations = confs;
this.actionIDs = r.actionIDs;
if (oldCache != null) {
for (String id : oldCache.keySet()) {
ActionData ad = oldCache.get(id);
ad.detach();
}
}
for (String id : newCache.keySet()) {
ActionData ad = newCache.get(id);
ad.attach(projectDirectory, null, fcl);
}
if (newCache.keySet().equals(oldConfigs)) {
// no change, no configurations added/removed.
LOG.log(Level.FINER, "No configuraiton change - stop.");
return cache;
}
ll = new ArrayList<>(listeners);
if (LOG.isLoggable(Level.FINER)) {
Set<String> toRemove;
Set<String> toAdd;
toRemove = new HashSet<>(oldCache == null ? Collections.emptySet() : oldCache.keySet());
toRemove.removeAll(newCache.keySet());
toAdd = new HashSet<>(newCache.keySet());
if (oldCache != null) {
toAdd.remove(oldCache.keySet());
}
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "Removed config: {0}, added config: {1}", new Object[] { toRemove, toAdd });
}
}
if (oldConfigs == null) {
return cache;
}
if (listeners.isEmpty()) {
return cache;
}
}
LOG.log(Level.FINER, "Firing config/action change events");
ChangeEvent e = new ChangeEvent(this);
ll.forEach(l -> l.stateChanged(e));
return r.actionMap;
}
private static String[] maybeEmpty(String args) {
if (args == null) {
return new String[0];
} else {
return BaseUtilities.parseParameters(args);
}
}
private Map<String, ActionMapping> loadCustomizedDefaultConfig() {
FileObject defaultConfigFile = project.getProjectDirectory().getFileObject(GradleFiles.GRADLE_PROPERTIES_NAME);
if (defaultConfigFile == null) {
return Collections.emptyMap();
}
Map<String, ActionMapping> mapping = new HashMap<>();
// update just the action mapping
try (InputStream is = defaultConfigFile.getInputStream()) {
Properties props = new Properties();
props.load(is);
Set<ActionMapping> actions = ActionMappingPropertyReader.loadMappings(props);
for (ActionMapping am : actions) {
mapping.put(am.getName(), am);
}
} catch (IOException ex) {
// log
}
return mapping;
}
private Map<String, ActionMapping> loadCustomActions(String id) {
if (GradleExecConfiguration.DEFAULT.equals(id)) {
return loadCustomizedDefaultConfig();
}
FileObject configFile = ActionPersistenceUtils.findActionsFile(projectDirectory, id);
if (configFile == null || !configFile.isValid() || !configFile.isData()) {
return Collections.emptyMap();
}
Map<String, ActionMapping> mapping = new HashMap<>();
// update just the action mapping
try (InputStream is = configFile.getInputStream()) {
Map<String, ActionMapping> map = new HashMap<>();
Set<ActionMapping> actions = ActionMappingScanner.loadMappings(is);
for (ActionMapping am : actions) {
mapping.put(am.getName(), am);
}
} catch (IOException | SAXException | ParserConfigurationException ex) {
// log
}
return mapping;
}
class Refresher implements Runnable {
Map<String, GradleExecConfiguration> config = new HashMap<>();
Set<String> actionIDs = new HashSet<>();
Map<String, ActionData> actionMap = new HashMap<>();
ActionData currentData;
public void run() {
Collection<? extends GradleActionsProvider> lst = providers();
for (GradleActionsProvider p : lst) {
LOG.log(Level.FINER, "Loading defaults from provider {0}", p);
actionIDs.addAll(p.getSupportedActions());
Set<ActionMapping> defaultMappings;
Map<GradleExecConfiguration, Set<ActionMapping>> mapp = new HashMap<>();
try (InputStream istm = p.defaultActionMapConfig()) {
if (istm == null) {
continue;
}
defaultMappings = ActionMappingScanner.loadMappings(istm, mapp);
} catch (IOException | ParserConfigurationException | SAXException ex) {
continue;
}
ActionData ad = actionMap.get(GradleExecConfiguration.DEFAULT);
currentData = ad;
if (ad == null) {
ad = new ActionData(true, GradleExecAccessor.createDefault());
actionMap.put(GradleExecConfiguration.DEFAULT, ad);
ad.customizedMappings = loadCustomActions(ad.cfg.getId());
LOG.log(Level.FINER, "Loaded customizations for <default>: {0}", ad.customizedMappings.keySet());
}
Set<String> newEntries = new HashSet<>();
for (ActionMapping m : defaultMappings) {
ad.mappings.add(m);
newEntries.add(m.getName());
}
LOG.log(Level.FINER, "Loaded actions: {0}", newEntries);
for (GradleExecConfiguration c : mapp.keySet()) {
LOG.log(Level.FINER, "Loading config {0}", c.getId());
GradleExecConfiguration existing = config.get(c.getId());
if (existing != null) {
LOG.log(Level.FINER, "Merging {0} with {1}", new Object[] { existing, c });
existing = mergeConfigurations(existing, c);
} else {
existing = c;
}
config.put(existing.getId(), existing);
ad = actionMap.get(existing.getId());
if (ad == null) {
ad = new ActionData(true, existing);
actionMap.put(existing.getId(), ad);
ad.customizedMappings = loadCustomActions(ad.cfg.getId());
LOG.log(Level.FINER, "Loaded customizations for config {1}: {0}", new Object[] { ad.customizedMappings.keySet(), existing.getId() });
}
newEntries = new HashSet<>();
for (ActionMapping m : mapp.get(c)) {
ad.mappings.add(m);
newEntries.add(m.getName());
}
LOG.log(Level.FINER, "Loaded config actions: {0}", newEntries);
}
}
}
private GradleExecConfiguration mergeConfigurations(GradleExecConfiguration one, GradleExecConfiguration two) {
String dispName = one.getName();
Map<String, String> props = new HashMap<>(one.getProjectProperties());
props.putAll(two.getProjectProperties());
String args = String.join(" ", one.getCommandLineArgs(), two.getCommandLineArgs());
String[] params = maybeEmpty(one.getCommandLineArgs());
String[] params2 = maybeEmpty(two.getCommandLineArgs());
String[] all = new String[params.length + params2.length];
System.arraycopy(params, 0, all, 0, params.length);
System.arraycopy(params2, 0, all, params.length, params2.length);
return GradleExecAccessor.instance().update(one, dispName, props, args);
}
}
private class ActionData {
private final GradleExecConfiguration cfg;
private final List<ActionMapping> mappings = new ArrayList<>();
private final boolean fromProvider;
private FileObject monitoringFile;
// @GuardedBy(ConfigurableActionProvider.this)
private FileChangeListener listener;
// @GuardedBy(ConfigurableActionProvider.this)
private Map<String, ActionMapping> customizedMappings = null;
public ActionData(boolean provided, GradleExecConfiguration cfg) {
this.fromProvider = provided;
this.cfg = cfg;
}
ActionMapping getAction(String id) {
ActionMapping result = customizedMappings.get(id);
if (result != null) {
return result;
} else {
return getDefaultAction(id);
}
}
ActionMapping getDefaultAction(String id) {
ActionMapping result = null;
for (ActionMapping mapping : mappings) {
if (mapping.getName().equals(id) && mapping.isApplicable(getPlugins())) {
if (result == null || result.compareTo(mapping) < 0) {
result = mapping;
}
}
}
return result;
}
// @GuardedBy(ConfigurableActionProvider.this)
void clearCustomMappings() {
customizedMappings = Collections.emptyMap();
detach();
}
void attach(FileObject projectDir, FileObject f, FileChangeListener fcl) {
if (listener != null) {
return;
}
if (f == null) {
f = ActionPersistenceUtils.findActionsFile(projectDir, cfg.getId());
if (f == null) {
return;
}
}
LOG.log(Level.FINER, "Started to monitor {0}", f);
monitoringFile = f;
listener = WeakListeners.create(FileChangeListener.class, fcl, f);
f.addFileChangeListener(listener);
}
void detach() {
if (monitoringFile == null) {
listener = null;
return;
}
if (listener != null) {
LOG.log(Level.FINER, "Stopped monitoring {0}", monitoringFile);
monitoringFile.removeFileChangeListener(listener);
}
monitoringFile = null;
listener = null;
}
}
private void reload() {
refresh();
}
/**
* Informs that action file has been changed, created or renamed. On rename, originalName
* should be non-null.
* @param af
* @param originalName
*/
private void actionFileChanged(FileObject af, String originalName, boolean delete) {
actionFileChanged0(af, originalName, delete);
}
private boolean actionFileChanged0(FileObject af, String originalName, boolean delete) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Action file changed: {0}, orig: {1}, deleted: {2}", new Object[] { af.getNameExt(), originalName, delete });
}
String name = af.getName();
if (af.getParent() != project.getProjectDirectory()) {
return false;
}
String selectedId = findConfigurationId(af.getNameExt());
ActionData dataHolder = null;
synchronized (this) {
if (cache != null && selectedId != null) {
dataHolder = cache.get(selectedId);
LOG.log(Level.FINER, "selectedId: {0}, holder: {1}", new Object[] { selectedId, dataHolder });
if (dataHolder == null) {
// no known configuration, go away; configurations ought to
// be loaded first.
return false;
}
// just clear, will be loaded on first query.
LOG.log(Level.FINER, "Invalidating holder");
if (delete) {
dataHolder.clearCustomMappings();
} else {
dataHolder.customizedMappings = null;
dataHolder.attach(projectDirectory, af, fcl);
}
}
}
return true;
}
private String findConfigurationId(String fileNameExt) {
if (GradleFiles.GRADLE_PROPERTIES_NAME.equals(fileNameExt)) {
return GradleExecConfiguration.DEFAULT;
} else if (fileNameExt.startsWith(ActionPersistenceUtils.NBACTIONS_CONFIG_PREFIX) && fileNameExt.endsWith(ActionPersistenceUtils.NBACTIONS_XML_EXT)) {
return fileNameExt.substring(ActionPersistenceUtils.NBACTIONS_CONFIG_PREFIX.length(), fileNameExt.length() - ActionPersistenceUtils.NBACTIONS_XML_EXT.length());
}
return null;
}
}