package org.netbeans.gradle.project.properties; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; import javax.swing.SwingUtilities; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.jtrim.collections.EqualityComparator; import org.jtrim.concurrent.UpdateTaskExecutor; import org.jtrim.event.CopyOnTriggerListenerManager; import org.jtrim.event.EventDispatcher; import org.jtrim.event.ListenerManager; import org.jtrim.event.ListenerRef; import org.jtrim.event.ListenerRegistries; import org.jtrim.property.MutableProperty; import org.jtrim.property.PropertyFactory; import org.jtrim.property.PropertySourceProxy; import org.jtrim.swing.concurrent.SwingUpdateTaskExecutor; import org.jtrim.utils.ExceptionHelper; import org.netbeans.gradle.project.api.config.ConfigPath; import org.netbeans.gradle.project.api.config.ConfigTree; import org.netbeans.gradle.project.api.config.PropertyDef; import org.netbeans.gradle.project.api.config.PropertyKeyEncodingDef; import org.netbeans.gradle.project.api.config.PropertyValueDef; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public final class ProfileSettings { private static final Logger LOGGER = Logger.getLogger(ProfileSettings.class.getName()); private static final int FILE_STREAM_BUFFER_SIZE = 8 * 1024; private static final Set<ConfigPath> ROOT_PATH = Collections.singleton(ConfigPath.ROOT); private static final Document EXPORT_DOCUMENT = tryCreateDocument(); private final ListenerManager<ConfigUpdateListener> configUpdateListeners; private final EventDispatcher<ConfigUpdateListener, Collection<ConfigPath>> configUpdateDispatcher; private final ReentrantLock configLock; private volatile Object configStateKey; private ConfigTree.Builder currentConfig; private final Map<DomElementKey, Element> auxConfigs; private static Document tryCreateDocument() { try { return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); } catch (ParserConfigurationException ex) { LOGGER.log(Level.SEVERE, "Cannot create document.", ex); return null; } } public ProfileSettings() { this.configLock = new ReentrantLock(); this.currentConfig = new ConfigTree.Builder(); this.configUpdateListeners = new CopyOnTriggerListenerManager<>(); this.configStateKey = new Object(); this.auxConfigs = new HashMap<>(); this.configUpdateDispatcher = new EventDispatcher<ConfigUpdateListener, Collection<ConfigPath>>() { @Override public void onEvent(ConfigUpdateListener eventListener, Collection<ConfigPath> arg) { eventListener.configUpdated(arg); } }; } public static boolean isEventThread() { return SwingUtilities.isEventDispatchThread(); } ListenerRef addDocumentChangeListener(final Runnable listener) { ExceptionHelper.checkNotNullArgument(listener, "listener"); return configUpdateListeners.registerListener(new ConfigUpdateListener() { @Override public void configUpdated(Collection<ConfigPath> changedPaths) { listener.run(); } }); } private static DocumentBuilder getDocumentBuilder() { try { return DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException ex) { throw new RuntimeException("Cannot create Document builder.", ex); } } private static Document getEmptyDocument() { return getDocumentBuilder().newDocument(); } private static Document readXml(InputStream xmlSource) throws IOException, SAXException { ExceptionHelper.checkNotNullArgument(xmlSource, "xmlSource"); return getDocumentBuilder().parse(xmlSource); } private static Document readXml(Path xmlFile) throws IOException, SAXException { ExceptionHelper.checkNotNullArgument(xmlFile, "xmlFile"); if (!Files.exists(xmlFile)) { return getEmptyDocument(); } try (InputStream fileInput = Files.newInputStream(xmlFile); InputStream input = new BufferedInputStream(fileInput, FILE_STREAM_BUFFER_SIZE)) { return readXml(input); } } public void loadFromFile(Path xmlFile) { Document document; try { document = readXml(xmlFile); } catch (IOException | SAXException ex) { LOGGER.log(Level.INFO, "Unable to parse XML config file: " + xmlFile, ex); return; } loadFromDocument(document); } public void loadFromStream(InputStream xmlSource) { Document document; try { document = readXml(xmlSource); } catch (IOException | SAXException ex) { LOGGER.log(Level.INFO, "Unable to parse XML config file from stream.", ex); return; } loadFromDocument(document); } public ConfigTree getContentSnapshot() { return ConfigXmlUtils.parseDocument(toXml()).create(); } private Document toXml() { ConfigTree configTree; List<Element> auxConfigList; configLock.lock(); try { configTree = currentConfig.create(); auxConfigList = new ArrayList<>(auxConfigs.values()); } finally { configLock.unlock(); } Document document; try { document = ConfigXmlUtils.createXml(configTree); } catch (ParserConfigurationException ex) { throw new RuntimeException(ex); } ConfigXmlUtils.addAuxiliary(document, auxConfigList.toArray(new Element[auxConfigList.size()])); return document; } public void saveToFile(Path xmlFile, ConfigSaveOptions saveOptions) throws IOException { ExceptionHelper.checkNotNullArgument(xmlFile, "xmlFile"); ExceptionHelper.checkNotNullArgument(saveOptions, "saveOptions"); Document document = toXml(); Path outputDir = xmlFile.getParent(); if (outputDir != null) { Files.createDirectories(outputDir); } ConfigXmlUtils.saveXmlTo(document, xmlFile, saveOptions); } private void fireDocumentUpdate(final Collection<ConfigPath> path) { configUpdateListeners.onEvent(configUpdateDispatcher, path); } private Object newConfigState() { assert configLock.isHeldByCurrentThread(); Object newState = new Object(); configStateKey = newState; return newState; } private static Node getChildByName(Element parent, String childName) { NodeList children = parent.getChildNodes(); int childCount = children.getLength(); for (int i = 0; i < childCount; i++) { Node child = children.item(i); if (Objects.equals(child.getNodeName(), childName)) { return child; } } return null; } private static List<Element> getAuxiliaryElements(Element parent) { if (parent == null) { return Collections.emptyList(); } Node auxRoot = getChildByName(parent, ConfigXmlUtils.AUXILIARY_NODE_NAME); if (auxRoot == null) { return Collections.emptyList(); } NodeList children = auxRoot.getChildNodes(); int childCount = children.getLength(); List<Element> result = new ArrayList<>(childCount); for (int i = 0; i < childCount; i++) { Node child = children.item(i); if (child instanceof Element) { result.add((Element)child); } } return result; } public void clearSettings() { configLock.lock(); try { auxConfigs.clear(); currentConfig = new ConfigTree.Builder(); newConfigState(); } finally { configLock.unlock(); } fireDocumentUpdate(ROOT_PATH); } private void loadFromDocument(final Document document) { ExceptionHelper.checkNotNullArgument(document, "document"); ConfigTree.Builder parsedDocument = ConfigXmlUtils.parseDocument(document, ConfigXmlUtils.AUXILIARY_NODE_NAME); List<Element> loadedAuxConfigs = getAuxiliaryElements(document.getDocumentElement()); configLock.lock(); try { auxConfigs.clear(); for (Element entry: loadedAuxConfigs) { auxConfigs.put(new DomElementKey(entry.getNodeName(), entry.getNamespaceURI()), entry); } currentConfig = parsedDocument; newConfigState(); } finally { configLock.unlock(); } fireDocumentUpdate(ROOT_PATH); } private static ConfigTree createChildTree(ConfigTree.Builder builer, ConfigPath path) { ConfigTree.Builder childBuilder = builer.getDeepChildBuilder(path); childBuilder.detachChildTreeBuilders(); return childBuilder.create(); } private <Value> ValueWithStateKey<Value> withStateKey(Value value) { assert configLock.isHeldByCurrentThread(); return new ValueWithStateKey<>(configStateKey, value); } private ValueWithStateKey<ConfigTree> getChildConfig(ConfigPath path) { configLock.lock(); try { return withStateKey(createChildTree(currentConfig, path)); } finally { configLock.unlock(); } } private ValueWithStateKey<ConfigTree> getChildConfig(ConfigPath basePath, ConfigPath[] relPaths) { if (relPaths.length == 1) { assert relPaths[0].getKeyCount() == 0; // Common case return getChildConfig(basePath); } Object resultStateKey; ConfigTree.Builder result = new ConfigTree.Builder(); configLock.lock(); try { resultStateKey = configStateKey; ConfigTree.Builder baseBuilder = currentConfig.getDeepChildBuilder(basePath); for (ConfigPath relPath: relPaths) { ConfigTree childTree = createChildTree(baseBuilder, relPath); setChildTree(result, relPath, childTree); } } finally { configLock.unlock(); } return new ValueWithStateKey<>(resultStateKey, result.create()); } public Collection<DomElementKey> getAuxConfigKeys() { configLock.lock(); try { return new ArrayList<>(auxConfigs.keySet()); } finally { configLock.unlock(); } } public Element getAuxConfigValue(DomElementKey key) { ExceptionHelper.checkNotNullArgument(key, "key"); Element result; configLock.lock(); try { result = auxConfigs.get(key); } finally { configLock.unlock(); } return result != null ? (Element)EXPORT_DOCUMENT.importNode(result, true) : null; } public boolean setAuxConfigValue(DomElementKey key, Element value) { ExceptionHelper.checkNotNullArgument(key, "key"); Element toAdd = value != null ? (Element)EXPORT_DOCUMENT.importNode(value, true) : null; configLock.lock(); try { if (toAdd == null) { return auxConfigs.remove(key) != null; } else { auxConfigs.put(key, value); return true; } } finally { configLock.unlock(); } } public <ValueType> MutableProperty<ValueType> getProperty( PropertyDef<?, ValueType> propertyDef) { return new DomTrackingProperty<>(propertyDef); } private static List<ConfigPath> copyPaths(Collection<ConfigPath> paths) { switch (paths.size()) { case 0: return Collections.emptyList(); case 1: return Collections.singletonList(paths.iterator().next()); default: return Collections.unmodifiableList(new ArrayList<>(paths)); } } private static ConfigPath[] removeTopParents(int removeCount, ConfigPath[] paths) { if (removeCount == 0) { return paths; } ConfigPath[] result = new ConfigPath[paths.length]; for (int i = 0; i < result.length; i++) { List<String> keys = paths[i].getKeys(); result[i] = ConfigPath.fromKeys(keys.subList(removeCount, keys.size())); } return result; } private static ConfigPath getCommonParent(ConfigPath[] paths) { if (paths.length == 1) { // Almost every time this path is taken. return paths[0]; } if (paths.length == 0) { return ConfigPath.ROOT; } int minLength = paths[0].getKeyCount(); for (int i = 1; i < paths.length; i++) { int keyCount = paths[i].getKeyCount(); if (keyCount < minLength) minLength = keyCount; } List<String> result = new ArrayList<>(); outerLoop: for (int keyIndex = 0; keyIndex < minLength; keyIndex++) { String key = paths[0].getKeyAt(keyIndex); for (int pathIndex = 1; pathIndex < paths.length; pathIndex++) { if (!key.equals(paths[pathIndex].getKeyAt(keyIndex))) { break outerLoop; } } result.add(key); } return ConfigPath.fromKeys(result); } private static void setChildTree(ConfigTree.Builder builder, ConfigPath path, ConfigTree content) { int keyCount = path.getKeyCount(); assert keyCount > 0; ConfigTree.Builder childConfig = builder; for (int i = 0; i < keyCount - 1; i++) { childConfig = childConfig.getChildBuilder(path.getKeyAt(i)); } childConfig.setChildTree(path.getKeyAt(keyCount - 1), content); } private <ValueKey> ValueWithStateKey<ValueKey> getValueKeyFromCurrentConfig( ConfigPath parent, ConfigPath[] relativePaths, PropertyKeyEncodingDef<ValueKey> keyEncodingDef) { ValueWithStateKey<ConfigTree> parentBasedConfig = getChildConfig(parent, relativePaths); ConfigTree value = parentBasedConfig.value; assert value != null; return parentBasedConfig.withNewValue(keyEncodingDef.decode(value)); } private static interface ConfigUpdateListener { public void configUpdated(Collection<ConfigPath> changedPaths); } private class DomTrackingProperty<ValueKey, ValueType> implements MutableProperty<ValueType> { private final ConfigPath configParent; private final ConfigPath[] configPaths; private final ConfigPath[] relativeConfigPaths; private final List<ConfigPath> configPathsAsList; private final PropertyKeyEncodingDef<ValueKey> keyEncodingDef; private final PropertyValueDef<ValueKey, ValueType> valueDef; private final EqualityComparator<? super ValueKey> valueKeyEquality; private final AtomicReference<ValueWithStateKey<ValueKey>> lastValueKeyRef; private final UpdateTaskExecutor eventThread; private final PropertySourceProxy<ValueType> source; public DomTrackingProperty(PropertyDef<ValueKey, ValueType> propertyDef) { ExceptionHelper.checkNotNullArgument(propertyDef, "propertyDef"); this.configPathsAsList = copyPaths(propertyDef.getConfigPaths()); this.configPaths = configPathsAsList.toArray(new ConfigPath[configPathsAsList.size()]); this.configParent = getCommonParent(this.configPaths); this.relativeConfigPaths = removeTopParents(configParent.getKeyCount(), this.configPaths); this.keyEncodingDef = propertyDef.getKeyEncodingDef(); this.valueDef = propertyDef.getValueDef(); this.valueKeyEquality = propertyDef.getValueKeyEquality(); ValueWithStateKey<ValueKey> initialValueKey = getValueKeyFromCurrentConfig( this.configParent, this.relativeConfigPaths, this.keyEncodingDef); this.lastValueKeyRef = new AtomicReference<>(initialValueKey); this.source = PropertyFactory.proxySource(valueDef.property(initialValueKey.value)); this.eventThread = new SwingUpdateTaskExecutor(true); } private void updateConfigFromKey() { ValueWithStateKey<ValueKey> valueKey; ValueWithStateKey<ValueKey> newValueKey; do { valueKey = lastValueKeyRef.get(); newValueKey = updateConfigFromKey(valueKey); } while (!lastValueKeyRef.compareAndSet(valueKey, newValueKey)); } private ValueWithStateKey<ValueKey> updateConfigFromKey(ValueWithStateKey<ValueKey> valueKeyWithState) { // Should only be called by updateConfigFromKey() ValueKey valueKey = valueKeyWithState.value; ConfigTree encodedValueKey = valueKey != null ? keyEncodingDef.encode(valueKey) : ConfigTree.EMPTY; Object newState; configLock.lock(); try { int pathCount = relativeConfigPaths.length; for (int i = 0; i < pathCount; i++) { ConfigPath relativePath = relativeConfigPaths[i]; ConfigPath path = configPaths[i]; ConfigTree configTree = encodedValueKey.getDeepChildTree(relativePath); updateConfigAtPath(path, configTree); } newState = newConfigState(); } finally { configLock.unlock(); } fireDocumentUpdate(configPathsAsList); return new ValueWithStateKey<>(newState, valueKeyWithState.value); } private void updateConfigAtPath(ConfigPath path, ConfigTree content) { assert configLock.isHeldByCurrentThread(); if (path.getKeyCount() == 0) { currentConfig = new ConfigTree.Builder(content); } else { setChildTree(currentConfig, path, content); } } private ValueWithStateKey<ValueKey> getUpToDateValueKey() { ValueWithStateKey<ValueKey> lastValueKey; Object currentConfigStateKey; while (true) { lastValueKey = lastValueKeyRef.get(); currentConfigStateKey = configStateKey; if (currentConfigStateKey == lastValueKey.stateKey) { // It is possible that there was a concurrent configuration // reload but in this case we can't decide if it came before // us or not, so we conveniently declare ourselves as the winner. return lastValueKey; } else { updateFromConfig(); } } } @Override public void setValue(final ValueType value) { ValueWithStateKey<ValueKey> lastValueKey = getUpToDateValueKey(); ValueKey valueKey = valueDef.getKeyFromValue(value); if (updateSource(lastValueKey.withNewValue(valueKey))) { updateConfigFromKey(); } } @Override public ValueType getValue() { if (lastValueKeyRef.get().stateKey != configStateKey) { updateFromConfig(); } return source.getValue(); } private boolean affectsThis(Collection<ConfigPath> changedPaths) { if (changedPaths == configPathsAsList) { // This event is comming from us, so we won't update. // This is necessary for correctness to avoid infinite loop // in updateConfigFromKey() return false; } for (ConfigPath changedPath: changedPaths) { for (ConfigPath ourPath: configPaths) { if (changedPath.isParentOfOrEqual(ourPath)) { return true; } } } return false; } private ValueWithStateKey<ValueKey> getValueKey() { return getValueKeyFromCurrentConfig(configParent, relativeConfigPaths, keyEncodingDef); } private boolean updateSource(ValueWithStateKey<ValueKey> valueKey) { ValueWithStateKey<ValueKey> prevValueKey = lastValueKeyRef.getAndSet(valueKey); if (valueKeyEquality.equals(prevValueKey.value, valueKey.value)) { return false; } else { source.replaceSource(valueDef.property(valueKey.value)); return true; } } private void updateFromConfig() { updateSource(getValueKey()); } @Override public ListenerRef addChangeListener(final Runnable listener) { ExceptionHelper.checkNotNullArgument(listener, "listener"); ListenerRef ref1 = configUpdateListeners.registerListener(new ConfigUpdateListener() { @Override public void configUpdated(Collection<ConfigPath> changedPaths) { if (affectsThis(changedPaths)) { updateFromConfig(); } } }); ListenerRef ref2 = source.addChangeListener(new Runnable() { @Override public void run() { eventThread.execute(listener); } }); return ListenerRegistries.combineListenerRefs(ref1, ref2); } @Override public String toString() { return "Property{" + Arrays.toString(configPaths) + '}'; } } private static final class ValueWithStateKey<Value> { public final Object stateKey; @Nullable public final Value value; public ValueWithStateKey(Object stateKey, Value valueKey) { this.stateKey = stateKey; this.value = valueKey; } public <NewValue> ValueWithStateKey<NewValue> withNewValue(NewValue newValue) { return new ValueWithStateKey<>(stateKey, newValue); } } }