diff --git a/archetypes/domui-hello/src/main/resources/archetype-resources/pom.xml b/archetypes/domui-hello/src/main/resources/archetype-resources/pom.xml index 7b1aa8fd29..93f76a1172 100644 --- a/archetypes/domui-hello/src/main/resources/archetype-resources/pom.xml +++ b/archetypes/domui-hello/src/main/resources/archetype-resources/pom.xml @@ -14,7 +14,7 @@ 3.12.3 1.8 1.8 - 1.5.20 + 1.7.20 UTF-8 9.4.6.v20170531 diff --git a/common/property-annotation-processor/src/main/java/db/annotationprocessing/PropertyAnnotationProcessor.java b/common/property-annotation-processor/src/main/java/db/annotationprocessing/PropertyAnnotationProcessor.java index 253be2c6ac..7bf4b22f0f 100644 --- a/common/property-annotation-processor/src/main/java/db/annotationprocessing/PropertyAnnotationProcessor.java +++ b/common/property-annotation-processor/src/main/java/db/annotationprocessing/PropertyAnnotationProcessor.java @@ -31,8 +31,6 @@ import java.util.TreeMap; import java.util.stream.Collectors; -@SupportedAnnotationTypes({"javax.persistence.Entity", "to.etc.annotations.GenerateProperties"}) -@SupportedSourceVersion(SourceVersion.RELEASE_8) /** * Generates QField classes for every Entity annotated class in the project where this processor is selected. * Leave the default .apt_generated folder as is. @@ -46,6 +44,8 @@ * @author Frits Jalvingh * Created on Feb 3, 2013 */ +@SupportedAnnotationTypes({"javax.persistence.Entity", "to.etc.annotations.GenerateProperties"}) +@SupportedSourceVersion(SourceVersion.RELEASE_8) public class PropertyAnnotationProcessor extends AbstractProcessor { static public final String PERSISTENCE_ANNOTATION = "javax.persistence.Entity"; @@ -61,6 +61,8 @@ public class PropertyAnnotationProcessor extends AbstractProcessor { private SourceVersion m_sourceVersion; + private boolean m_debug = System.getenv().get("DOMUI_ANNDEBUG") != null; + static public final class Property { private final TypeMirror m_type; @@ -128,8 +130,9 @@ public boolean process(Set annotations, RoundEnvironment String pkgName = processingEnv.getElementUtils().getPackageOf(classElement).getQualifiedName().toString(); String entityName = classElement.getSimpleName().toString(); + if(m_debug) + System.out.println("ANN> Processing entity " + entityName); - //String entityName = classElement.asType().toString(); try { List properties = getProperties(classElement); @@ -161,6 +164,8 @@ String getLinkClass(String entityName) { } private JavaFileObject createFile(String name, TypeElement ann) throws IOException { + if((m_debug)) + System.out.println("ANN> createFile " + name + " source " + ann); return processingEnv.getFiler().createSourceFile(name, ann); } diff --git a/common/to.etc.alg/src/main/java/to/etc/util/FileTool.java b/common/to.etc.alg/src/main/java/to/etc/util/FileTool.java index ed7e170ed0..53757137b7 100644 --- a/common/to.etc.alg/src/main/java/to/etc/util/FileTool.java +++ b/common/to.etc.alg/src/main/java/to/etc/util/FileTool.java @@ -1967,7 +1967,7 @@ public static void delete(File file) { try { Files.delete(file.toPath()); } catch(Exception x) { - LOG.error("Failed to delete " + file + ": " + x, x); + LOG.debug("Failed to delete " + file + ": " + x, x); } } } diff --git a/common/to.etc.alg/src/main/java/to/etc/util/SecurityUtils.java b/common/to.etc.alg/src/main/java/to/etc/util/SecurityUtils.java index f29f387fd1..b5ec0f089e 100644 --- a/common/to.etc.alg/src/main/java/to/etc/util/SecurityUtils.java +++ b/common/to.etc.alg/src/main/java/to/etc/util/SecurityUtils.java @@ -266,6 +266,17 @@ static public String getSha256Hex(@NonNull String in, @NonNull Charset encoding) } } + @NonNull + static public String getSha256Base64(@NonNull String in, @NonNull Charset encoding) { + try { + MessageDigest sha = MessageDigest.getInstance("SHA-256"); + byte[] hash = sha.digest(in.getBytes(encoding)); + return StringTool.encodeBase64ToString(hash); + } catch(Exception ex) { + throw WrappedException.wrap(ex); + } + } + static public byte[] createSalt(int bytes) { byte[] salt = new byte[bytes]; RANDOM.nextBytes(salt); diff --git a/common/to.etc.alg/src/main/java/to/etc/util/StringTool.java b/common/to.etc.alg/src/main/java/to/etc/util/StringTool.java index b4b1bfca46..7e68ab6910 100644 --- a/common/to.etc.alg/src/main/java/to/etc/util/StringTool.java +++ b/common/to.etc.alg/src/main/java/to/etc/util/StringTool.java @@ -45,6 +45,8 @@ import java.util.StringTokenizer; import java.util.regex.Pattern; +import static java.lang.Character.isJavaIdentifierPart; + /** * This static utility class contains a load of string functions. And some other * stuff I could not quickly find a place for ;-) @@ -112,7 +114,7 @@ static public boolean isValidJavaIdentifier(@NonNull final String s) { if(!Character.isJavaIdentifierStart(s.charAt(0))) return false; for(int i = 1; i < len; i++) { - if(!Character.isJavaIdentifierPart(s.charAt(i))) + if(!isJavaIdentifierPart(s.charAt(i))) return false; } return true; @@ -3099,6 +3101,71 @@ public static String stripAccents(String s) { return s; } + /** + * Util that locates given qualified name in expression (with ignored casing), and replaces it with new qualified name. + * It ignores other cases when old name is part of naming of other variables in expression. + * It requires that old qualified name prefix and name are named by java identifier convention. + * + * @param expression expression where we replace variables with qualified names + * @param prefixQName prefix in qualified name to replace. + * @param oldName name in qualified name to replace. + * @param newQName new qualified name that replaced old one. + * @return replaced expression. + */ + @Nullable + public static String replaceQualifiedNameInExpression(@Nullable String expression, String prefixQName, String oldName, String newQName) { + if(null == expression) { + return null; + } + String lEntityName = prefixQName.toLowerCase(); + String lOldName = oldName.toLowerCase(); + String literalToFind = lEntityName + "." + lOldName; + return replaceVariableNameInExpression(expression, literalToFind, newQName); + } + + /** + * Util that locates given variable name in expression (with ignored casing), and replaces it with new name. + * It ignores other cases when old name is part of naming of other variables in expression. + * It requires that old and new name are named by java identifier convention. + * + * @param expression expression where we replace variable name + * @param oldName name to replace. + * @return replaced expression. + */ + @Nullable + public static String replaceVariableNameInExpression(@Nullable String expression, String oldName, String newName) { + if(null == expression) { + return null; + } + String lowerCaseExpression = expression.toLowerCase(); + String literalToFind = oldName.toLowerCase(); + int lastReplacedIndex = 0; + int pos = -1; + StringBuilder replaceSb = new StringBuilder(); + do { + pos = lowerCaseExpression.indexOf(literalToFind, pos + 1); + if(pos > -1) { + Character nextChar = null; + if(lowerCaseExpression.length() > pos + literalToFind.length()) { + nextChar = lowerCaseExpression.charAt(pos + literalToFind.length()); + } + Character prevChar = null; + if(pos > 0) { + prevChar = expression.charAt(pos - 1); + } + if((nextChar == null || !isJavaIdentifierPart(nextChar)) && (prevChar == null || !isJavaIdentifierPart(prevChar))) { + replaceSb + .append(expression.substring(lastReplacedIndex, pos)) + .append(newName); + lastReplacedIndex = pos + literalToFind.length(); + } + } else if(lastReplacedIndex < expression.length()) { + replaceSb.append(expression.substring(lastReplacedIndex)); + } + } while(pos >= 0); + return replaceSb.toString(); + } + /** * This method checks that the name passed only contains a name, * and nothing that looks like a SQL injection. @@ -3121,6 +3188,67 @@ static public void sqlCheckNoQuotes(@Nullable String password) { if(null != password && password.contains("'")) throw new IllegalArgumentException("Invalid characters in SQL"); } + + static private final char[] PUNCT = "!#_^&*.;".toCharArray(); + + static private final char[] DIGITS = "023456789".toCharArray(); + + static private final char[] LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + + /** + * Generate a reasonably secure password. + */ + static public String generatePassword(int nchar) { + return generatePassword(nchar, 2, 2); + } + + /* + * Postgres' passwords should not include dollar signs nor percentage signs. + * jal 20200608 actually postgres is OK with it, it's Azure Tabular that dies with it. + */ + static public String generatePassword(int nchar, int punctuation, int digits) { + if(nchar < 6) + throw new IllegalStateException("Don't be silly."); + + char[] buf = new char[nchar]; // Password buffer + + //-- Randomly assign the #of punctuation chars + while(punctuation > 0) { + char c = PUNCT[m_random.nextInt(PUNCT.length)]; // Random punctuation + + for(; ; ) { + int pos = m_random.nextInt(nchar); // Get a position + if(buf[pos] == 0) { + buf[pos] = c; + break; + } + } + punctuation--; + } + + //-- Randomly assign digits + while(digits > 0) { + char c = DIGITS[m_random.nextInt(DIGITS.length)]; // Random punctuation + + for(; ; ) { + int pos = m_random.nextInt(nchar); // Get a position + if(buf[pos] == 0) { + buf[pos] = c; + break; + } + } + digits--; + } + + //-- And finally: fill the rest with random letters. + for(int i = 0; i < nchar; i++) { + if(buf[i] == 0) { + buf[i] = LETTERS[m_random.nextInt(LETTERS.length)]; + } + } + return new String(buf); + } + } diff --git a/common/to.etc.alg/src/test/java/to/etc/util/StringToolTest.java b/common/to.etc.alg/src/test/java/to/etc/util/StringToolTest.java deleted file mode 100644 index 3326c846af..0000000000 --- a/common/to.etc.alg/src/test/java/to/etc/util/StringToolTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package to.etc.util; - -import org.junit.Test; - -import java.util.List; - -import static junit.framework.TestCase.assertEquals; - -public class StringToolTest { - - @Test - public void testStripAccents() { - var examples = List.of( - new Pair<>("filë_name.xls", "file_name.xls"), - new Pair<>("ćčćč", "cccc") ); - for(var example : examples) { - var rename = StringTool.stripAccents(example.get1()); - assertEquals(example.get2(), rename); - } - } -} diff --git a/common/to.etc.alg/src/test/java/to/etc/util/TestStringTool.java b/common/to.etc.alg/src/test/java/to/etc/util/TestStringTool.java index 04ed7541a2..d0c91a547c 100644 --- a/common/to.etc.alg/src/test/java/to/etc/util/TestStringTool.java +++ b/common/to.etc.alg/src/test/java/to/etc/util/TestStringTool.java @@ -8,7 +8,10 @@ import java.util.Arrays; import java.util.List; +import static junit.framework.TestCase.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; /** @@ -456,4 +459,27 @@ public void testRemoveRepeatingCharacters_whenHasRepeatingDigits_dontShortenStri res = StringTool.removeRepeatingCharacters(s); Assert.assertEquals("abc1111111abc 123456789123456789", res); } + + @Test + public void testStripAccents() { + var examples = List.of( + new Pair<>("filë_name.xls", "file_name.xls"), + new Pair<>("ćčćč", "cccc") ); + for(var example : examples) { + var rename = StringTool.stripAccents(example.get1()); + assertEquals(example.get2(), rename); + } + } + + @Test + public void testReplaceQualifiedNameInExpression() { + assertEquals(StringTool.replaceQualifiedNameInExpression("a.aaaa + a.aaab-a.aAa", "A", "aaa", "Aa.ccc"), "a.aaaa + a.aaab-Aa.ccc"); + assertEquals(StringTool.replaceQualifiedNameInExpression("aa.aaa-A.aaa*1-a.AAa ", "A", "aaa", "Aa.ccc"), "aa.aaa-Aa.ccc*1-Aa.ccc "); + } + + @Test + public void testReplaceVariableNameInExpression() { + assertEquals(StringTool.replaceVariableNameInExpression("aaaa + aaab-aAa", "aaa", "ccc"), "aaaa + aaab-ccc"); + assertEquals(StringTool.replaceVariableNameInExpression("baaa-aaa*1-AAa ", "aaa", "ccc"), "baaa-ccc*1-ccc "); + } } diff --git a/examples/skeleton/pom.xml b/examples/skeleton/pom.xml index 009a6a1423..5a7f57f2e3 100644 --- a/examples/skeleton/pom.xml +++ b/examples/skeleton/pom.xml @@ -37,7 +37,7 @@ 1.8 1.8 3.12.3 - 1.5.20 + 1.7.20 3.3.0 @@ -249,7 +249,7 @@ org.postgresql postgresql - 42.4.1 + 42.5.1 diff --git a/integrations/to.etc.domui.hibutil/src/main/java/to/etc/domui/hibernate/idgen/UUIDGenerator23.java b/integrations/to.etc.domui.hibutil/src/main/java/to/etc/domui/hibernate/idgen/UUIDGenerator23.java index 3c2f3be132..796fe891b3 100644 --- a/integrations/to.etc.domui.hibutil/src/main/java/to/etc/domui/hibernate/idgen/UUIDGenerator23.java +++ b/integrations/to.etc.domui.hibutil/src/main/java/to/etc/domui/hibernate/idgen/UUIDGenerator23.java @@ -29,16 +29,10 @@ * Created on 12-2-18. */ final public class UUIDGenerator23 implements IdentifierGenerator { + @Override public Serializable generate(SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException { - UUID uuid = UUID.randomUUID(); - byte[] data = new byte[16]; - - moveBytes(data, 0, uuid.getMostSignificantBits()); - moveBytes(data, 8, uuid.getLeastSignificantBits()); - String str = StringTool.encodeBase64ToString(data); - - return str.substring(0, 23); // Strip the == + return createUUID(); } static private void moveBytes(byte[] bytes, int offset, long bits) { diff --git a/integrations/to.etc.domui.selenium/pom.xml b/integrations/to.etc.domui.selenium/pom.xml index ae78375b7b..b078e6f313 100644 --- a/integrations/to.etc.domui.selenium/pom.xml +++ b/integrations/to.etc.domui.selenium/pom.xml @@ -104,19 +104,10 @@ - - org.seleniumhq.selenium - htmlunit-driver - - - - - - - - - - + + + + diff --git a/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverConnector.java b/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverConnector.java index a63f60eeac..46e88145d2 100644 --- a/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverConnector.java +++ b/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverConnector.java @@ -223,12 +223,14 @@ static public WebDriverConnector get() throws Exception { @NonNull private static WebDriverType getDriverType(@Nullable String hubUrl) { - if(null == hubUrl || hubUrl.trim().isEmpty()) - return WebDriverType.HTMLUNIT; // Used as a target because it can emulate multiple browser types - if("local".equals(hubUrl.trim())) - return WebDriverType.LOCAL; - if(hubUrl.startsWith(BROWSERSTACK)) { - return WebDriverType.BROWSERSTACK; + if(null != hubUrl) { + //if(null == hubUrl || hubUrl.trim().isEmpty()) + // return WebDriverType.HTMLUNIT; // Used as a target because it can emulate multiple browser types + if("local".equals(hubUrl.trim())) + return WebDriverType.LOCAL; + if(hubUrl.startsWith(BROWSERSTACK)) { + return WebDriverType.BROWSERSTACK; + } } return WebDriverType.REMOTE; } diff --git a/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverFactory.java b/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverFactory.java index 0a928478bb..2607d97989 100644 --- a/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverFactory.java +++ b/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverFactory.java @@ -4,7 +4,6 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openqa.selenium.Capabilities; import org.openqa.selenium.Dimension; import org.openqa.selenium.UnexpectedAlertBehaviour; import org.openqa.selenium.WebDriver; @@ -14,7 +13,6 @@ import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.firefox.FirefoxOptions; import org.openqa.selenium.firefox.FirefoxProfile; -import org.openqa.selenium.htmlunit.HtmlUnitDriver; import org.openqa.selenium.ie.InternetExplorerDriver; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DesiredCapabilities; @@ -60,8 +58,8 @@ public static WebDriver allocateInstance(WebDriverType type, BrowserModel browse default: throw new IllegalStateException("? unhandled driver type"); - case HTMLUNIT: - return allocateHtmlUnitInstance(browser, lang); + //case HTMLUNIT: + // return allocateHtmlUnitInstance(browser, lang); case LOCAL: return allocateLocalInstance(browser, lang); @@ -199,10 +197,10 @@ private static WebDriver allocateBrowserStack(BrowserModel browser, String hubur return dir; } - private static WebDriver allocateHtmlUnitInstance(BrowserModel browser, Locale lang) throws Exception { - Capabilities capabilities = calculateCapabilities(browser, lang); - return new HtmlUnitDriver(capabilities); - } + //private static WebDriver allocateHtmlUnitInstance(BrowserModel browser, Locale lang) throws Exception { + // Capabilities capabilities = calculateCapabilities(browser, lang); + // return new HtmlUnitDriver(capabilities); + //} private static WebDriver allocateRemoteInstance(BrowserModel browser, @NonNull String hubUrl, Locale lang) throws Exception { return new RemoteWebDriver(new URL(hubUrl), calculateCapabilities(browser, lang)); @@ -494,24 +492,16 @@ private static DesiredCapabilities getChromeHeadlessCapabilities(Locale lang) { return options; } - //private static DesiredCapabilities getPhantomCapabilities(Locale lang) { - // DesiredCapabilities capabilities = DesiredCapabilities.phantomjs(); - // String value = lang.getLanguage().toLowerCase(); - // capabilities.setCapability(PhantomJSDriverService.PHANTOMJS_PAGE_CUSTOMHEADERS_PREFIX + "Accept-Language", value); - // capabilities.setCapability(CapabilityType.UNEXPECTED_ALERT_BEHAVIOUR, UnexpectedAlertBehaviour.IGNORE); - // return capabilities; - //} - @Nullable public static IWebdriverScreenshotHelper getScreenshotHelper(WebDriverType webDriverType, BrowserModel browserModel) { - switch(webDriverType) { - default: - break; - - case HTMLUNIT: - //-- HTMLUNIT does not render, so it cannot create screenshots. - return null; - } + //switch(webDriverType) { + // default: + // break; + // + // case HTMLUNIT: + // //-- HTMLUNIT does not render, so it cannot create screenshots. + // return null; + //} switch(browserModel) { default: diff --git a/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverType.java b/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverType.java index 9dce081537..3a2ea27dcb 100644 --- a/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverType.java +++ b/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/core/WebDriverType.java @@ -5,5 +5,5 @@ * Created on 20-3-17. */ public enum WebDriverType { - LOCAL, HTMLUNIT, BROWSERSTACK, REMOTE + LOCAL, BROWSERSTACK, REMOTE } diff --git a/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/poproxies/AbstractCpPage.java b/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/poproxies/AbstractCpPage.java index 011eea43ac..8ce8326922 100644 --- a/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/poproxies/AbstractCpPage.java +++ b/integrations/to.etc.domui.selenium/src/main/java/to/etc/domui/webdriver/poproxies/AbstractCpPage.java @@ -1,7 +1,7 @@ package to.etc.domui.webdriver.poproxies; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.net.URLEncodedUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openqa.selenium.By; import org.openqa.selenium.support.ui.ExpectedConditions; diff --git a/parent/pom.xml b/parent/pom.xml index c1efa8b69d..72df610a4f 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -56,7 +56,7 @@ 11 3.20.0 - 1.6.20 + 1.7.20 3.3.0 @@ -100,7 +100,7 @@ 1.7.7 0.7.5 - 1.8 + 1.16 3.4 1.1.4 @@ -115,7 +115,7 @@ 9.4.14.v20181114 3.141.59 - 2.7.0 + 2.7.1 2.3.0 @@ -314,7 +314,7 @@ org.postgresql postgresql - 42.5.0 + 42.5.1 @@ -522,11 +522,11 @@ 1.15.0 - - org.seleniumhq.selenium - htmlunit-driver - 2.50.0 - + + + + + diff --git a/to.etc.domui.demo/pom.xml b/to.etc.domui.demo/pom.xml index 4ade32d101..de8fb87ecc 100644 --- a/to.etc.domui.demo/pom.xml +++ b/to.etc.domui.demo/pom.xml @@ -245,7 +245,7 @@ --> maven-resources-plugin - 3.2.0 + 3.3.0 copy-resources diff --git a/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/BindError1Page.java b/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/BindError1Page.java index 597ecd2833..c61d6c7009 100644 --- a/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/BindError1Page.java +++ b/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/BindError1Page.java @@ -10,10 +10,10 @@ /** * Test for the following: - * - * - a mandatoty control bound to a model has an initial value through the model (i.e. the start value is not null) + *

+ * - a mandatory control bound to a model has an initial value through the model (i.e. the start value is not null) * - clear the value in the control, then press the click button which validates the bindings - * + *

* expected result: as the control is now empty it is invalid, and a mandatory error must be shown. * actual result before: the validation fails, but no message is shown. * @@ -23,7 +23,8 @@ public class BindError1Page extends UrlPage { private String m_fullName = "Hello ladies"; - @Override public void createContent() throws Exception { + @Override + public void createContent() throws Exception { Text2 text = new Text2<>(String.class); add(text); text.setMandatory(true); @@ -31,7 +32,7 @@ public class BindError1Page extends UrlPage { text.bind().to(this, "fullName"); - add(new DefaultButton("click", a-> save())); + add(new DefaultButton("click", a -> save())); } private void save() throws Exception { diff --git a/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/BindvalidationErrorPage.java b/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/BindvalidationErrorPage.java new file mode 100644 index 0000000000..304c463282 --- /dev/null +++ b/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/BindvalidationErrorPage.java @@ -0,0 +1,58 @@ +package to.etc.domuidemo.pages.test.binding.binderrors; + +import to.etc.domui.component.buttons.DefaultButton; +import to.etc.domui.component.misc.VerticalSpacer; +import to.etc.domui.component2.form4.FormBuilder; +import to.etc.domui.dom.html.Div; +import to.etc.domui.dom.html.TextArea; +import to.etc.domui.dom.html.UrlPage; + +/** + * @author Frits Jalvingh + * Created on 10-11-22. + */ +final public class BindvalidationErrorPage extends UrlPage { + private String m_value; + + private final Div m_resultDiv = new Div(); + + @Override + public void createContent() throws Exception { + m_value = "bad"; + TextArea ta = new TextArea(80, 2); + ta.addValidator(new TestValueValidator()); + FormBuilder fb = new FormBuilder(this); + fb.property(this, "value").control(ta); + + ta.setTestID("text"); + + DefaultButton click = new DefaultButton("Click", a -> handleClick()); + add(click); + click.setTestID("click"); + + add(new VerticalSpacer(10)); + add(m_resultDiv); + m_resultDiv.setTestID("result"); + } + + private void handleClick() throws Exception { + m_resultDiv.removeAllChildren(); + if(bindErrors()) { + m_resultDiv.add("Failed"); + m_resultDiv.addCssClass("test-failed"); + m_resultDiv.removeCssClass("test-ok"); + } else { + m_resultDiv.add("worked"); + m_resultDiv.removeCssClass("test-failed"); + m_resultDiv.addCssClass("test-ok"); + } + } + + public String getValue() { + return m_value; + } + + public void setValue(String value) { + m_value = value; + } +} diff --git a/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/TestValueValidator.java b/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/TestValueValidator.java new file mode 100644 index 0000000000..6d4d1fbac4 --- /dev/null +++ b/to.etc.domui.demo/src/main/java/to/etc/domuidemo/pages/test/binding/binderrors/TestValueValidator.java @@ -0,0 +1,22 @@ +package to.etc.domuidemo.pages.test.binding.binderrors; + +import to.etc.domui.converter.IValueValidator; +import to.etc.domui.trouble.ValidationException; +import to.etc.domui.util.Msgs; + +/** + * Test validator which accepts only the value "good". + * + * @author Frits Jalvingh + * Created on 10-11-22. + */ +final public class TestValueValidator implements IValueValidator { + @Override + public void validate(String input) throws Exception { + if(null == input) + return; + if(input.equals("good")) + return; + throw new ValidationException(Msgs.vInvalid, input); + } +} diff --git a/to.etc.domui.demo/src/test/java/to/etc/domuidemo/pages/test/binding/ITBindValidationError.java b/to.etc.domui.demo/src/test/java/to/etc/domuidemo/pages/test/binding/ITBindValidationError.java new file mode 100644 index 0000000000..94005508a1 --- /dev/null +++ b/to.etc.domui.demo/src/test/java/to/etc/domuidemo/pages/test/binding/ITBindValidationError.java @@ -0,0 +1,35 @@ +package to.etc.domuidemo.pages.test.binding; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import to.etc.domui.webdriver.core.AbstractWebDriverTest; +import to.etc.domuidemo.pages.test.binding.binderrors.BindvalidationErrorPage; + +import java.util.concurrent.TimeUnit; + +/** + * @author Frits Jalvingh + * Created on 10-11-22. + */ +final public class ITBindValidationError extends AbstractWebDriverTest { + @Test + public void testValidationErrorIsShown() throws Exception { + wd().openScreen(BindvalidationErrorPage.class); + + //-- Initially the value shown must be "bad" + String value = wd().getValue("text"); + Assert.assertEquals("The initial value in the control must be correct", "bad", value); + + //-- Now: enter another (incorrect) value + String incorrectValue = "incorrect"; + wd().cmd().type(incorrectValue).on("text"); + wd().cmd().click().on("click"); + + //-- We should have a failure + wd().wait(By.className("test-failed"), 2, TimeUnit.SECONDS); + + String newval = wd().getValue("text"); + Assert.assertEquals("The incorrect value entered should still be seen in the control", incorrectValue, newval); + } +} diff --git a/to.etc.domui/src/main/java/to/etc/domui/component/ace/AceEditor.java b/to.etc.domui/src/main/java/to/etc/domui/component/ace/AceEditor.java index 109f2da613..9df99c9844 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component/ace/AceEditor.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component/ace/AceEditor.java @@ -185,6 +185,7 @@ public void createContent() throws Exception { sb.append("ed.__id='").append(editorId).append("';\n"); sb.append("var Range = require('ace/range').Range;\n"); sb.append("window['").append(editorId).append("'] = ed;\n"); + sb.append("ed.setBehavioursEnabled(true);\n"); sb.append("WebUI.registerInputControl('").append(editorId).append("', {getInputField: function(fields) {"); sb.append(" let select = ed.getSelectedText();\n"); sb.append(" fields['").append(editorId).append("_s'] = select;\n"); diff --git a/to.etc.domui/src/main/java/to/etc/domui/component/binding/AbstractComponentPropertyBinding.java b/to.etc.domui/src/main/java/to/etc/domui/component/binding/AbstractComponentPropertyBinding.java index 971e0b43e9..158b7a196b 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component/binding/AbstractComponentPropertyBinding.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component/binding/AbstractComponentPropertyBinding.java @@ -115,7 +115,7 @@ public M getInstance() { } //@Nullable - public IValueAccessor getInstanceProperty() { + public PropertyMetaModel getInstanceProperty() { return m_instanceProperty; } diff --git a/to.etc.domui/src/main/java/to/etc/domui/component/binding/ComponentPropertyBindingBidi.java b/to.etc.domui/src/main/java/to/etc/domui/component/binding/ComponentPropertyBindingBidi.java index e51e0c672f..2757f27937 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component/binding/ComponentPropertyBindingBidi.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component/binding/ComponentPropertyBindingBidi.java @@ -122,7 +122,26 @@ public BindingValuePair getBindingDifference() throws Exception { m_bindError = null; } catch(CodeException cx) { controlModelValue = null; - m_lastValueFromControlAsModelValue = null; + /* + * 20221110 jal Commented out because it seems wrong. With it, the following happens. + * Have a TextArea with a Validator and some initial (invalid) value coming from a Model. Now + * change the value to some other (invalid) value and save the screen. This SHOULD report an + * error. Instead what happens is this: + * - No error is shown + * - The screen save does not complete however + * - The control gets back its PREVIOUS (incorrect) value! + * + * The reason is this assignment. The last value field is used to check whether the data + * in the MODEL actually changed inside AbstractComponentPropertyBinding.moveModelToControl. We + * only want to change the value inside the CONTROL when the MODEL has a change if the control is + * in error. If the control is in error and the model value does not change the control needs to + * retain its incorrect value. By clearing that last-read value here we effectively say that the + * model value WAS null previous time, and as it is not the control gets overwritten from the + * actual value. + * + * ERRONEOUS STATEMENT: + * m_lastValueFromControlAsModelValue = null; + */ newError = UIMessage.error(cx); newError.setErrorNode(control); newError.setErrorLocation(control.getErrorLocation()); @@ -134,18 +153,29 @@ public BindingValuePair getBindingDifference() throws Exception { //System.out.println("~~ " + control + " to " + instanceProperty + ": " + cx); } - //-- When in error we cannot set anything anyway, so exit. - if(null != newError && !newError.getCode().equals(Msgs.mandatory)) { - /* - * jal 20171018 When a mandatory LookupInput gets cleared its value becomes null, and this - * value should be propagated to the model. It seems likely that in ALL cases of error - * we need to move a null there! - */ + MV currentModelValue = getValueFromModel(); + + if(null != newError) { + //-- When in error the only option we have is to set something to null.. We only do that for the mandatory error if possible + if(newError.getCode().equals(Msgs.mandatory)) { + /* + * jal 20171018 When a mandatory LookupInput gets cleared its value becomes null, and this + * value should be propagated to the model. It seems likely that in ALL cases of error + * we need to move a null there! + * + * jal 20221110 but only if the property can accept that, i.e. is not a primitive.. + */ + if(! MetaManager.areObjectsEqual(currentModelValue, controlModelValue) && ! getInstanceProperty().getActualType().isPrimitive()) { + //-- We WILL set the value of the MODEL to null, but we need to KEEP the value in the control + m_lastValueFromControlAsModelValue = null; // This should make sure the control does NOT get updated + return new BindingValuePair<>(this, null); + } + } + //-- For all other errors: leave the value be return null; } - MV currentModelValue = getValueFromModel(); if(MetaManager.areObjectsEqual(currentModelValue, controlModelValue)) return null; diff --git a/to.etc.domui/src/main/java/to/etc/domui/component/binding/OldBindingHandler.java b/to.etc.domui/src/main/java/to/etc/domui/component/binding/OldBindingHandler.java index d20115332b..459ba3ed02 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component/binding/OldBindingHandler.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component/binding/OldBindingHandler.java @@ -18,12 +18,13 @@ * This is the default binding manager. * * @author Frits Jalvingh - * Created on 12-3-17. + * Created on 12-3-17. */ final public class OldBindingHandler { static public final String BINDING_ERROR = "BindingError"; - private OldBindingHandler() {} + private OldBindingHandler() { + } /** * System helper method to move all bindings from control into the model (called at request start). @@ -73,7 +74,7 @@ public Object after(NodeBase n) throws Exception { * errors. */ static public boolean reportBindingErrors(@NonNull NodeBase root) throws Exception { - final boolean[] silly = new boolean[1]; // Not having free variables is a joke. + final boolean[] silly = new boolean[1]; // Not having free variables is a joke. DomUtil.walkTreeUndelegated(root, new DomUtil.IPerNode() { @Override public Object before(NodeBase n) throws Exception { @@ -82,7 +83,7 @@ public Object before(NodeBase n) throws Exception { List list = n.getBindingList(); if(null != list) { - List bindErrorList= new ArrayList<>(); + List bindErrorList = new ArrayList<>(); //-- Find all bindings with an error for(IBinding sb : list) { @@ -94,7 +95,7 @@ public Object before(NodeBase n) throws Exception { //-- If there is an error somewhere- report the 1st one on the component if(!bindErrorList.isEmpty()) { - UIMessage message = bindErrorList.get(0); // Report the first error as the binding error. + UIMessage message = bindErrorList.get(0); // Report the first error as the binding error. message.group(BINDING_ERROR); silly[0] = true; n.setMessage(message); @@ -126,7 +127,7 @@ public Object after(NodeBase n) throws Exception { } @Nullable - public static ComponentPropertyBindingBidi findBinding(NodeBase nodeBase, String string) { + public static ComponentPropertyBindingBidi findBinding(NodeBase nodeBase, String string) { List list = nodeBase.getBindingList(); if(list != null) { for(IBinding sb : list) { diff --git a/to.etc.domui/src/main/java/to/etc/domui/component/ckeditor/CKEditResPart.java b/to.etc.domui/src/main/java/to/etc/domui/component/ckeditor/CKEditResPart.java index 2e18e603fa..a4225471b0 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component/ckeditor/CKEditResPart.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component/ckeditor/CKEditResPart.java @@ -116,7 +116,7 @@ private IBrowserOutput defaultHeader(RequestContextImpl ctx, String cmd, String DomApplication.get().getDefaultHTTPHeaderMap().forEach((header, value) -> rr.addHeader(header, value)); Writer outputWriter = ctx.getOutputWriter("text/xml; charset=UTF-8", "utf-8"); - IBrowserOutput w = new PrettyXmlOutputWriter(outputWriter); + IBrowserOutput w = new PrettyXmlOutputWriter(outputWriter, null); w.tag("Connector"); w.attr("command", cmd); w.attr("resourceType", rtype); @@ -200,7 +200,7 @@ private void sendInit(DomApplication app, IEditorFileSystem ifs, RequestContextI IRequestResponse rr = ctx.getRequestResponse(); DomApplication.get().getDefaultHTTPHeaderMap().forEach((header, value) -> rr.addHeader(header, value)); - IBrowserOutput w = new PrettyXmlOutputWriter(ctx.getOutputWriter("text/xml; charset=UTF-8", "utf-8")); + IBrowserOutput w = new PrettyXmlOutputWriter(ctx.getOutputWriter("text/xml; charset=UTF-8", "utf-8"), null); w.tag("Connector"); w.endtag(); w.tag("Error"); diff --git a/to.etc.domui/src/main/java/to/etc/domui/component/input/AbstractLookupInputBase.java b/to.etc.domui/src/main/java/to/etc/domui/component/input/AbstractLookupInputBase.java index b81e5f278a..7a8bcfee95 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component/input/AbstractLookupInputBase.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component/input/AbstractLookupInputBase.java @@ -29,8 +29,10 @@ import to.etc.util.StringTool; import to.etc.util.WrappedException; import to.etc.webapp.query.QCriteria; +import to.etc.webapp.query.QField; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -135,6 +137,9 @@ protected enum RebuildCause { @Nullable private String m_selectionCssClass; + @Nullable + private List> m_customSearchFields; + @Nullable private IClickableRowRenderer m_formRowRenderer; @@ -763,6 +768,20 @@ public List getKeywordLookupPropertyList() { return m_keywordLookupPropertyList; } + @Nullable + public List> getCustomSearchFields() { + return m_customSearchFields; + } + + /** The search properties to use in the lookup form when created. If null uses the default attributes on the class. */ + public void setCustomSearchFields(@Nullable List> searchFields) { + m_customSearchFields = searchFields; + } + + /** The search properties to use in the lookup form when created. If null uses the default attributes on the class. */ + public void setSearchProperties(QField... searchFields) { + m_customSearchFields = Arrays.asList(searchFields); + } /** * Returns configured custom {@link IClickableRowRenderer}<OT> render for rows when the popup lookup form is used. * diff --git a/to.etc.domui/src/main/java/to/etc/domui/component/input/LookupInputBase.java b/to.etc.domui/src/main/java/to/etc/domui/component/input/LookupInputBase.java index 6676816959..5ef529c482 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component/input/LookupInputBase.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component/input/LookupInputBase.java @@ -763,6 +763,7 @@ public void setKeySearchHint(@Nullable String keySearchHint) { * Set the list of lookup properties to use for lookup in the lookup form, when shown. * @return */ + @Nullable public List getSearchProperties() { return m_searchPropertyList; } diff --git a/to.etc.domui/src/main/java/to/etc/domui/component/layout/TabPanelBase.java b/to.etc.domui/src/main/java/to/etc/domui/component/layout/TabPanelBase.java index 2bcc92e742..ff9fd5fd9d 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component/layout/TabPanelBase.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component/layout/TabPanelBase.java @@ -60,6 +60,13 @@ public interface ITabSelected { void onTabSelected(TabPanelBase tabPanel, int oldTabIndex, int newTabIndex) throws Exception; } + /** + * Represents check if tab can be selected. + */ + public interface ITabSelectable { + boolean isTabSelectable(ITabHandle currentSelection, ITabHandle newSelection) throws Exception; + } + private List m_tablist = new ArrayList(); /** @@ -72,8 +79,15 @@ public interface ITabSelected { */ final private boolean m_markErrorTabs; + @Nullable private ITabSelected m_onTabSelected; + /** + * Used when we need to have explicit control if certain tab can be selected in given moment. + */ + @Nullable + private ITabSelectable m_onTabSelectable; + @Nullable private TabBuilder m_tabBuilder; @@ -382,8 +396,17 @@ public void setCurrentTab(int index) throws Exception { return; //-- We must switch the styles on the current "active" panel and the current "old" panel int oldIndex = getCurrentTab(); + TabInstance oldti = m_tablist.get(getCurrentTab()); // Get the currently active instance, TabInstance newti = m_tablist.get(index); + + ITabSelectable onTabSelectable = m_onTabSelectable; + if(null != onTabSelectable) { + if(!onTabSelectable.isTabSelectable(oldti, newti)) { + return; + } + } + NodeBase oldc = oldti.getContent(); oldc.setDisplay(DisplayType.NONE); // Switch displays on content @@ -403,8 +426,9 @@ public void setCurrentTab(int index) throws Exception { if(null != newtab) newtab.addCssClass("ui-tab-sel"); - if(m_onTabSelected != null) { - m_onTabSelected.onTabSelected(this, oldIndex, index); + ITabSelected onTabSelected = m_onTabSelected; + if(onTabSelected != null) { + onTabSelected.onTabSelected(this, oldIndex, index); } INotify onHide = oldti.getOnHide(); @@ -428,15 +452,29 @@ public int getTabCount() { return m_tablist.size(); } - - public void setOnTabSelected(ITabSelected onTabSelected) { + public void setOnTabSelected(@Nullable ITabSelected onTabSelected) { m_onTabSelected = onTabSelected; } + @Nullable public ITabSelected getOnTabSelected() { return m_onTabSelected; } + /** + * If used, please make sure that there is some UI that makes clear that selection of tab has failed due to some check failure. + * + * @param onTabSelectable tab selectable check handler + */ + public void setOnTabSelectable(@Nullable ITabSelectable onTabSelectable) { + m_onTabSelectable = onTabSelectable; + } + + @Nullable + public ITabSelectable getOnTabSelectable() { + return m_onTabSelectable; + } + public int getTabIndex(NodeBase tabContent) { for(TabInstance tab : m_tablist) { if(tab.getContent() == tabContent) { diff --git a/to.etc.domui/src/main/java/to/etc/domui/component2/lookupinput/DefaultPopupOpener.java b/to.etc.domui/src/main/java/to/etc/domui/component2/lookupinput/DefaultPopupOpener.java index b9b579518a..bbb6fb1579 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component2/lookupinput/DefaultPopupOpener.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component2/lookupinput/DefaultPopupOpener.java @@ -4,17 +4,33 @@ import org.eclipse.jdt.annotation.Nullable; import to.etc.domui.component.layout.Dialog; import to.etc.domui.component.layout.IWindowClosed; +import to.etc.domui.component.meta.SearchPropertyMetaModel; import to.etc.domui.component.tbl.IClickableRowRenderer; import to.etc.domui.component.tbl.ITableModel; import to.etc.domui.component2.lookupinput.LookupInputBase2.IPopupOpener; import to.etc.domui.dom.html.IClicked; import to.etc.function.IExecute; +import java.util.List; + public class DefaultPopupOpener implements IPopupOpener { + @Nullable + private List m_searchPropertyList; + @Nullable private IClickableRowRenderer m_formRowRenderer; + @Nullable + public List getSearchPropertyList() { + return m_searchPropertyList; + } + + /** The search properties to use in the lookup form when created. If null uses the default attributes on the class. */ + public void setSearchPropertyList(@Nullable List searchPropertyList) { + m_searchPropertyList = searchPropertyList; + } + /** * Returns configured custom {@link IClickableRowRenderer}<OT> render for rows when the popup lookup form is used. * @@ -45,6 +61,8 @@ public void clicked(DefaultLookupInputDialog clickednode) throws Exception } }); + dlg.setSearchProperties(getSearchPropertyList()); + //-- Move all extra stuff needed String ttl = control.getDefaultTitle(); dlg.title(ttl); diff --git a/to.etc.domui/src/main/java/to/etc/domui/component2/lookupinput/LookupInputBase2.java b/to.etc.domui/src/main/java/to/etc/domui/component2/lookupinput/LookupInputBase2.java index 7e6720e695..b4b7341529 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/component2/lookupinput/LookupInputBase2.java +++ b/to.etc.domui/src/main/java/to/etc/domui/component2/lookupinput/LookupInputBase2.java @@ -33,7 +33,9 @@ import to.etc.domui.component.input.SimpleLookupInputRenderer; import to.etc.domui.component.layout.Dialog; import to.etc.domui.component.meta.ClassMetaModel; +import to.etc.domui.component.meta.MetaManager; import to.etc.domui.component.meta.SearchPropertyMetaModel; +import to.etc.domui.component.meta.impl.SearchPropertyMetaModelImpl; import to.etc.domui.component.tbl.BasicRowRenderer; import to.etc.domui.component.tbl.IClickableRowRenderer; import to.etc.domui.component.tbl.IQueryHandler; @@ -52,9 +54,11 @@ import to.etc.domui.util.Msgs; import to.etc.webapp.ProgrammerErrorException; import to.etc.webapp.query.QCriteria; +import to.etc.webapp.query.QField; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; abstract public class LookupInputBase2 extends AbstractLookupInputBase implements IControl, ITypedControl, IHasModifiedIndication, IQueryManipulator, IForTarget { private static boolean m_globalDisableSelectOne = false; @@ -80,6 +84,8 @@ protected void setKeySearch(@Nullable SearchInput2 keySearch) { private int m_keyWordSearchPopupWidth; + private String m_keyWordSearchPopupMaxHeight; + @Nullable private INotify

m_onPopupOpen; @@ -307,6 +313,13 @@ public void setOnPopupOpen(@Nullable INotify onPopupOpen) { @NonNull private IPopupOpener createPopupOpener() { DefaultPopupOpener po = new DefaultPopupOpener<>(); + List> searchProps = getCustomSearchFields(); + if(null != searchProps) { + po.setSearchPropertyList(searchProps + .stream().map(sp -> new SearchPropertyMetaModelImpl(getQueryMetaModel(), MetaManager.getPropertyMeta(getQueryClass(), sp))) + .collect(Collectors.toList())); + } + IClickableRowRenderer rr = getFormRowRenderer(); if(null != rr) { po.setFormRowRenderer(rr); @@ -353,6 +366,9 @@ private void openResultsPopup(@NonNull ITableModel model) throws Exception { IRenderInto renderer = new DefaultPopupRowRenderer(getOutputMetaModel()); SelectOnePanel pnl = m_selectPanel = new SelectOnePanel(list, renderer); + if(getKeyWordSearchPopupMaxHeight() != null) { + pnl.setMaxHeight(getKeyWordSearchPopupMaxHeight()); + } DomUtil.nullChecked(getKeySearch()).add(pnl); pnl.setOnValueChanged((IValueChanged>) component -> { @@ -470,6 +486,14 @@ public void setKeyWordSearchPopupWidth(int keyWordSearchPopupWidth) { m_keyWordSearchPopupWidth = keyWordSearchPopupWidth; } + public String getKeyWordSearchPopupMaxHeight() { + return m_keyWordSearchPopupMaxHeight; + } + + public void setKeyWordSearchPopupMaxHeight(String keyWordSearchPopupMaxHeight) { + m_keyWordSearchPopupMaxHeight = keyWordSearchPopupMaxHeight; + } + /** * Define the columns to show in "display current value" mode. This actually creates a * content renderer (a {@link SimpleLookupInputRenderer}) to render the fields. diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/FastXmlOutputWriter.java b/to.etc.domui/src/main/java/to/etc/domui/dom/FastXmlOutputWriter.java index c84f231f01..d6140851c2 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/FastXmlOutputWriter.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/FastXmlOutputWriter.java @@ -27,8 +27,8 @@ import java.io.*; public class FastXmlOutputWriter extends XmlOutputWriterBase implements IBrowserOutput { - public FastXmlOutputWriter(Writer w) { - super(w); + public FastXmlOutputWriter(Writer w, ICSPSupport csp) { + super(w, csp); } @Override diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/HtmlFileRenderer.java b/to.etc.domui/src/main/java/to/etc/domui/dom/HtmlFileRenderer.java index 2abd623f39..64ae146859 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/HtmlFileRenderer.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/HtmlFileRenderer.java @@ -121,7 +121,7 @@ public void addHeaderContributor(HeaderContributor contributor) { } static public HtmlFileRenderer create(@NonNull Writer output, @NonNull NodeContainer rootNode) throws Exception { - FastXmlOutputWriter out = new FastXmlOutputWriter(output); + FastXmlOutputWriter out = new FastXmlOutputWriter(output, rootNode.getPage()); HtmlTagRenderer rr = new StandardHtmlTagRenderer(BrowserVersion.INSTANCE, out, false); rr.setRenderInline(true); HtmlFileRenderer fr = new HtmlFileRenderer(rr, out, rootNode); @@ -146,7 +146,7 @@ static public HtmlFileRenderer create(@NonNull Writer output, @NonNull Page sour renderRoot = body; } - FastXmlOutputWriter out = new FastXmlOutputWriter(output); + FastXmlOutputWriter out = new FastXmlOutputWriter(output, sourcePage); HtmlTagRenderer rr = new StandardHtmlTagRenderer(BrowserVersion.INSTANCE, out, false); rr.setRenderInline(true); HtmlFileRenderer fr = new HtmlFileRenderer(rr, out, renderRoot); diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/HtmlFullRenderer.java b/to.etc.domui/src/main/java/to/etc/domui/dom/HtmlFullRenderer.java index ea87f63d8f..cd72ba20e6 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/HtmlFullRenderer.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/HtmlFullRenderer.java @@ -197,13 +197,20 @@ private void renderAfterBody() throws Exception { o().text("WebUI.focus('" + f.getActualID() + "');"); m_page.setFocusComponent(null); } + + //-- Render all component-requested Javascript code for this phase. First domuiJs (js as result of CSP header support js), then createJs and at the end normal js, in that strict order. + StringBuilder domuiSb = m_page.internalFlushAppendDomuiJS(); + if(null != domuiSb) { + o().writeRaw(domuiSb); + } if(getCreateJS().length() > 0) { o().writeRaw(getCreateJS().toString()); // o().text(m_createJS.toString()); } StringBuilder sb = m_page.internalFlushAppendJS(); - if(null != sb) + if(null != sb) { o().writeRaw(sb); + } sb = m_page.internalFlushJavascriptStateChanges(); if(null != sb) o().writeRaw(sb); diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/IBrowserOutput.java b/to.etc.domui/src/main/java/to/etc/domui/dom/IBrowserOutput.java index 0e3463a245..8a2cd47a52 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/IBrowserOutput.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/IBrowserOutput.java @@ -82,6 +82,4 @@ public interface IBrowserOutput { void attr(String name, int value) throws IOException; void attr(String name, boolean value) throws IOException; - - } diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/ICSPSupport.java b/to.etc.domui/src/main/java/to/etc/domui/dom/ICSPSupport.java new file mode 100644 index 0000000000..6e95d52a52 --- /dev/null +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/ICSPSupport.java @@ -0,0 +1,23 @@ +package to.etc.domui.dom; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * Defines contract for supporting the Http Content-Security-Policy (CSP) header. + */ +public interface ICSPSupport { + + /** + * Defines if given attribute needs to be handled by CSP support code. + */ + boolean isAttributeHandled(@NonNull String attributeName); + + /** + * Renders given attribute value as javascript expression for given element identified by selector. + * + * @param selector Normally just jquery selector for element ID, like #_AA. + * @param attribute Name of the attribute. + * @param value Attribute value that has to be translated to a javascript expression. I.e. onclick = "WebUI.clicked('#_AA');" -> $('#_AA').click(function() { WebUI.clicked('#_AA'); }); + */ + void renderAsJavaScript(@NonNull String selector, @NonNull String attribute, @NonNull String value); +} diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/PrettyXmlOutputWriter.java b/to.etc.domui/src/main/java/to/etc/domui/dom/PrettyXmlOutputWriter.java index 0aa4d397f2..ea26b85757 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/PrettyXmlOutputWriter.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/PrettyXmlOutputWriter.java @@ -25,6 +25,7 @@ package to.etc.domui.dom; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import to.etc.util.IndentWriter; import java.io.IOException; @@ -40,8 +41,8 @@ public class PrettyXmlOutputWriter extends XmlOutputWriterBase implements IBrowserOutput { private IndentWriter m_w; - public PrettyXmlOutputWriter(@NonNull Writer out) { - super(new IndentWriter(out)); + public PrettyXmlOutputWriter(@NonNull Writer out, @Nullable ICSPSupport csp) { + super(new IndentWriter(out), csp); m_w = (IndentWriter) getWriter(); } diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/XmlOutputWriterBase.java b/to.etc.domui/src/main/java/to/etc/domui/dom/XmlOutputWriterBase.java index 8d8e4bbbab..ca3329e670 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/XmlOutputWriterBase.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/XmlOutputWriterBase.java @@ -24,21 +24,36 @@ */ package to.etc.domui.dom; -import java.io.*; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import to.etc.domui.util.DomUtil; +import to.etc.util.Pair; + +import java.io.IOException; +import java.io.Writer; +import java.util.Stack; public class XmlOutputWriterBase { private Writer m_w; protected boolean m_intag; - public XmlOutputWriterBase(Writer w) { + @Nullable + private final ICSPSupport m_scp; + + @NonNull + protected Stack> m_tagNamesAndIds = new Stack<>(); + + public XmlOutputWriterBase(Writer w, @Nullable ICSPSupport scp) { m_w = w; + m_scp = scp; } protected Writer getWriter() { return m_w; } + /** * Writes string data. This escapes XML control characters to their entity * equivalent. This does NOT indent data with newlines, because string data @@ -95,9 +110,11 @@ protected void println() throws IOException { public void nl() throws IOException {} - public void inc() {} + public void inc() { + } - public void dec() {} + public void dec() { + } public boolean isIndentEnabled() { return false; @@ -106,9 +123,6 @@ public boolean isIndentEnabled() { /** * Writes a tag start. It can be followed by attr() calls. If the namespace is in the current * namespace the tag will not have prefixes. - * - * @param namespace - * @param tagname */ public void tag(final String tagname) throws IOException { closePrevious(); // If an earlier tag is open close it, @@ -118,6 +132,7 @@ public void tag(final String tagname) throws IOException { writeRaw(tagname); m_intag = true; inc(); + m_tagNamesAndIds.push(new Pair<>(tagname, null)); } /** @@ -143,7 +158,6 @@ public void endtag() throws IOException { /** * Ends a tag by adding />. - * @throws IOException */ public void endAndCloseXmltag() throws IOException { if(!m_intag) @@ -151,6 +165,7 @@ public void endAndCloseXmltag() throws IOException { m_intag = false; writeRaw("/>"); dec(); + m_tagNamesAndIds.pop(); } public void closetag(String name) throws IOException { @@ -161,6 +176,7 @@ public void closetag(String name) throws IOException { writeRaw(">"); if(isIndentEnabled()) nl(); + m_tagNamesAndIds.pop(); } /*--------------------------------------------------------------*/ @@ -169,15 +185,36 @@ public void closetag(String name) throws IOException { /** * Appends an attribute to the last tag. The value's characters that are invalid are quoted into * entities. - * - * @param namespace - * @param name - * @param value - * @throws IOException */ public void attr(String name, String value) throws IOException { if(!m_intag) throw new IllegalStateException("No tag is currently 'active'"); + + ICSPSupport scp = m_scp; + if(DomUtil.isIn(name, "id") && null != scp) { + Pair current = m_tagNamesAndIds.pop(); + m_tagNamesAndIds.push(new Pair<>(current.get1(), value)); + } + if("select".equals(name) && null != scp) { + Pair current = m_tagNamesAndIds.peek(); + if(null != current && "changeTagAttributes".equals(current.get1())) { + m_tagNamesAndIds.pop(); + if(value.startsWith("#")) { + m_tagNamesAndIds.push(new Pair<>(current.get1(), value.substring(1))); + } + } + } + + if(null != scp && scp.isAttributeHandled(name)) { + Pair current = m_tagNamesAndIds.peek(); + if(null == current) { + throw new IllegalStateException("Can't resolve the tagNameAndId on stack!?"); + } + //m_scp.registerInlineJs(value); + scp.renderAsJavaScript(current.get2(), name, value); + return; + } + writeRaw(" "); writeRaw(name); writeRaw("=\""); @@ -247,11 +284,6 @@ private void writeAttrValue(String value) throws IOException { /** * Write a simple numeric attribute thingy. - * - * @param namespace - * @param name - * @param value - * @throws IOException */ public void attr(String name, long value) throws IOException { attr(name, Long.toString(value)); diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/html/OptimalDeltaRenderer.java b/to.etc.domui/src/main/java/to/etc/domui/dom/html/OptimalDeltaRenderer.java index 3ea3e29eca..23326618e4 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/html/OptimalDeltaRenderer.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/html/OptimalDeltaRenderer.java @@ -254,15 +254,22 @@ public void render() throws Exception { m_page.setFocusComponent(null); } - //-- Render all component-requested Javascript code for this phase + //-- Render all component-requested Javascript code for this phase. First domuiJs (js as result of CSP header support js), then createJs and at the end normal js, in that strict order. + StringBuilder domuiSb = m_page.internalFlushAppendDomuiJS(); + if(null != domuiSb) { + o().text(domuiSb.toString()); + } + o().text(m_fullRenderer.getCreateJS().toString()); + StringBuilder sb = m_page.internalFlushAppendJS(); - if(null != sb) + if(null != sb) { o().text(sb.toString()); + } sb = m_page.internalFlushJavascriptStateChanges(); - if(null != sb) + if(null != sb) { o().writeRaw(sb); - + } //-- Handle delayed stuff... if(DeveloperOptions.getBool("domui.polling", true)) { diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/html/Page.java b/to.etc.domui/src/main/java/to/etc/domui/dom/html/Page.java index e75ea1de3c..6191e46289 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/html/Page.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/html/Page.java @@ -32,6 +32,7 @@ import to.etc.domui.component.binding.OldBindingHandler; import to.etc.domui.component.layout.FloatingDiv; import to.etc.domui.component.misc.WindowParameters; +import to.etc.domui.dom.ICSPSupport; import to.etc.domui.dom.errors.IErrorFence; import to.etc.domui.dom.errors.UIMessage; import to.etc.domui.dom.header.HeaderContributor; @@ -47,6 +48,7 @@ import to.etc.domui.util.javascript.JavascriptStmt; import to.etc.domui.util.resources.IResourceRef; import to.etc.function.IExecute; +import to.etc.util.StringTool; import to.etc.util.WrappedException; import to.etc.webapp.core.IRunnable; import to.etc.webapp.nls.NlsContext; @@ -74,7 +76,7 @@ * Created on Aug 18, 2007 */ @NonNullByDefault -final public class Page implements IQContextContainer { +final public class Page implements IQContextContainer, ICSPSupport { static private final Logger LOG = LoggerFactory.getLogger(Page.class); static private final int MAX_DOMUI_NODES_PER_PAGE = 100_000; @@ -134,6 +136,9 @@ final public class Page implements IQContextContainer { @Nullable private StringBuilder m_appendJS; + @Nullable + private StringBuilder m_appendDomuiJS; + /** Temp for checking shelve order. */ private boolean m_shelved; @@ -901,6 +906,13 @@ public void appendJS(@NonNull final CharSequence sq) { internalGetAppendJS().append(sq); } + @Nullable + public StringBuilder internalFlushAppendDomuiJS() { + StringBuilder sb = m_appendDomuiJS; + m_appendDomuiJS = null; + return sb; + } + @Nullable public StringBuilder internalFlushAppendJS() { if(internalCanLeaveCurrentPageByBrowser()) { @@ -925,6 +937,15 @@ public StringBuilder internalGetAppendJS() { return sb; } + @NonNull + private StringBuilder internalGetAppendDomuiJS() { + StringBuilder sb = m_appendDomuiJS; + if(null == sb) { + sb = m_appendDomuiJS = new StringBuilder(512); + } + return sb; + } + /** * Force the browser to open a new window with a user-specified URL. The new window does NOT @@ -1504,6 +1525,52 @@ public String getNonce() { return nonce; } + /** + * Currently supported list of attributes (other than event handlers) that we handle by CSP handler. + */ + private static final Set CSP_JS_INLINE_ATTRIBUTES_TO_HANDLE = Set.of("style"); + + @Override + public boolean isAttributeHandled(@NonNull String attributeName) { + if(attributeName.startsWith("on")) { + return true; + } + return CSP_JS_INLINE_ATTRIBUTES_TO_HANDLE.contains(attributeName); + } + + @Override + public void renderAsJavaScript(String id, String attribute, String value) { + boolean isFunction = isAttributeMappedToJsFunction(attribute); + if(isFunction) { + String event = translateAttributeToEvent(attribute); + internalGetAppendDomuiJS().append("$('#" + id + "').off('" + event + "');"); + if(!StringTool.isBlank(value)) { + internalGetAppendDomuiJS().append("$('#" + id + "').on('" + event + "', function() {" + value + ";});"); + } + }else { + String field = translateAttributeToField(attribute); + internalGetAppendDomuiJS().append("$('#" + id + "').attr(\"" + field + "\", \"" + value + "\");"); + } + } + + private boolean isAttributeMappedToJsFunction(String attribute) { + return attribute.startsWith("on"); + } + + private String translateAttributeToEvent(String attribute) { + if(attribute.startsWith("on")) { + return attribute.substring(2); + } + throw new IllegalArgumentException("What else? " + attribute); + } + + private String translateAttributeToField(String attribute) { + switch(attribute) { + case "style": return "style"; + default: throw new IllegalArgumentException("What else? " + attribute); + } + } + public Map getHeaderVariableMap() { m_headerVariableMap.put("NONCE", getNonce()); return m_headerVariableMap; diff --git a/to.etc.domui/src/main/java/to/etc/domui/dom/html/TextArea.java b/to.etc.domui/src/main/java/to/etc/domui/dom/html/TextArea.java index d5d4274d51..4b9fcb2042 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/dom/html/TextArea.java +++ b/to.etc.domui/src/main/java/to/etc/domui/dom/html/TextArea.java @@ -24,23 +24,35 @@ */ package to.etc.domui.dom.html; +import org.apache.poi.ss.formula.functions.T; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import to.etc.domui.component.meta.MetaManager; import to.etc.domui.component.meta.MetaUtils; import to.etc.domui.component.meta.PropertyMetaModel; +import to.etc.domui.component.meta.PropertyMetaValidator; +import to.etc.domui.component.meta.impl.MetaPropertyValidatorImpl; import to.etc.domui.component.misc.UIControlUtil; +import to.etc.domui.converter.IValueValidator; +import to.etc.domui.converter.ValidatorRegistry; import to.etc.domui.dom.errors.UIMessage; import to.etc.domui.server.DomApplication; +import to.etc.domui.trouble.UIException; import to.etc.domui.trouble.ValidationException; import to.etc.domui.util.DomUtil; import to.etc.domui.util.Msgs; +import to.etc.util.RuntimeConversionException; import to.etc.util.StringTool; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; public class TextArea extends InputNodeContainer implements INativeChangeListener, IControl, IHasModifiedIndication, IHtmlInput { - /** Hint to use in property meta data to select this component. */ + /** + * Hint to use in property meta data to select this component. + */ static public final String HINT = "textarea"; private int m_cols = -1; @@ -51,12 +63,19 @@ public class TextArea extends InputNodeContainer implements INativeChangeListene private boolean m_disabled; - /** Indication if the contents of this thing has been altered by the user. This merely compares any incoming value with the present value and goes "true" when those are not equal. */ + /** + * Indication if the contents of this thing has been altered by the user. This merely compares any incoming value with the present value and goes "true" when those are not equal. + */ private boolean m_modifiedByUser; - private int m_maxLength; + private int m_maxLength; + + /** Defined value validators on this field. */ + private List> m_validators = Collections.EMPTY_LIST; - /** Oracle <= 11 has a hard limit of 4000 bytes in a varchar2. TextArea's bound to an Oracle column might need this second limit observed too. This assumes UTF-8 encoding in the database too. */ + /** + * Oracle <= 11 has a hard limit of 4000 bytes in a varchar2. TextArea's bound to an Oracle column might need this second limit observed too. This assumes UTF-8 encoding in the database too. + */ private int m_maxByteLength; @Nullable @@ -72,7 +91,9 @@ public TextArea(int cols, int rows) { m_rows = rows; } - @Nullable @Override public NodeBase getForTarget() { + @Nullable + @Override + public NodeBase getForTarget() { return this; } @@ -120,7 +141,7 @@ protected void internalSetValue(String value) { */ @Nullable public String getBindValue() { - validate(); // Validate, and throw exception without UI change on trouble. + validate(); // Validate, and throw exception without UI change on trouble. return m_value; } @@ -138,6 +159,16 @@ private void validate() { throw new ValidationException(Msgs.mandatory); } } + try { + for(IValueValidator vv : m_validators) + ((IValueValidator) vv).validate(m_value); + } catch(UIException x) { + throw new ValidationException(x); + } catch(RuntimeConversionException x) { + throw new ValidationException(Msgs.notValid, m_value); + } catch(Exception x) { + throw new ValidationException(Msgs.unexpectedException, x); + } } @Override @@ -162,7 +193,6 @@ public boolean hasError() { return super.hasError(); } - public String getRawValue() { return internalGetValue(); } @@ -178,7 +208,7 @@ public void setDisabled(boolean disabled) { return; changed(); m_disabled = disabled; - if(! disabled) + if(!disabled) setOverrideTitle(null); } @@ -225,13 +255,17 @@ public boolean acceptRequestParameter(@NonNull String[] values) throws Exception //vmijic 20091126 - now IE returns \r\n, but FF returns \n... So, both nw and cur have to be compared with "\r\n" replaced by "\n"... String flattenLineBreaksNw = (nw != null) ? nw.replaceAll("\r\n", "\n") : null; - //vmijic 20101117 - it is discovered (call 28340) that from some reason first \n is not rendered in TextArea on client side in initial page render. That cause that same \n is missing from unchanged text area input that comes through client request roundtrip and cause modified flag to be set... So as dirty fix we have to compare without that starting \n too... + // vmijic 20101117 - it is discovered (call 28340) that from some reason first \n is + // not rendered in TextArea on client side in initial page render. That cause that + // same \n is missing from unchanged text area input that comes through client request + // roundtrip and cause modified flag to be set... So as dirty fix we have to compare + // without that starting \n too... if(flattenLineBreaksNw != null && cur != null && cur.startsWith("\n") && !flattenLineBreaksNw.startsWith("\n")) { cur = cur.substring(1); } int maxLength = getMaxLength(); - if(maxLength > 0 && nw != null && nw.length() > maxLength) // Be very sure we are limited even if javascript does not execute. + if(maxLength > 0 && nw != null && nw.length() > maxLength) // Be very sure we are limited even if javascript does not execute. nw = nw.substring(0, maxLength); int maxBytes = getMaxByteLength(); @@ -252,6 +286,7 @@ public boolean acceptRequestParameter(@NonNull String[] values) throws Exception /*--------------------------------------------------------------*/ /* CODING: IHasModifiedIndication impl */ /*--------------------------------------------------------------*/ + /** * Returns the modified-by-user flag. */ @@ -269,7 +304,7 @@ public void setModified(boolean as) { } @NonNull - static public TextArea create(@NonNull PropertyMetaModel< ? > pmm) { + static public TextArea create(@NonNull PropertyMetaModel pmm) { TextArea ta = new TextArea(); String cth = pmm.getComponentTypeHint(); if(cth != null) { @@ -324,7 +359,28 @@ public void setMaxByteLength(int maxByteLength) { m_maxByteLength = maxByteLength; } - @Override public void setHint(String hintText) { + @Override + public void setHint(String hintText) { setTitle(hintText); } + + public void addValidator(IValueValidator v) { + if(m_validators == Collections.EMPTY_LIST) + m_validators = new ArrayList<>(5); + m_validators.add(v); + } + + public void addValidator(PropertyMetaValidator v) { + IValueValidator vi = ValidatorRegistry.getValueValidator((Class>) v.getValidatorClass(), v.getParameters()); + addValidator(vi); + } + + public void addValidator(Class> clz) { + IValueValidator vi = ValidatorRegistry.getValueValidator(clz, null); + addValidator(vi); + } + + public void addValidator(Class> clz, String[] parameters) { + addValidator(new MetaPropertyValidatorImpl(clz, parameters)); + } } diff --git a/to.etc.domui/src/main/java/to/etc/domui/server/ApplicationRequestHandler.java b/to.etc.domui/src/main/java/to/etc/domui/server/ApplicationRequestHandler.java index 40bbb09176..06ae7f9502 100644 --- a/to.etc.domui/src/main/java/to/etc/domui/server/ApplicationRequestHandler.java +++ b/to.etc.domui/src/main/java/to/etc/domui/server/ApplicationRequestHandler.java @@ -101,7 +101,7 @@ static public void generateHttpRedirect(RequestContextImpl ctx, String to, Strin Map varMap = Map.of("NONCE", nonce); a.renderHeaders(ctx.getRequestResponse(), httpHeaders, varMap); - IBrowserOutput out = new PrettyXmlOutputWriter(ctx.getOutputWriter("text/html; charset=UTF-8", "utf-8")); + IBrowserOutput out = new PrettyXmlOutputWriter(ctx.getOutputWriter("text/html; charset=UTF-8", "utf-8"), null); out.writeRaw("\n" + "