package fr.brouillard.oss.cssfx.impl; /* * #%L * CSSFX * %% * Copyright (C) 2014 CSSFX by Matthieu Brouillard * %% * Licensed 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. * #L% */ import static fr.brouillard.oss.cssfx.impl.log.CSSFXLogger.logger; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; 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.HashSet; import java.util.Iterator; import java.util.List; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Pattern; import javafx.application.Platform; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; import fr.brouillard.oss.cssfx.api.URIToPathConverter; import fr.brouillard.oss.cssfx.impl.events.CSSFXEvent; import fr.brouillard.oss.cssfx.impl.events.CSSFXEvent.EventType; import fr.brouillard.oss.cssfx.impl.events.CSSFXEventListener; import fr.brouillard.oss.cssfx.impl.monitoring.CleanupDetector; import fr.brouillard.oss.cssfx.impl.monitoring.PathsWatcher; /** * CSSFXMonitor is the central controller of the CSS monitoring feature. * * @author Matthieu Brouillard */ public class CSSFXMonitor { private PathsWatcher pw; // keep insertion order private List<URIToPathConverter> knownConverters = new CopyOnWriteArrayList<>(); private ObservableList<? extends Window> windows; private ObservableList<Scene> scenes; private ObservableList<Node> nodes; private List<CSSFXEventListener> eventListeners = new CopyOnWriteArrayList<>(); private Set<Scene> knownScenes = Collections.newSetFromMap(new WeakHashMap<>()); private Set<Window> knownWindows = Collections.newSetFromMap(new WeakHashMap<>()); private Set<Node> knownNodes = Collections.newSetFromMap(new WeakHashMap<>()); public CSSFXMonitor() { } public void setStages(ObservableList<Stage> stages) { setWindows(stages); } public void setWindows(ObservableList<? extends Window> stages) { this.windows = stages; } public void setScenes(ObservableList<Scene> scenes) { this.scenes = scenes; } public void setNodes(ObservableList<Node> nodes) { this.nodes = nodes; } public void addAllConverters(Collection<URIToPathConverter> converters) { knownConverters.addAll(converters); } public void addAllConverters(URIToPathConverter... converters) { knownConverters.addAll(Arrays.asList(converters)); } public void addConverter(URIToPathConverter newConverter) { knownConverters.add(newConverter); } public void removeConverter(URIToPathConverter converter) { knownConverters.remove(converter); } public void addEventListener(CSSFXEventListener listener) { eventListeners.add(listener); } public void removeEventListener(CSSFXEventListener listener) { eventListeners.remove(listener); } public void start() { logger(CSSFXMonitor.class).info("CSS Monitoring is about to start"); pw = new PathsWatcher(); Runnable starter = () -> { // start to monitor stage changes if (windows != null) { monitorWindows(windows); } else if (scenes != null) { monitorScenes(scenes); } else if (nodes != null) { monitorChildren(nodes); } }; if (Platform.isFxApplicationThread()) { starter.run(); } else { Platform.runLater(starter); } pw.watch(); logger(CSSFXMonitor.class).info("CSS Monitoring started"); } public void stop() { pw.stop(); } private void monitorWindows(ObservableList<? extends Window> observableWindows) { // first listen for changes observableWindows.addListener(new ListChangeListener<Window>() { @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends Window> c) { while (c.next()) { if (c.wasRemoved()) { for (Window removedWindow : c.getRemoved()) { unregisterWindow(removedWindow); } } if (c.wasAdded()) { for (Window addedWindow : c.getAddedSubList()) { registerWindow(addedWindow); } } } } }); // then process already existing stages for (Window stage : observableWindows) { registerWindow(stage); } } private void monitorStageScene(ReadOnlyObjectProperty<Scene> stageSceneProperty) { // first listen to changes stageSceneProperty.addListener(new ChangeListener<Scene>() { @Override public void changed(ObservableValue<? extends Scene> ov, Scene o, Scene n) { if (o != null) { unregisterScene(o); } if (n != null) { registerScene(n); } } }); if (stageSceneProperty.getValue() != null) { registerScene(stageSceneProperty.getValue()); } } private void monitorRoot(ObjectProperty<Parent> rootProperty) { // register on modification rootProperty.addListener((ov, o, n) -> { if (o != null) { unregisterNode(o); } if (n != null) { registerNode(n); } }); // check current value if (rootProperty.getValue() != null) { registerNode(rootProperty.getValue()); } } private void unregisterNode(Node removedNode) { if (knownNodes.remove(removedNode)) { eventNotify(CSSFXEvent.newEvent(EventType.NODE_REMOVED, removedNode)); } } private void registerNode(Node node) { if (knownNodes.add(node)) { if (node instanceof Parent) { Parent p = (Parent) node; monitorStylesheets(p.getStylesheets()); monitorChildren(p.getChildrenUnmodifiable()); } eventNotify(CSSFXEvent.newEvent(EventType.NODE_ADDED, node)); } } private void monitorScenes(ObservableList<Scene> observableScenes) { // first listen for changes observableScenes.addListener(new ListChangeListener<Scene>() { @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends Scene> c) { while (c.next()) { if (c.wasRemoved()) { for (Scene removedScene : c.getRemoved()) { unregisterScene(removedScene); } } if (c.wasAdded()) { for (Scene addedScene : c.getAddedSubList()) { registerScene(addedScene); } } } } }); // then add existing values for (Scene s : observableScenes) { registerScene(s); } } private void monitorChildren(ObservableList<Node> childrenUnmodifiable) { // first listen to changes childrenUnmodifiable.addListener(new ListChangeListener<Node>() { @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends Node> c) { while (c.next()) { if (c.wasRemoved()) { for (Node removedNode : c.getRemoved()) { unregisterNode(removedNode); } } if (c.wasAdded()) { for (Node addedNode : c.getAddedSubList()) { registerNode(addedNode); } } } } }); // then look already existing children for (Node node : childrenUnmodifiable) { registerNode(node); } } public void monitorStylesheets(ObservableList<String> stylesheets) { final URIRegistrar registrar = new URIRegistrar(knownConverters, pw); // first register for changes stylesheets.addListener(new StyleSheetChangeListener(registrar)); // then look already set stylesheets uris // iterate over a copy to avoid concurrent modification ArrayList<String> stylesheetsURI = new ArrayList<>(stylesheets); for (String uri : stylesheetsURI) { registrar.register(uri, stylesheets); } CleanupDetector.onCleanup(stylesheets, () -> { Platform.runLater(() -> { // This is important, so no empty "Runnables" build up in the PathsWatcher registrar.cleanup(); }); }); } private void registerScene(Scene scene) { if (knownScenes.add(scene)) { eventNotify(CSSFXEvent.newEvent(EventType.SCENE_ADDED, scene)); monitorStylesheets(scene.getStylesheets()); monitorRoot(scene.rootProperty()); } } private void unregisterScene(Scene removedScene) { if (knownScenes.remove(removedScene)) { eventNotify(CSSFXEvent.newEvent(EventType.SCENE_REMOVED, removedScene)); } } private void registerWindow(Window stage) { if (knownWindows.add(stage)) { eventNotify(CSSFXEvent.newEvent(EventType.STAGE_ADDED, stage)); monitorStageScene(stage.sceneProperty()); } } private void unregisterWindow(Window removedStage) { if (knownWindows.remove(removedStage)) { if (removedStage.getScene() != null) { unregisterScene(removedStage.getScene()); } eventNotify(CSSFXEvent.newEvent(EventType.STAGE_REMOVED, removedStage)); } } private void eventNotify(CSSFXEvent<?> e) { for (CSSFXEventListener listener : eventListeners) { listener.onEvent(e); } } public static class URIRegistrar { final Map<String, Path> sourceURIs = new HashMap<>(); final Map<Path, List<Runnable>> actions = new HashMap<>(); final List<URIToPathConverter> converters; private PathsWatcher wp; public URIRegistrar(List<URIToPathConverter> c, PathsWatcher wp) { converters = c; this.wp = wp; } // The logic of this method was taken from the class javafx.scene.image.Image private static final Pattern URL_QUICKMATCH = Pattern.compile("^\\p{Alpha}[\\p{Alnum}+.-]*:.*$"); private String classpathToURI(String str) { if (!URL_QUICKMATCH.matcher(str).matches()) { final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); URL resource; if (str.charAt(0) == '/') { resource = contextClassLoader.getResource(str.substring(1)); } else { resource = contextClassLoader.getResource(str); } if(resource != null) { return resource.toString(); } else { return str; } } try { return new URL(str).toString(); } catch (MalformedURLException e) { return str; } } public void register(String uri, ObservableList<? extends String> stylesheets) { if (!sourceURIs.containsKey(uri)) { String simplifiedURI = classpathToURI(uri); logger(CSSFXMonitor.class).debug("searching source for css[%s]", uri); for (URIToPathConverter c : converters) { Path sourceFile = c.convert(simplifiedURI); List<Runnable> runnables = new LinkedList<>(); if (sourceFile != null) { logger(CSSFXMonitor.class).info("css[%s] will be mapped to source[%s]", uri, sourceFile); Path directory = sourceFile.getParent(); Runnable r = new URIStyleUpdater(uri, sourceFile.toUri().toString(), (ObservableList<String>) stylesheets); wp.monitor(directory.toAbsolutePath().normalize(), sourceFile.toAbsolutePath().normalize(), r); runnables.add(r); sourceURIs.put(sourceFile.toUri().toString(), sourceFile); if (Platform.isFxApplicationThread()) { r.run(); } else { Platform.runLater(r); } } actions.put(sourceFile,runnables); } } } public void unregister(String uri) { } public void cleanup() { actions.forEach((path,runnables) -> { runnables.forEach( runnable -> { wp.unregister(path.getParent().toAbsolutePath().normalize(), path.toAbsolutePath().normalize(), runnable); }); }); } } private static class StyleSheetChangeListener implements ListChangeListener<String> { private URIRegistrar registrar; private StyleSheetChangeListener(URIRegistrar registrar) { this.registrar = registrar; } @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends String> c) { while (c.next()) { if (c.wasRemoved()) { for (String removedURI : c.getRemoved()) { registrar.unregister(removedURI); } } if (c.wasAdded()) { for (String newURI : c.getAddedSubList()) { registrar.register(newURI, c.getList()); } } } } } public static class URIStyleUpdater implements Runnable { private final String sourceURI; private final String originalURI; private final WeakReference<ObservableList<String>> cssURIsWeak; public URIStyleUpdater(String originalURI, String sourceURI, ObservableList<String> cssURIs) { this.originalURI = originalURI; this.sourceURI = sourceURI; this.cssURIsWeak = new WeakReference<>(cssURIs); } @Override public void run() { ObservableList<String> cssURIs = cssURIsWeak.get(); if(cssURIs != null) { Runnable task = () -> { int counter = 0; while(counter < cssURIs.size()) { String v = cssURIs.get(counter); if(v.equals(originalURI) || v.equals(sourceURI)) { cssURIs.remove(counter); cssURIs.add(counter, sourceURI); } counter += 1; } }; // It's important that we are using runLater even when we are using the JavaFX Thread. // This way we make sure we are currently not running the ChangeListener // which would result in an Exception. Platform.runLater(task); } } } }