org.nuiton.jaxx.compiler.css.StylesheetHelper Maven / Gradle / Ivy
The newest version!
/*
* #%L
* JAXX :: Compiler
* %%
* Copyright (C) 2008 - 2024 Code Lutin, Ultreia.io
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
package org.nuiton.jaxx.compiler.css;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.nuiton.jaxx.compiler.CompiledObject;
import org.nuiton.jaxx.compiler.CompilerException;
import org.nuiton.jaxx.compiler.JAXXCompiler;
import org.nuiton.jaxx.compiler.binding.DataBinding;
import org.nuiton.jaxx.compiler.binding.DataBindingHelper;
import org.nuiton.jaxx.compiler.binding.PseudoClassDataBinding;
import org.nuiton.jaxx.compiler.css.parser.CSSParser;
import org.nuiton.jaxx.compiler.css.parser.CSSParserConstants;
import org.nuiton.jaxx.compiler.css.parser.CSSParserTreeConstants;
import org.nuiton.jaxx.compiler.css.parser.SimpleNode;
import org.nuiton.jaxx.compiler.reflect.ClassDescriptor;
import org.nuiton.jaxx.compiler.reflect.ClassDescriptorHelper;
import org.nuiton.jaxx.compiler.reflect.MethodDescriptor;
import org.nuiton.jaxx.compiler.tags.DefaultComponentHandler;
import org.nuiton.jaxx.compiler.tags.DefaultObjectHandler;
import org.nuiton.jaxx.compiler.tags.TagManager;
import org.nuiton.jaxx.compiler.tags.swing.TabWithValidatorHandler;
import org.nuiton.jaxx.compiler.types.TypeManager;
import org.nuiton.jaxx.runtime.css.Pseudoclasses;
import org.nuiton.jaxx.runtime.css.Rule;
import org.nuiton.jaxx.runtime.css.Selector;
import org.nuiton.jaxx.runtime.css.Stylesheet;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* A helper class to compute {@link Stylesheet}, {@link Rule} and {@link Selector}
* and extract all the compiler logic from this class.
*
* In that way we can make the compiler as a single module and a runtime as another module.
*
* @author Tony Chemit - [email protected]
*/
public class StylesheetHelper {
/** Logger */
static private final Logger log = LogManager.getLogger(StylesheetHelper.class);
public static URL getStyleURL(String attrValue, File baseDirectory, ClassLoader classLoader) throws IOException {
if (attrValue.startsWith("classpath:/")) {
URL resource = classLoader.getResource(attrValue.substring("classpath:".length() + 1));
if (resource == null) {
throw new IOException("Style not found: " + attrValue);
}
return resource;
}
if (baseDirectory == null) {
Path path = Paths.get(attrValue);
return Files.exists(path) ? path.toUri().toURL() : URI.create(attrValue).toURL();
}
File styleFile = new File(baseDirectory, attrValue.replace('/', File.separatorChar));
if (!styleFile.exists()) {
throw new IOException("Style not found: " + attrValue);
}
return styleFile.toURI().toURL();
}
public static Stylesheet processStylesheet(String stylesheetText) throws CompilerException {
CSSParser p = new CSSParser(new StringReader(stylesheetText));
SimpleNode node;
try {
node = p.Stylesheet();
} catch (Error e) {
throw new CompilerException(e);
}
List rules = new ArrayList<>();
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
SimpleNode ruleNode = node.getChild(i);
Rule rule = processRule(ruleNode);
rules.add(rule);
}
Stylesheet stylesheet;
stylesheet = new Stylesheet(rules.toArray(new Rule[rules.size()]));
return stylesheet;
}
public static Rule processRule(SimpleNode ruleNode) {
if (ruleNode.getId() != CSSParserTreeConstants.JJTRULE) {
throw new IllegalArgumentException("argument node is not a Rule");
}
SimpleNode selectorsNode = ruleNode.getChild(0);
assert selectorsNode.getId() == CSSParserTreeConstants.JJTSELECTORS :
"expected node to be of type Selectors";
List selectors = new ArrayList<>();
for (int i = 0; i < selectorsNode.jjtGetNumChildren(); i++) {
SimpleNode selectorNode = selectorsNode.getChild(i);
selectors.add(processSelector(selectorNode));
}
Map properties = new HashMap<>();
for (int i = 1; i < ruleNode.jjtGetNumChildren(); i++) {
SimpleNode declarationNode = ruleNode.getChild(i);
if (declarationNode.getId() == CSSParserTreeConstants.JJTDECLARATION) {
String key = declarationNode.getChild(0).getText();
SimpleNode valueNode = declarationNode.getChild(1);
String value = valueNode.getText();
if (valueNode.firstToken.kind == CSSParserConstants.STRING) {
value = value.substring(1, value.length() - 1);
}
properties.put(key, value);
}
}
Rule rule;
rule = new Rule(selectors.toArray(
new Selector[selectors.size()]), properties);
return rule;
}
public static Selector processSelector(SimpleNode selector) {
if (selector.getId() != CSSParserTreeConstants.JJTSELECTOR) {
throw new IllegalArgumentException("argument node is not a Selector");
}
String javaClassName = null;
String styleClass = null;
String pseudoClass = null;
String id = null;
for (int i = 0; i < selector.jjtGetNumChildren(); i++) {
SimpleNode child = selector.getChild(i);
switch (child.getId()) {
case CSSParserTreeConstants.JJTJAVACLASS:
if (!child.getText().trim().equals("*")) {
javaClassName = child.getText();
}
break;
case CSSParserTreeConstants.JJTCLASS:
styleClass = child.getText().substring(1);
break;
case CSSParserTreeConstants.JJTPSEUDOCLASS:
pseudoClass = child.getText().substring(1);
break;
case CSSParserTreeConstants.JJTID:
id = child.getText().substring(1);
break;
default:
throw new IllegalStateException(
"unexpected child of Selector node, type=" +
child.getId());
}
}
return new Selector(javaClassName, styleClass, pseudoClass, id);
}
public enum MouseEventEnum {
mouseover("mouseEntered", "mouseExited"),
mouseout("mouseExited", "mouseReleased"),
mousedown("mousePressed", "mousePressed"),
mouseup("mouseReleased", "mousePressed");
final String addMethod;
final String removeMethod;
// ClassDescriptor mouseListenerDescriptor;
// ClassDescriptor mouseEventDescriptor;
MouseEventEnum(String addMethod, String removeMethod) {
this.removeMethod = removeMethod;
this.addMethod = addMethod;
}
public String getProperty(int i) {
return i == 0 ? addMethod : removeMethod;
}
}
static ClassDescriptor mouseListenerDescriptor;
static ClassDescriptor mouseEventDescriptor;
public static ClassDescriptor getMouseEventDescriptor() {
if (mouseEventDescriptor == null) {
mouseEventDescriptor =
ClassDescriptorHelper.getClassDescriptor(MouseEvent.class);
}
return mouseEventDescriptor;
}
public static ClassDescriptor getMouseListenerDescriptor() {
if (mouseListenerDescriptor == null) {
mouseListenerDescriptor =
ClassDescriptorHelper.getClassDescriptor(MouseListener.class);
}
return mouseListenerDescriptor;
}
public static MethodDescriptor getAddMouseListenerMethod(CompiledObject object) {
try {
return object.getObjectClass().getMethodDescriptor(
"addMouseListener",
getMouseListenerDescriptor()
);
} catch (NoSuchMethodException e) {
throw new CompilerException(
"could not find addMouseListener for object " + object);
}
}
public static MethodDescriptor getMouseListenerMethod(CompiledObject object, String property) {
try {
return getMouseListenerDescriptor().getMethodDescriptor(
property,
getMouseEventDescriptor()
);
} catch (NoSuchMethodException e) {
throw new CompilerException(
"could not find " + property + " for object " + object);
}
}
public static void applyTo(CompiledObject object,
JAXXCompiler compiler,
Stylesheet stylesheet,
Stylesheet overrides) throws CompilerException {
Map overriddenProperties;
if (overrides != null) {
overriddenProperties = getApplicableProperties(overrides, object);
//overriddenProperties = overrides.getApplicableProperties(s,object);
} else {
overriddenProperties = null;
}
Map properties = getApplicableProperties(stylesheet,
object);
if (properties != null) {
if (overriddenProperties != null) {
properties.keySet().removeAll(overriddenProperties.keySet());
}
DefaultObjectHandler handler = TagManager.getTagHandler(object.getObjectClass());
if (properties.containsKey(DefaultComponentHandler.I18N_PROPERTY_ATTRIBUTE)) {
String key = DefaultComponentHandler.I18N_PROPERTY_ATTRIBUTE;
String value = properties.get(key);
if (handler instanceof DefaultComponentHandler)
handler.setAttribute(object, key, value, false, compiler);
else if (handler instanceof TabWithValidatorHandler)
handler.setAttribute(object, key, value, false, compiler);
else {
handler.setAttributeFromCss(object, key, value, compiler);
}
}
for (Map.Entry e : properties.entrySet()) {
String value = e.getValue();
if (value.equals(Rule.INLINE_ATTRIBUTE) || value.equals(Rule.DATA_BINDING)) {
continue;
}
if (e.getKey().equals(DefaultComponentHandler.I18N_PROPERTY_ATTRIBUTE)) {
continue;
}
if (handler instanceof DefaultComponentHandler)
handler.setAttribute(object, e.getKey(), e.getValue(), false, compiler);
else if (handler instanceof TabWithValidatorHandler)
handler.setAttribute(object, e.getKey(), e.getValue(), false, compiler);
else {
handler.setAttributeFromCss(object, e.getKey(), value, compiler);
}
}
}
Rule[] pseudoClasses = getApplicablePseudoClasses(stylesheet, object);
if (pseudoClasses != null) {
Map> combinedPseudoClasses =
new LinkedHashMap<>();
for (Rule pseudoClass1 : pseudoClasses) {
Selector[] selectors = pseudoClass1.getSelectors();
for (Selector selector : selectors) {
if (appliesTo(selector, object) ==
Selector.PSEUDOCLASS_APPLIES) {
properties = pseudoClass1.getProperties();
String pseudoClass = selector.getPseudoClass();
// TODO: overrides by downstream pseudoclasses are not handled
Map combinedProperties =
combinedPseudoClasses.computeIfAbsent(pseudoClass, k -> new HashMap<>());
combinedProperties.putAll(properties);
}
}
}
int count = 0;
for (Map.Entry> e :
combinedPseudoClasses.entrySet()) {
applyPseudoClass(e.getKey(), e.getValue(), object, compiler,
count++);
}
}
}
public static String unwrap(ClassDescriptor type, String valueCode) {
if (ClassDescriptorHelper.getClassDescriptor(boolean.class).equals(type)) {
return "((Boolean) " + valueCode + ").booleanValue()";
}
if (ClassDescriptorHelper.getClassDescriptor(byte.class).equals(type)) {
return "((Byte) " + valueCode + ").byteValue()";
}
if (ClassDescriptorHelper.getClassDescriptor(short.class).equals(type)) {
return "((Short) " + valueCode + ").shortValue()";
}
if (ClassDescriptorHelper.getClassDescriptor(int.class).equals(type)) {
return "((Integer) " + valueCode + ").intValue()";
}
if (ClassDescriptorHelper.getClassDescriptor(long.class).equals(type)) {
return "((Long) " + valueCode + ").longValue()";
}
if (ClassDescriptorHelper.getClassDescriptor(float.class).equals(type)) {
return "((Float) " + valueCode + ").floatValue()";
}
if (ClassDescriptorHelper.getClassDescriptor(double.class).equals(type)) {
return "((Double) " + valueCode + ").doubleValue()";
}
if (ClassDescriptorHelper.getClassDescriptor(char.class).equals(type)) {
return "((Character) " + valueCode + ").charValue()";
}
return valueCode;
}
public enum PseudoClassEnum {
focused("{ object.hasFocus() }"),
unfocused("{ !object.hasFocus() }"),
enabled("{ object.isEnabled() }"),
disabled("{ !object.isEnabled() }"),
selected("{ object.isSelected() }"),
deselected("{ !object.isSelected() }");
final String code;
PseudoClassEnum(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
public static void applyPseudoClass(String pseudoClass,
Map properties,
CompiledObject object,
JAXXCompiler compiler,
int priority) throws CompilerException {
if (pseudoClass.contains("[")) {
pseudoClass = pseudoClass.substring(0, pseudoClass.indexOf("["));
}
StringBuilder buffer = new StringBuilder();
DefaultObjectHandler handler =
TagManager.getTagHandler(object.getObjectClass());
boolean valueDeclared = false;
String eol = JAXXCompiler.getLineSeparator();
DataBindingHelper bindingHelper = compiler.getBindingHelper();
String pseudoClassesPrefix = null;
String dataBindingPrefix = null;
if (!properties.isEmpty()) {
pseudoClassesPrefix = compiler.getImportedType(Pseudoclasses.class);
dataBindingPrefix = compiler.getImportedType(org.nuiton.jaxx.runtime.css.DataBinding.class);
}
String outputClassName =
compiler.getImportedType(compiler.getOutputClassName());
for (Map.Entry e : properties.entrySet()) {
String property = e.getKey();
ClassDescriptor type = handler.getPropertyType(object,
property,
compiler
);
if (log.isDebugEnabled()) {
log.debug("will test if databinding : [" + e.getValue() +
"] type=" + type);
}
String dataBindingCode = compiler.processDataBindings(e.getValue());
String valueCode;
String simpleType = compiler.getImportedType(JAXXCompiler.getCanonicalName(type));
if (dataBindingCode != null) {
String code = object.getId() + "." + property + "." + priority;
valueCode = "new " + dataBindingPrefix + "(" +
TypeManager.getJavaCode(code) + ")";
DataBinding binding = new DataBinding(
code,
dataBindingCode,
handler.getSetPropertyCode(
object.getJavaCode(),
property,
"(" + simpleType + ") " + dataBindingCode,
// "(" + JAXXCompiler.getCanonicalName(type) + ") " + dataBindingCode,
compiler
),
false
);
bindingHelper.registerDataBinding(binding);
} else {
try {
Class> typeClass = type != null ?
ClassDescriptorHelper.getClass(
type.getName(),
type.getClassLoader()
) :
null;
valueCode = TypeManager.getJavaCode(
TypeManager.convertFromString(e.getValue(), typeClass)
);
} catch (ClassNotFoundException ex) {
compiler.reportError(
"could not find class " + type.getName());
return;
}
}
if (!valueDeclared) {
buffer.append("Object ");
valueDeclared = true;
}
buffer.append("value = ");
buffer.append(pseudoClassesPrefix);
buffer.append(".applyProperty(");
buffer.append(outputClassName);
buffer.append(".this, ");
buffer.append(object.getJavaCode());
buffer.append(", ");
buffer.append(TypeManager.getJavaCode(property));
buffer.append(", ");
buffer.append(valueCode);
buffer.append(", ");
buffer.append(pseudoClassesPrefix);
buffer.append(".wrap(");
buffer.append(handler.getGetPropertyCode(object.getJavaCode(), property, compiler));
buffer.append("), ");
buffer.append(priority);
buffer.append(");");
buffer.append(eol);
buffer.append("if (!(value instanceof ");
buffer.append(dataBindingPrefix);
buffer.append(")) {");
buffer.append(eol);
String unwrappedValue = unwrap(type, "value");
buffer.append(" ");
buffer.append(handler.getSetPropertyCode(object.getJavaCode(), property, "(" + simpleType + ") " + unwrappedValue, compiler));
buffer.append(eol);
buffer.append("}").append(eol);
}
try {
PseudoClassEnum classEnum = PseudoClassEnum.valueOf(pseudoClass);
pseudoClass = classEnum.getCode();
} catch (IllegalArgumentException e) {
// should never happens ?
// throw new RuntimeException("could not find " + PseudoClassEnum.class + " with pseudoClass " + pseudoClass, e);
}
compilePseudoClass(pseudoClass, object, buffer.toString(), 0, "add", compiler, false);
buffer.setLength(0);
valueDeclared = false;
for (Map.Entry e : properties.entrySet()) {
String property = e.getKey();
ClassDescriptor type = handler.getPropertyType(object, property, compiler);
String simpleType = compiler.getImportedType(JAXXCompiler.getCanonicalName(type));
if (log.isDebugEnabled()) {
log.debug("will test if databinding : [" + e.getValue() + "] type=" + type);
}
String dataBindingCode = compiler.processDataBindings(e.getValue());
String valueCode;
if (dataBindingCode != null) {
String code = object.getId() + "." + property + "." + priority;
valueCode = "new " + dataBindingPrefix + "(" + TypeManager.getJavaCode(code) + ")";
DataBinding binding = new DataBinding(
code,
dataBindingCode,
handler.getSetPropertyCode(
object.getJavaCode(),
property,
"(" + simpleType + ") " + dataBindingCode,
compiler
),
false
);
bindingHelper.registerDataBinding(binding);
} else {
try {
Class> typeClass =
type != null ?
ClassDescriptorHelper.getClass(type.getName(), type.getClassLoader()) :
null;
valueCode = TypeManager.getJavaCode(TypeManager.convertFromString(e.getValue(), typeClass));
} catch (ClassNotFoundException ex) {
compiler.reportError("could not find class " + type.getName());
return;
}
}
if (!valueDeclared) {
buffer.append("Object ");
valueDeclared = true;
}
buffer.append("value = ").append(pseudoClassesPrefix).append(".removeProperty(");
buffer.append(outputClassName);
buffer.append(".this, ");
buffer.append(object.getJavaCode());
buffer.append(", ");
buffer.append(TypeManager.getJavaCode(property));
buffer.append(", ");
buffer.append(valueCode);
buffer.append(", ").append(pseudoClassesPrefix).append(".wrap(");
buffer.append(handler.getGetPropertyCode(object.getJavaCode(),
property,
compiler)
);
buffer.append("), ");
buffer.append(priority);
buffer.append(");");
buffer.append(eol);
buffer.append("if (!(value instanceof ");
buffer.append(dataBindingPrefix);
buffer.append(")) {");
buffer.append(eol);
// String simpleType = importManager.getType(JAXXCompiler.getCanonicalName(type));
String unwrappedValue = unwrap(type, "value");
buffer.append(" ");
buffer.append(handler.getSetPropertyCode(
object.getJavaCode(),
property,
"(" + simpleType + ") " + unwrappedValue,
compiler)
);
buffer.append(eol);
buffer.append("}").append(eol);
}
compilePseudoClass(pseudoClass,
object,
buffer.toString(),
1,
"remove",
compiler,
true
);
}
public static void compilePseudoClass(String pseudoClass,
CompiledObject object,
String propertyCode,
int pos,
String methodName,
JAXXCompiler compiler,
boolean invertTest) throws CompilerException {
PseudoClassDataBinding binding =
PseudoClassDataBinding.newPseudoClassDataBinding(
pseudoClass,
object,
propertyCode,
methodName,
invertTest
);
if (binding != null) {
compiler.getBindingHelper().registerDataBinding(binding);
return;
}
MouseEventEnum constant = MouseEventEnum.valueOf(pseudoClass);
String property = constant.getProperty(pos);
MethodDescriptor addMouseListener = getAddMouseListenerMethod(object);
MethodDescriptor methodDescriptor =
getMouseListenerMethod(object, property);
object.addEventHandler("style." + pseudoClass + "." + methodName,
addMouseListener,
methodDescriptor,
propertyCode,
compiler
);
}
public static Map getApplicableProperties(
Stylesheet s, CompiledObject object) throws CompilerException {
DefaultObjectHandler handler =
TagManager.getTagHandler(object.getObjectClass());
Map result = null;
for (Rule rule : s.getRules()) {
int apply = appliesTo(rule, object);
if (apply == Selector.ALWAYS_APPLIES ||
apply == Selector.ALWAYS_APPLIES_INHERIT_ONLY) {
if (result == null) {
result = new HashMap<>();
}
for (Map.Entry entry :
rule.getProperties().entrySet()) {
String property = entry.getKey();
if (apply == Selector.ALWAYS_APPLIES || handler.isPropertyInherited(property)) {
String ruleValue = entry.getValue().replace("%%", object.getId());
result.put(property, ruleValue);
}
}
}
}
return result;
}
public static Rule[] getApplicablePseudoClasses(
Stylesheet s, CompiledObject object) throws CompilerException {
List result = null;
for (Rule rule : s.getRules()) {
if (appliesTo(rule, object) == Selector.PSEUDOCLASS_APPLIES) {
if (result == null) {
result = new ArrayList<>();
}
result.add(rule);
}
}
return result != null ? result.toArray(new Rule[result.size()]) : null;
}
public static Rule inlineAttribute(CompiledObject object,
String propertyName,
boolean dataBinding) {
Map properties = new HashMap<>();
properties.put(propertyName, dataBinding ?
Rule.DATA_BINDING :
Rule.INLINE_ATTRIBUTE);
return new Rule(new Selector[]{
new Selector(null, null, null, object.getId(), true)},
properties
);
}
public static int appliesTo(Rule rule,
CompiledObject object) throws CompilerException {
int appliesTo = Selector.NEVER_APPLIES;
for (Selector selector : rule.getSelectors()) {
appliesTo = Math.max(appliesTo(selector, object), appliesTo);
if (appliesTo == Selector.ALWAYS_APPLIES ||
appliesTo == Selector.ALWAYS_APPLIES_INHERIT_ONLY) {
break;
}
}
return appliesTo;
}
public static int appliesTo(Selector selector, CompiledObject object) {
boolean inheritOnly = false;
CompiledObject parent = object;
String javaClassName = selector.getJavaClassName();
String styleClass = selector.getStyleClass();
String pseudoClass = selector.getPseudoClass();
String id = selector.getId();
while (parent != null) {
boolean classMatch = javaClassName == null;
if (!classMatch) {
ClassDescriptor javaClass = parent.getObjectClass();
do {
String name = javaClass.getName();
if (name.equals(javaClassName) ||
name.substring(name.lastIndexOf(".") + 1).equals(javaClassName)) {
classMatch = true;
break;
}
javaClass = javaClass.getSuperclass();
} while (javaClass != null);
}
boolean styleClassMatch = parent.matchStyleClass(styleClass);
String objectId = parent.getId();
objectId = objectId.substring(objectId.lastIndexOf(".") + 1);
boolean idMatch = id == null ||
(' ' + objectId + ' ').contains(' ' + id + ' ');
if (classMatch && styleClassMatch && idMatch) {
if (pseudoClass != null) {
return inheritOnly ?
Selector.PSEUDOCLASS_APPLIES_INHERIT_ONLY :
Selector.PSEUDOCLASS_APPLIES;
} else {
return inheritOnly ?
Selector.ALWAYS_APPLIES_INHERIT_ONLY :
Selector.ALWAYS_APPLIES;
}
}
parent = parent.getParent();
inheritOnly = true;
}
return Selector.NEVER_APPLIES;
}
}