From ef3b7260ea806892a1c55d12b3e8076af6ec76f2 Mon Sep 17 00:00:00 2001 From: Tomek Kopycki <39048175+tomekfab@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:57:03 +0200 Subject: [PATCH] Changes from apache/commons-jexl (#2) * Adjusted the name of the binary and source archives on the download page git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1720781 13f79535-47bb-0310-9956-ffa450edef68 * Downgrade again the findbugs plugin (3.0.0) to work around an encoding issue with the latest release git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1720784 13f79535-47bb-0310-9956-ffa450edef68 * Fixed the links to the previous API documentations git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1721833 13f79535-47bb-0310-9956-ffa450edef68 * Set the release date for JEXL 3.0 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1721953 13f79535-47bb-0310-9956-ffa450edef68 * Changed the version to 3.1-SNAPSHOT git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1721954 13f79535-47bb-0310-9956-ffa450edef68 * Update copyright for 2016 in NOTICE.txt git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1725436 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-186: correcting arithmetic operator overloading caching / discovery git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1726257 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-187: added 'continue' and 'break' to syntax reference git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1727856 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Updated changes.xml to reflect JEXL-18{6,7} git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1727858 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Fix typos in doc wrt to {start,end}sWith operators (=^,=$) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1729855 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Fixing JEXL-188 using more information from regexp (groups) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1731915 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Fixing JEXL-189 using logical or (and avoiding masking the interruption status) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1731917 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Updated changes.xml git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1731918 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Updated changes.xml git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1731919 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: FindBugs warning git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1731934 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: FindBugs warning git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1731935 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: FindBugs warning git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1731936 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-191: When introspecting an object which is a class, also check for fields in the class itself (ie seek static members) rather than in its class. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1735550 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Get at least line/column info from jexlInfo() git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1735590 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-190: Modified logic to solve function calls, seeking variables then context, null namespace or arithmetic methods git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1735607 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Checkstyle git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1735971 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-186: Cleaner arithmetic * operators handling git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1735973 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-192: Fix antish variable * method evaluation logic git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1736210 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Preparing for 3.0.1 release git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1736215 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Preparing for 3.0.1 release git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1736235 13f79535-47bb-0310-9956-ffa450edef68 * Standard Maven location for assemblies git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1739830 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Forgot to mention side-effect operators (+=, etc...) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1741404 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Javadoc typo git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1741405 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Fixing JEXL-193, refined handling of interruption (InterruptedException handling & interrupted()), more checks around cancellation (blocks,set/map/array literals), refactored internal Callable, more tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1741426 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Fixing JEXL-193 take 2; added a cancellable flag that controls whether JexlException.Cancel are throw when evaluation is interrupted/cancelled. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1743249 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-195: added AtomicBoolean handling where relevant in JexlArithmetic. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1743627 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-196: break out of method resolution loop when already failed to resolve through context method git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1745646 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-194: added overload handling of forEach operator in arithmetic to allow customized iterator, added examples of synchronized arithmetic & context git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1747591 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-198: added the submitted code & test (thanks Terefang) to trunk git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1747593 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: updated changes.xml & release-notes.txt git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1747630 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Initial code for annotations JEXL-197 - @syntax The @default part of the specification is not implemented (and probably wont) to avoid incurring a prohibitive evaluation cost); The JexStatement/Interceptor parts have been simplified with Callable and JexlContext.AnnotationProcessor. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1751163 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Fixing JEXL-202 by extending parsing check on l-value for all assignment operators git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1751647 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Fixing JEXL-202 typo in error msg git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1751777 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Partial fix for JEXL-201 / JEXL-203 - moved silent/strict/cancellable flags out of interpreter (thus overridable by contextual options) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1752310 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Blind fix for JEXL-205 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1752311 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Blind fix for JEXL-205 / JEXL-206 - added latches in tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1752318 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-205; check interruption status within loop to detect cancellation git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1752420 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-205; check interruption status within loop to detect cancellation git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1752424 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-205 / JEXL-206 - dont interpret cancelled scripts git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1752426 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-205 / JEXL-206 - clean up tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1752595 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Updated changes and release notes to reflect last fixes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1752674 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-207: split Interpreter in 2 (class was too big), reworked error/exception handling to be more coherent, added tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1754746 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-207: update changes & release notes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1754750 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-207: update changes & release notes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1754751 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: JEXL-208: typos in doc git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1754844 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-197: modified grammar to allow '@annotation var...' git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1755110 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-209; added test, updated changes & release notes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1755186 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-201: added setting options from engine git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1755188 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-201: added setting options from context git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1755189 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Bulk fix for JEXL-210, JEXL-207 and JEXL-197; reverted to a sane version of error handling, exceptions stop script execution, annotation may create a new arithmetic if needed git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1755195 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Javadoc fixes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1755315 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Typo git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1755433 13f79535-47bb-0310-9956-ffa450edef68 * Restored a constructor in JexlException$Property to preserve the compatibility git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1755545 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Preparing for release git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1756139 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-208: documentation typos git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1758189 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-217: protect tryInvoke in the same try/catch as invoke git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1758541 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-214: remove useless call to fillInStackTrace git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1758577 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-213: add charset(Charset arg) to JexlBuilder, deprecated loader(Charset...) which is a mistake git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1758578 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-215: create default jexlinfo in parse() rather than in multiple places git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1758579 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Restore ThreadLocal as specialization of JexlContext git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1764403 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-219: Adding explicit white/black listing flag for default behavior of sandbox git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1764408 13f79535-47bb-0310-9956-ffa450edef68 * Add missing license header. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1770440 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-211: Protect various executors from null properties in tryInvoke git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786350 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-216: Use r/w lock on cache, busy-flag logic on parser (parser is not reentrant, create a temp one if busy) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786352 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-211: Add callable method to JexlExpression interface - breaks compatibility but it is not expected than any codeimplements this interface besides Jexl git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786353 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Various updates & improvements related to last fixes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786354 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Avoiding clirr error git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786382 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Added clear statement about compatibility break git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786387 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Removed (wrong) statement about Java7 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786389 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Reworded compatibility break warning git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786564 13f79535-47bb-0310-9956-ffa450edef68 * Use the same signature as the overridden method git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786664 13f79535-47bb-0310-9956-ffa450edef68 * Update dependencies Allow Clirr to be run from command line git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786665 13f79535-47bb-0310-9956-ffa450edef68 * Add @since markers git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786671 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Javadoc typo git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786897 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Javadoc typo git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786898 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Removed unused imports git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786899 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-222: Improved IndexedType, allow caching of last set/get, expose container class & name git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786904 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Updated changes & release notes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786909 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Updated doc on the risks of implementing JEXL interfaces in user classes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786940 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Checkstyle & javadoc git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1786990 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Removed misleading/too broad warning git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1787168 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Add implementation warning git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1787206 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Add implementation warning git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1787207 13f79535-47bb-0310-9956-ffa450edef68 * Removed an unused local variable git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788574 13f79535-47bb-0310-9956-ffa450edef68 * Removed an unnecessary type cast git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788575 13f79535-47bb-0310-9956-ffa450edef68 * Inner enums are implicitly static git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788576 13f79535-47bb-0310-9956-ffa450edef68 * foreach loop git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788577 13f79535-47bb-0310-9956-ffa450edef68 * Inner enums are implicitly static (more) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788578 13f79535-47bb-0310-9956-ffa450edef68 * Minor simplification, the cache is guaranteed to be never null git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788579 13f79535-47bb-0310-9956-ffa450edef68 * Update copyright to 2017 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788581 13f79535-47bb-0310-9956-ffa450edef68 * Added the missing @Test annotations in the JUnit 4 tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788590 13f79535-47bb-0310-9956-ffa450edef68 * Moved the @Test annotations to a separate line git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788591 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Fix & re-added a few commented out tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1788925 13f79535-47bb-0310-9956-ffa450edef68 * Fixed a typo in the syntax guide git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790129 13f79535-47bb-0310-9956-ffa450edef68 * Improved the release notes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790528 13f79535-47bb-0310-9956-ffa450edef68 * Replaced 'canceled' with 'cancelled' for consistency git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790529 13f79535-47bb-0310-9956-ffa450edef68 * Updated the commons.release.version property to 3.1 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790531 13f79535-47bb-0310-9956-ffa450edef68 * Updated download page in preparation for 3.1 release git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790532 13f79535-47bb-0310-9956-ffa450edef68 * Refreshed CONTRIBUTING.md and README.md git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790533 13f79535-47bb-0310-9956-ffa450edef68 * Fortunately CVS is no longer used git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790540 13f79535-47bb-0310-9956-ffa450edef68 * Updated some version numbers in the site pages git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790543 13f79535-47bb-0310-9956-ffa450edef68 * Removed unnecessary null checks (reported by Findbugs) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790548 13f79535-47bb-0310-9956-ffa450edef68 * Short circuit evaluation in ArrayBuilder.add() (reported by Findbugs) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790550 13f79535-47bb-0310-9956-ffa450edef68 * Removed the @inheritDoc tags with no extra information git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790553 13f79535-47bb-0310-9956-ffa450edef68 * Removed unnecessary fully qualified names git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790560 13f79535-47bb-0310-9956-ffa450edef68 * Add oraclejdk7 to Travis CI. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1790595 13f79535-47bb-0310-9956-ffa450edef68 * Set the release date for JEXL 3.1 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1791328 13f79535-47bb-0310-9956-ffa450edef68 * Move to the next version git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1791358 13f79535-47bb-0310-9956-ffa450edef68 * Fix up URL so relative URLs such as Security work git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1792641 13f79535-47bb-0310-9956-ffa450edef68 * (chore) adding commons.module.name to pom git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1797214 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-230: documentation git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800055 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-226: add ?? operator git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800127 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-226: add ?? operator git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800128 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-231: Syntax for accessing List elements is not mentioned in docs git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800134 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-234: updated JexlArithmetic.start,ends}With to use CharSequence as first param git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800482 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-227: JexlScriptEngineFactory.getEngineVersion() should return actual version git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800511 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: added empty and nested set tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800843 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: added null coalescing operator script test git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800855 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-236: modified arguments class checks & casts, added tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1800856 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-224: ObjectContext rewrite based on lower level calls git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1801955 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-224: added logic to allow detecting an overloaded call(...) method in JexlArithmetic usable to perform a function call git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1801956 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-224: JEXL-225: new tests, split non-regression test class (IssuesTest) in 3 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1801957 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-224: JEXL-225: changes, release notes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1801958 13f79535-47bb-0310-9956-ffa450edef68 * .travis.yml: remove oraclejdk7 (no longer available) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1804605 13f79535-47bb-0310-9956-ffa450edef68 * .travis.yml: build pull requests (not only trunk) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1804606 13f79535-47bb-0310-9956-ffa450edef68 * README.md: add build status badge and remove now redundant license badge git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1804607 13f79535-47bb-0310-9956-ffa450edef68 * README.md: fix travis badge (side effect: close #4) git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1804623 13f79535-47bb-0310-9956-ffa450edef68 * try to fix build on travis by increasing max memory for tests git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1804627 13f79535-47bb-0310-9956-ffa450edef68 * Fix typos in Javadoc git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1807436 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-240: Expose the Jexl engine evaluating a script/expression as a thread local; Make classes functors, ie class(arg) will attempt to call a ctor, a simpler version of new(class, arg); Fix antish variable used as method/function call; git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1811336 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-240: Javadoc git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1811338 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-238: Changed signatures & variable types git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1811339 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Updating changes & release notes for last issues fixes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1811341 13f79535-47bb-0310-9956-ffa450edef68 * fix java 6 build on travis by using precise distribution git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1811465 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-241: bad implementation of a double-check lazy initialization, aka item 71 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1812625 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-241: Updating changes & release notes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1812627 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Javadoc git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1812991 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-240: Reverting to not considering class objects as functor (by default). git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1813542 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-243: First drop of feature control code git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1813544 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-244: Blind fix, removing static anonymous inner classes git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1814413 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Made permissions an explicit instance in preparation for future / further sandboxing capabilities git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1815813 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-243: Features, refined version. Allow fine grain on what is syntactically available for both scripts and expressions (and make expressions a subset of script features). git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1816640 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-245: Better handling of null properties during de-referencing git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1817082 13f79535-47bb-0310-9956-ffa450edef68 * Update commons-parent from 42 to 43. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1820492 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Reducing FindBugs complaints git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1820566 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-246: Better error handling or operator overload error, added specific test, changes & release notes updated git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1820568 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-246: Refined AmbiguousException with severity flag to consider null arguments as being often benign; when benign (aka not severe), AmbiguousException no longer trigger logging git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1820883 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-246: Javadoc git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1820884 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Javadoc git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821294 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-246: 3rd times a charm... relaxing ambiguity rules wrt null / object; properly detect JexlArithmetic operator methods git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821295 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Removing references to junit.framework, replaced by org.junit git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821466 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Missing @Test git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821738 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Protect against potential NPE git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821740 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Log signature collision in debug git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821777 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Protect against potential NPE git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821779 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-246: Improved detection of operator methods that arent overrides git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821780 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Coverage, added a test on empty array assignment git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821781 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Coverage, added a test on method overloads / object parameters / null arguments git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821782 13f79535-47bb-0310-9956-ffa450edef68 * JEXL: Coverage, added a test / call debug method git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821783 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-248: Fixed left-value check during assignment parsing, added test git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821784 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-248: Fixed left-value check during assignment parsing, added test git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821785 13f79535-47bb-0310-9956-ffa450edef68 * JEXL-246: Quiesced down logging, was missing check on actual signature diff git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1821853 13f79535-47bb-0310-9956-ffa450edef68 Co-authored-by: Emmanuel Bourg Co-authored-by: Gary D. Gregory Co-authored-by: Henri Biestro Co-authored-by: Sebastian Bazley Co-authored-by: henrib Co-authored-by: Rob Tompkins Co-authored-by: Pascal Schumacher Co-authored-by: Bruno P. Kinoshita --- .travis.yml | 22 +- CONTRIBUTING.md | 40 +- NOTICE.txt | 2 +- README.md | 201 +-- RELEASE-NOTES.txt | 110 +- pom.xml | 52 +- src/{main => }/assembly/bin.xml | 0 src/{main => }/assembly/src.xml | 0 .../apache/commons/jexl3/JexlArithmetic.java | 233 ++- .../org/apache/commons/jexl3/JexlBuilder.java | 91 +- .../org/apache/commons/jexl3/JexlContext.java | 65 +- .../org/apache/commons/jexl3/JexlEngine.java | 186 +- .../apache/commons/jexl3/JexlException.java | 163 +- .../apache/commons/jexl3/JexlExpression.java | 24 +- .../apache/commons/jexl3/JexlFeatures.java | 474 +++++ .../apache/commons/jexl3/JexlOperator.java | 124 +- .../org/apache/commons/jexl3/JexlScript.java | 2 + .../org/apache/commons/jexl3/JxltEngine.java | 113 +- .../apache/commons/jexl3/ObjectContext.java | 55 +- .../commons/jexl3/internal/ArrayBuilder.java | 2 +- .../commons/jexl3/internal/Closure.java | 18 +- .../commons/jexl3/internal/Debugger.java | 82 +- .../apache/commons/jexl3/internal/Engine.java | 317 ++-- .../commons/jexl3/internal/Interpreter.java | 928 +++++----- .../jexl3/internal/InterpreterBase.java | 301 ++++ .../commons/jexl3/internal/Operators.java | 64 +- .../apache/commons/jexl3/internal/Script.java | 115 +- .../commons/jexl3/internal/ScriptVisitor.java | 496 ++++++ .../commons/jexl3/internal/SoftCache.java | 211 +++ .../apache/commons/jexl3/internal/Source.java | 94 + .../jexl3/internal/TemplateEngine.java | 32 +- .../jexl3/internal/TemplateScript.java | 10 +- .../introspection/AbstractExecutor.java | 3 - .../introspection/ArrayListWrapper.java | 3 - .../introspection/BooleanGetExecutor.java | 11 +- .../internal/introspection/ClassMap.java | 63 +- .../introspection/DuckGetExecutor.java | 12 +- .../introspection/DuckSetExecutor.java | 11 +- .../internal/introspection/IndexedType.java | 123 +- .../internal/introspection/Introspector.java | 26 +- .../introspection/ListGetExecutor.java | 4 +- .../introspection/ListSetExecutor.java | 2 +- .../introspection/MapGetExecutor.java | 9 +- .../introspection/MapSetExecutor.java | 6 +- .../internal/introspection/MethodKey.java | 346 ++-- .../internal/introspection/Permissions.java | 48 +- .../introspection/SandboxUberspect.java | 33 - .../internal/introspection/Uberspect.java | 86 +- .../jexl3/introspection/JexlSandbox.java | 113 +- .../org/apache/commons/jexl3/package.html | 2 + .../commons/jexl3/parser/ASTAnnotation.java | 54 + .../commons/jexl3/parser/ASTArrayLiteral.java | 2 - .../commons/jexl3/parser/ASTJexlScript.java | 46 +- .../commons/jexl3/parser/ASTJxltLiteral.java | 1 - .../commons/jexl3/parser/ASTMapLiteral.java | 2 - .../jexl3/parser/ASTNumberLiteral.java | 2 +- .../commons/jexl3/parser/ASTSetLiteral.java | 2 - .../jexl3/parser/ASTStringLiteral.java | 4 - .../jexl3/parser/FeatureController.java | 229 +++ .../apache/commons/jexl3/parser/JexlNode.java | 60 +- .../commons/jexl3/parser/JexlParser.java | 260 ++- .../commons/jexl3/parser/NumberParser.java | 32 +- .../apache/commons/jexl3/parser/Parser.jjt | 62 +- .../commons/jexl3/parser/ParserVisitor.java | 8 +- .../scripting/JexlScriptEngineFactory.java | 6 +- src/site/site.xml | 6 +- src/site/xdoc/changes.xml | 158 +- src/site/xdoc/download_jexl.xml | 30 +- src/site/xdoc/reference/examples.xml | 2 +- src/site/xdoc/reference/jsr223.xml | 2 +- src/site/xdoc/reference/syntax.xml | 1542 ++++++++++------- .../apache/commons/jexl3/AnnotationTest.java | 275 +++ .../apache/commons/jexl3/AntishCallTest.java | 174 ++ .../commons/jexl3/ArithmeticOperatorTest.java | 78 + .../apache/commons/jexl3/ArithmeticTest.java | 110 +- .../apache/commons/jexl3/ArrayAccessTest.java | 25 + .../org/apache/commons/jexl3/AssignTest.java | 1 + .../org/apache/commons/jexl3/CaptureLog.java | 137 ++ .../commons/jexl3/ContextNamespaceTest.java | 48 + .../apache/commons/jexl3/ExceptionTest.java | 121 +- .../apache/commons/jexl3/FeaturesTest.java | 290 ++++ .../java/org/apache/commons/jexl3/Foo.java | 9 +- .../java/org/apache/commons/jexl3/IfTest.java | 96 +- .../org/apache/commons/jexl3/IssuesTest.java | 784 +-------- .../apache/commons/jexl3/IssuesTest100.java | 814 +++++++++ .../apache/commons/jexl3/IssuesTest200.java | 350 ++++ .../org/apache/commons/jexl3/JXLTTest.java | 13 +- .../apache/commons/jexl3/JexlEvalContext.java | 38 +- .../org/apache/commons/jexl3/JexlTest.java | 47 +- .../org/apache/commons/jexl3/LambdaTest.java | 1 + .../org/apache/commons/jexl3/MethodTest.java | 117 +- .../org/apache/commons/jexl3/PragmaTest.java | 29 +- .../commons/jexl3/PropertyAccessTest.java | 123 +- .../commons/jexl3/PublicFieldsTest.java | 40 +- .../apache/commons/jexl3/ReadonlyContext.java | 6 +- .../commons/jexl3/ScriptCallableTest.java | 357 +++- .../org/apache/commons/jexl3/ScriptTest.java | 37 +- .../apache/commons/jexl3/SetLiteralTest.java | 13 + .../apache/commons/jexl3/SideEffectTest.java | 227 +++ .../commons/jexl3/SynchronizedArithmetic.java | 256 +++ .../commons/jexl3/SynchronizedContext.java | 74 + .../jexl3/SynchronizedOverloadsTest.java | 84 + .../commons/jexl3/examples/ArrayTest.java | 3 +- .../jexl3/examples/MethodPropertyTest.java | 5 +- .../apache/commons/jexl3/examples/Output.java | 5 +- .../apache/commons/jexl3/internal/Util.java | 14 +- .../internal/introspection/MethodKeyTest.java | 73 +- .../jexl3/introspection/SandboxTest.java | 36 +- .../apache/commons/jexl3/junit/Asserter.java | 28 +- .../commons/jexl3/junit/AsserterTest.java | 8 +- .../commons/jexl3/parser/ParserTest.java | 50 +- .../JexlScriptEngineOptionalTest.java | 20 +- .../jexl3/scripting/JexlScriptEngineTest.java | 104 +- src/test/resources/log4j.xml | 34 - src/test/scripts/testAdd.jexl | 18 + 115 files changed, 9737 insertions(+), 3473 deletions(-) rename src/{main => }/assembly/bin.xml (100%) rename src/{main => }/assembly/src.xml (100%) create mode 100644 src/main/java/org/apache/commons/jexl3/JexlFeatures.java create mode 100644 src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java create mode 100644 src/main/java/org/apache/commons/jexl3/internal/ScriptVisitor.java create mode 100644 src/main/java/org/apache/commons/jexl3/internal/SoftCache.java create mode 100644 src/main/java/org/apache/commons/jexl3/internal/Source.java create mode 100644 src/main/java/org/apache/commons/jexl3/parser/ASTAnnotation.java create mode 100644 src/main/java/org/apache/commons/jexl3/parser/FeatureController.java create mode 100644 src/test/java/org/apache/commons/jexl3/AnnotationTest.java create mode 100644 src/test/java/org/apache/commons/jexl3/AntishCallTest.java create mode 100644 src/test/java/org/apache/commons/jexl3/CaptureLog.java create mode 100644 src/test/java/org/apache/commons/jexl3/FeaturesTest.java create mode 100644 src/test/java/org/apache/commons/jexl3/IssuesTest100.java create mode 100644 src/test/java/org/apache/commons/jexl3/IssuesTest200.java create mode 100644 src/test/java/org/apache/commons/jexl3/SynchronizedArithmetic.java create mode 100644 src/test/java/org/apache/commons/jexl3/SynchronizedContext.java create mode 100644 src/test/java/org/apache/commons/jexl3/SynchronizedOverloadsTest.java delete mode 100644 src/test/resources/log4j.xml create mode 100644 src/test/scripts/testAdd.jexl diff --git a/.travis.yml b/.travis.yml index 0a0f062a8..f48251075 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + language: java sudo: false +dist: precise + jdk: - openjdk6 - openjdk7 - oraclejdk8 -# only build trunk -branches: - only: - - trunk - after_success: - mvn clean test jacoco:report coveralls:report diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3ba801ea..8485c81c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,47 +51,65 @@ Getting Started + Make sure you have a [JIRA account](https://issues.apache.org/jira/). + Make sure you have a [GitHub account](https://github.com/signup/free). + If you're planning to implement a new feature it makes sense to discuss you're changes on the [dev list](https://commons.apache.org/mail-lists.html) first. This way you can make sure you're not wasting your time on something that isn't considered to be in Apache Commons JEXL's scope. -+ Submit a ticket for your issue, assuming one does not already exist. ++ Submit a [Jira Ticket][jira] for your issue, assuming one does not already exist. + Clearly describe the issue including steps to reproduce when it is a bug. + Make sure you fill in the earliest version that you know has the issue. -+ Fork the repository on GitHub. ++ Find the corresponding [repository on GitHub](https://github.com/apache/?query=commons-), +[fork](https://help.github.com/articles/fork-a-repo/) and check out your forked repository. Making Changes -------------- -+ Create a topic branch from where you want to base your work (this is usually the master/trunk branch). ++ Create a _topic branch_ for your isolated work. + * Usually you should base your branch on the `master` or `trunk` branch. + * A good topic branch name can be the JIRA bug id plus a keyword, e.g. `JEXL-123-InputStream`. + * If you have submitted multiple JIRA issues, try to maintain separate branches and pull requests. + Make commits of logical units. + * Make sure your commit messages are meaningful and in the proper format. Your commit message should contain the key of the JIRA issue. + * e.g. `JEXL-123: Close input stream earlier` + Respect the original code style: + Only use spaces for indentation. - + Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted create a separate PR for this change. - + Check for unnecessary whitespace with git diff --check before committing. -+ Make sure your commit messages are in the proper format. Your commit message should contain the key of the JIRA issue. -+ Make sure you have added the necessary tests for your changes. + + Create minimal diffs - disable _On Save_ actions like _Reformat Source Code_ or _Organize Imports_. If you feel the source code should be reformatted create a separate PR for this change first. + + Check for unnecessary whitespace with `git diff` -- check before committing. ++ Make sure you have added the necessary tests for your changes, typically in `src/test/java`. + Run all the tests with `mvn clean verify` to assure nothing else was accidentally broken. Making Trivial Changes ---------------------- +The JIRA tickets are used to generate the changelog for the next release. + For changes of a trivial nature to comments and documentation, it is not always necessary to create a new ticket in JIRA. In this case, it is appropriate to start the first line of a commit with '(doc)' instead of a ticket number. + Submitting Changes ------------------ -+ Sign the [Contributor License Agreement][cla] if you haven't already. ++ Sign and submit the Apache [Contributor License Agreement][cla] if you haven't already. + * Note that small patches & typical bug fixes do not require a CLA as + clause 5 of the [Apache License](https://www.apache.org/licenses/LICENSE-2.0.html#contributions) + covers them. + Push your changes to a topic branch in your fork of the repository. -+ Submit a pull request to the repository in the apache organization. ++ Submit a _Pull Request_ to the corresponding repository in the `apache` organization. + * Verify _Files Changed_ shows only your intended changes and does not + include additional files like `target/*.class` + Update your JIRA ticket and include a link to the pull request in the ticket. +If you prefer to not use GitHub, then you can instead use +`git format-patch` (or `svn diff`) and attach the patch file to the JIRA issue. + + Additional Resources -------------------- + [Contributing patches](https://commons.apache.org/patches.html) -+ [Apache Commons JEXL JIRA project page](https://issues.apache.org/jira/browse/JEXL) ++ [Apache Commons JEXL JIRA project page][jira] + [Contributor License Agreement][cla] + [General GitHub documentation](https://help.github.com/) + [GitHub pull request documentation](https://help.github.com/send-pull-requests/) + [Apache Commons Twitter Account](https://twitter.com/ApacheCommons) -+ #apachecommons IRC channel on freenode.org ++ `#apache-commons` IRC channel on `irc.freenode.net` [cla]:https://www.apache.org/licenses/#clas +[jira]:https://issues.apache.org/jira/browse/JEXL diff --git a/NOTICE.txt b/NOTICE.txt index f80f9dd2a..388baaab9 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Apache Commons JEXL -Copyright 2001-2015 The Apache Software Foundation +Copyright 2001-2017 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). diff --git a/README.md b/README.md index 3c5bac0cc..f30cd0d04 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,103 @@ - - -Apache Commons JEXL -=================== - -The Apache Commons JEXL library is an implementation of the JSTL Expression Language with extensions. - -Documentation -------------- - -More information can be found on the [homepage](https://commons.apache.org/proper/commons-jexl). -The [JavaDoc](https://commons.apache.org/proper/commons-jexl/javadocs/api-release) can be browsed. -Questions related to the usage of Apache Commons JEXL should be posted to the [user mailing list][ml]. - -Where can I get the latest release? ------------------------------------ -You can download source and binaries from our [download page](https://commons.apache.org/proper/commons-jexl/download_jexl.cgi). - -Alternatively you can pull it from the central Maven repositories: - -```xml - - org.apache.commons - commons-jexl3 - 3.0 - -``` - -Contributing ------------- - -We accept PRs via github. The [developer mailing list][ml] is the main channel of communication for contributors. -There are some guidelines which will make applying PRs easier for us: -+ No tabs! Please use spaces for indentation. -+ Respect the code style. -+ Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted create a separate PR for this change. -+ Provide JUnit tests for your changes and make sure your changes don't break any existing tests by running ```mvn clean test```. - -If you plan to contribute on a regular basis, please consider filing a [contributor license agreement](https://www.apache.org/licenses/#clas). -You can learn more about contributing via GitHub in our [contribution guidelines](CONTRIBUTING.md). - -License -------- -Code is under the [Apache Licence v2](https://www.apache.org/licenses/LICENSE-2.0.txt). - -Donations ---------- -You like Apache Commons JEXL? Then [donate back to the ASF](https://www.apache.org/foundation/contributing.html) to support the development. - -Additional Resources --------------------- - -+ [Apache Commons Homepage](https://commons.apache.org/) -+ [Apache Bugtracker (JIRA)](https://issues.apache.org/jira/) -+ [Apache Commons Twitter Account](https://twitter.com/ApacheCommons) -+ #apachecommons IRC channel on freenode.org - -[ml]:https://commons.apache.org/mail-lists.html + + +Apache Commons JEXL +=================== + +[![Build Status](https://travis-ci.org/apache/commons-jexl.svg?branch=trunk)](https://travis-ci.org/apache/commons-jexl) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.commons/commons-jexl3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.apache.commons/commons-jexl3/) + +The Apache Commons JEXL library is an implementation of the JSTL Expression Language with extensions. + +Documentation +------------- + +More information can be found on the [Apache Commons JEXL homepage](https://commons.apache.org/proper/commons-jexl). +The [JavaDoc](https://commons.apache.org/proper/commons-jexl/javadocs/api-release) can be browsed. +Questions related to the usage of Apache Commons JEXL should be posted to the [user mailing list][ml]. + +Where can I get the latest release? +----------------------------------- +You can download source and binaries from our [download page](https://commons.apache.org/proper/commons-jexl/download_jexl.cgi). + +Alternatively you can pull it from the central Maven repositories: + +```xml + + org.apache.commons + commons-jexl3 + 3.1 + +``` + +Contributing +------------ + +We accept Pull Requests via GitHub. The [developer mailing list][ml] is the main channel of communication for contributors. +There are some guidelines which will make applying PRs easier for us: ++ No tabs! Please use spaces for indentation. ++ Respect the code style. ++ Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted create a separate PR for this change. ++ Provide JUnit tests for your changes and make sure your changes don't break any existing tests by running ```mvn clean test```. + +If you plan to contribute on a regular basis, please consider filing a [contributor license agreement](https://www.apache.org/licenses/#clas). +You can learn more about contributing via GitHub in our [contribution guidelines](CONTRIBUTING.md). + +License +------- +This code is under the [Apache Licence v2](https://www.apache.org/licenses/LICENSE-2.0). + +See the `NOTICE.txt` file for required notices and attributions. + +Donations +--------- +You like Apache Commons JEXL? Then [donate back to the ASF](https://www.apache.org/foundation/contributing.html) to support the development. + +Additional Resources +-------------------- + ++ [Apache Commons Homepage](https://commons.apache.org/) ++ [Apache Issue Tracker (JIRA)](https://issues.apache.org/jira/browse/JEXL) ++ [Apache Commons Twitter Account](https://twitter.com/ApacheCommons) ++ `#apache-commons` IRC channel on `irc.freenode.org` + +[ml]:https://commons.apache.org/mail-lists.html diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 229360ba6..5dd65e994 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,6 @@ Apache Commons JEXL - Version 3.0 + Version 3.2 Release Notes @@ -19,6 +19,114 @@ Its goal is to expose scripting features usable by technical operatives or consu http://commons.apache.org/jexl/ +======================================================================================================================== +Release 3.2 +======================================================================================================================== + +Version 3.2 is a minor release. + +New Features in 3.2: +==================== + +* JEXL-248: Allow range subexpression as an array property assignment identifier +* JEXL-243: Allow restricting available features in Script/Expressions +* JEXL-238: Restrict getLiteralClass to a Number for NumberLiterals +* JEXL-237: Ability to restrict usage of certain names when declaring local variables +* JEXL-236: Support CharSequence in size(), empty() and contains() operators +* JEXL-234: Extend application of operators startsWith and endsWith from String to CharSequence types +* JEXL-226: add ?? operator support +* JEXL-224: The ability to overload call() operator in customized JexlArithmetic implementation +* JEXL-212: Restrict usage of assignment statements in JexlExpression + +Bugs Fixed in 3.2: +================== + +* JEXL-246: Intermittent ambiguous method invocation when processing assignOverload +* JEXL-245: Engine in strict mode fails to fail on unsolvable variables or properties +* JEXL-244: Webapp classloader memory leaks +* JEXL-241: NPE when script containing string interpolation executed in multiple threads +* JEXL-240: Unable to invoke a call operator using antish style variable resolution +* JEXL-231: Syntax for accessing List elements is not mentioned in docs +* JEXL-230: List literal is not mentioned in docs +* JEXL-227: JexlScriptEngineFactory.getEngineVersion() should return actual version +* JEXL-225: Incorrect invoking methods with ObjectContext + +There are no other changes. + + +======================================================================================================================== +Release 3.1 +======================================================================================================================== + +Version 3.1 is a minor release. + +Compatibility with previous releases +==================================== +Version 3.1 is binary compatible with 3.0. + +However it is not source compatible, this release does break source compatibility by adding methods to existing +interfaces; these interfaces are however not expected to be implemented by code external to the JEXL project. +This compatibility break should remain hypothetical. It is nevertheless possible and we are sorry if it causes problems +to any of you. +If you encounter this issue, besides continuing using JEXL 3.0, your code likely delegates method calls to JEXL3 objects +and you shall be able to continue doing so with these new methods. + +The three interfaces and methods are: + +1 - 'isCancellable()' in interface org.apache.commons.jexl3.JexlEngine$Options +2 - 'callable(JexlContext)' in interface org.apache.commons.jexl3.JexlExpression +3 - 'getPragmas()' in interface org.apache.commons.jexl3.JxltEngine$Template + +What's new in 3.1: +================== +* Annotations syntax (@annotation) and processing capabilities. + Annotations in JEXL are 'meta-statements'; they allow to wrap the execution of the JEXL statement in a user provided + caller; a typical example would be: ""@synchronized(x) x.someMethod();". Annotations may be declared with zero or more + parameters. Annotation processing is implemented by providing a JexlContext.AnnotationProcessor; its processAnnotation + method will call the annotated statement encapsulated in a Callable. Annotation arguments are evaluated and passed + as arguments to processAnnotation. +* Better support for script execution options, error handling and cancelling. +* All operators can be overloaded. + +New Features in 3.1: +==================== +* JEXL-222: The ability to declare indexed property getter/setter in customized JexlArithmetic implementation +* JEXL-219: Blacklist by default in sandbox +* JEXL-216: Improve parsing concurrency in multithreaded environment +* JEXL-211: Add callable method to JexlExpression interface +* JEXL-201: Allow Interpreter to use live values from JexlEngine.Option interface implemented by JexlContext +* JEXL-198: JxltEngine Template does not expose pragmas +* JEXL-197: Add annotations +* JEXL-194: Allow synchronization on iterableValue in foreach statement + +Bugs Fixed in 3.1: +================== +* JEXL-221: Sporadic undefined property error caused by NPE at MapGetExecutor.tryInvoke() +* JEXL-217: Interpreter.getAttribute() raises exception in non-strict mode when cached property resolver is used +* JEXL-215: JexlEngine.createInfo() is redundantly called when debug and caching is enabled leading to sub-optimal performance +* JEXL-214: Redundant call of fillInStackTrace() in JexlEngine.createInfo() ? +* JEXL-213: rename JexlBuilder.loader(Charset arg) to JexlBuilder.charset(Charset arg) +* JEXL-210: The way to cancel script execution with an error +* JEXL-209: Unsolvable function/method '.(...)' +* JEXL-208: Documentation typos/inconsistencies +* JEXL-207: Inconsistent error handling +* JEXL-206: testCallableCancel() test hangs sporadically +* JEXL-205: testCancelForever() is not terminated properly as 'Fixed' +* JEXL-204: Script is not interrupted by a method call throwing Exception +* JEXL-203: JexlArithmetic.options() diverts Interpreter to use default implementation of JexlArithmetic instead of custom one +* JEXL-202: Detect invalid assignment operator usage with non-assignable l-value during script parsing +* JEXL-196: Script execution hangs while calling method with one argument without parameter +* JEXL-195: Support for AtomicBoolean in logical expressions +* JEXL-193: InterruptedException is swallowed in function call in silent and non-strict mode +* JEXL-192: Invalid return type when expected result is null +* JEXL-191: Jexl3 unsolvable property exception when using enum +* JEXL-190: local function within context is not resolved if function resolver class without namespace is specified +* JEXL-189: Possible bug in Interpreter.isCancelled() +* JEXL-188: Possible bug in JexlArithmetic.isFloatingPointNumber() +* JEXL-187: Jexl Syntax doc does not mention 'continue' and 'break' operators +* JEXL-186: Performance regression in arithmetic operations compared to JEXL 2.1 + + ======================================================================================================================== Release 3.0 ======================================================================================================================== diff --git a/pom.xml b/pom.xml index 2579be8fd..eb5910531 100644 --- a/pom.xml +++ b/pom.xml @@ -19,16 +19,16 @@ org.apache.commons commons-parent - 39 + 43 4.0.0 org.apache.commons commons-jexl3 - 3.0-SNAPSHOT + 3.2-SNAPSHOT Apache Commons JEXL 2001 The Apache Commons JEXL library is an implementation of the JSTL Expression Language with extensions. - http://commons.apache.org/jexl/ + http://commons.apache.org/proper/commons-jexl/ jira @@ -42,11 +42,11 @@ - - people.apache.org - Apache Commons JEXL - scp://people.apache.org/www/commons.apache.org/jexl - + + apache.website + Apache Commons Site + scm:svn:https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-net/ + @@ -115,8 +115,9 @@ 1.6 1.6 - jexl - 3.0 + jexl3 + org.apache.commons.jexl3 + 3.1 jexl https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-jexl site-content @@ -124,6 +125,7 @@ RC1 2.1.1 + commons-jexl-${commons.release.version} commons-jexl-${commons.release.2.version} commons-jexl-${commons.release.3.version} Legacy @@ -131,7 +133,7 @@ JEXL 12310479 - 2.16 + 2.17 6.13 @@ -148,14 +150,16 @@ **/*Test.java + + -Xmx64m maven-assembly-plugin - src/main/assembly/bin.xml - src/main/assembly/src.xml + src/assembly/bin.xml + src/assembly/src.xml gnu commons-jexl-${project.version} @@ -244,6 +248,18 @@ + + + org.codehaus.mojo + clirr-maven-plugin + 2.8 + + + org/apache/commons/jexl3/parser/** + org/apache/commons/jexl3/internal/** + + + org.apache.rat @@ -270,7 +286,7 @@ org.apache.maven.plugins maven-changes-plugin - 2.11 + 2.12.1 ${basedir}/src/site/xdoc/changes.xml @@ -297,7 +313,7 @@ org.codehaus.mojo findbugs-maven-plugin - 3.0.2 + 3.0.4 ${basedir}/src/main/config/findbugs-exclude-filter.xml true @@ -308,7 +324,7 @@ org.apache.maven.plugins maven-pmd-plugin - 3.5 + 3.7 ${maven.compiler.target} @@ -327,7 +343,7 @@ org.codehaus.mojo clirr-maven-plugin - + 2.8 org/apache/commons/jexl3/parser/** @@ -346,4 +362,4 @@ - \ No newline at end of file + diff --git a/src/main/assembly/bin.xml b/src/assembly/bin.xml similarity index 100% rename from src/main/assembly/bin.xml rename to src/assembly/bin.xml diff --git a/src/main/assembly/src.xml b/src/assembly/src.xml similarity index 100% rename from src/main/assembly/src.xml rename to src/assembly/src.xml diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index 638349b80..2e6114eaa 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -25,13 +25,15 @@ import java.math.MathContext; import java.util.Collection; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Perform arithmetic, implements JexlOperator methods. - * + * *

This is the class to derive to implement new operator behaviors.

- * + * *

The 5 base arithmetic operators (+, - , *, /, %) follow the same evaluation rules regarding their arguments.

*
    *
  1. If both are null, result is 0
  2. @@ -45,9 +47,9 @@ *
* * - * + * * Note that the only exception thrown by JexlArithmetic is and must be ArithmeticException. - * + * * @see JexlOperator * @since 2.0 */ @@ -82,7 +84,7 @@ public static class NullOperand extends ArithmeticException {} /** * Creates a JexlArithmetic. - * + * * @param astrict whether this arithmetic is strict or lenient */ public JexlArithmetic(boolean astrict) { @@ -91,7 +93,7 @@ public JexlArithmetic(boolean astrict) { /** * Creates a JexlArithmetic. - * + * * @param astrict whether this arithmetic is lenient or strict * @param bigdContext the math context instance to use for +,-,/,*,% operations on big decimals. * @param bigdScale the scale used for big decimals. @@ -104,29 +106,59 @@ public JexlArithmetic(boolean astrict, MathContext bigdContext, int bigdScale) { /** * Apply options to this arithmetic which eventually may create another instance. - * + * @see #createWithOptions(boolean, java.math.MathContext, int) + * * @param options the {@link JexlEngine.Options} to use * @return an arithmetic with those options set */ public JexlArithmetic options(JexlEngine.Options options) { - boolean ostrict = options.isStrictArithmetic() == null - ? this.strict - : options.isStrictArithmetic(); + Boolean ostrict = options.isStrictArithmetic(); + if (ostrict == null) { + ostrict = isStrict(); + } MathContext bigdContext = options.getArithmeticMathContext(); if (bigdContext == null) { - bigdContext = mathContext; + bigdContext = getMathContext(); } int bigdScale = options.getArithmeticMathScale(); if (bigdScale == Integer.MIN_VALUE) { - bigdScale = mathScale; + bigdScale = getMathScale(); } - if ((ostrict != this.strict) - || bigdScale != this.mathScale - || bigdContext != this.mathContext) { - return new JexlArithmetic(ostrict, bigdContext, bigdScale); - } else { - return this; + if (ostrict != isStrict() + || bigdScale != getMathScale() + || bigdContext != getMathContext()) { + return createWithOptions(ostrict, bigdContext, bigdScale); } + return this; + } + + /** + * Apply options to this arithmetic which eventually may create another instance. + * @see #createWithOptions(boolean, java.math.MathContext, int) + * + * @param context the context that may extend {@link JexlEngine.Options} to use + * @return a new arithmetic instance or this + * @since 3.1 + */ + public JexlArithmetic options(JexlContext context) { + return context instanceof JexlEngine.Options + ? options((JexlEngine.Options) context) + : this; + } + + /** + * Creates a JexlArithmetic instance. + * Called by options(...) method when another instance of the same class of arithmetic is required. + * @see #options(org.apache.commons.jexl3.JexlEngine.Options) + * + * @param astrict whether this arithmetic is lenient or strict + * @param bigdContext the math context instance to use for +,-,/,*,% operations on big decimals. + * @param bigdScale the scale used for big decimals. + * @return default is a new JexlArithmetic instance + * @since 3.1 + */ + protected JexlArithmetic createWithOptions(boolean astrict, MathContext bigdContext, int bigdScale) { + return new JexlArithmetic(astrict, bigdContext, bigdScale); } /** @@ -136,7 +168,7 @@ public JexlArithmetic options(JexlEngine.Options options) { public interface Uberspect { /** * Checks whether this uberspect has overloads for a given operator. - * + * * @param operator the operator to check * @return true if an overload exists, false otherwise */ @@ -144,7 +176,7 @@ public interface Uberspect { /** * Gets the most specific method for an operator. - * + * * @param operator the operator * @param arg the arguments * @return the most specific method or null if no specific override could be found @@ -154,9 +186,9 @@ public interface Uberspect { /** * Helper interface used when creating an array literal. - * + * *

The default implementation creates an array and attempts to type it strictly.

- * + * *
    *
  • If all objects are of the same type, the array returned will be an array of that same type
  • *
  • If all objects are Numbers, the array returned will be an array of Numbers
  • @@ -168,14 +200,14 @@ public interface ArrayBuilder { /** * Adds a literal to the array. - * + * * @param value the item to add */ void add(Object value); /** * Creates the actual "array" instance. - * + * * @param extended true when the last argument is ', ...' * @return the array */ @@ -184,7 +216,7 @@ public interface ArrayBuilder { /** * Called by the interpreter when evaluating a literal array. - * + * * @param size the number of elements in the array * @return the array builder */ @@ -199,14 +231,14 @@ public ArrayBuilder arrayBuilder(int size) { public interface SetBuilder { /** * Adds a literal to the set. - * + * * @param value the item to add */ void add(Object value); /** * Creates the actual "set" instance. - * + * * @return the set */ Object create(); @@ -214,7 +246,7 @@ public interface SetBuilder { /** * Called by the interpreter when evaluating a literal set. - * + * * @param size the number of elements in the set * @return the array builder */ @@ -229,7 +261,7 @@ public SetBuilder setBuilder(int size) { public interface MapBuilder { /** * Adds a new entry to the map. - * + * * @param key the map entry key * @param value the map entry value */ @@ -237,7 +269,7 @@ public interface MapBuilder { /** * Creates the actual "map" instance. - * + * * @return the map */ Object create(); @@ -245,7 +277,7 @@ public interface MapBuilder { /** * Called by the interpreter when evaluating a literal map. - * + * * @param size the number of elements in the map * @return the map builder */ @@ -256,7 +288,7 @@ public MapBuilder mapBuilder(int size) { /** * Creates a literal range. *

    The default implementation only accepts integers and longs.

    - * + * * @param from the included lower bound value (null if none) * @param to the included upper bound value (null if none) * @return the range as an iterable @@ -276,7 +308,7 @@ public Iterable createRange(Object from, Object to) throws ArithmeticExceptio /** * Checks whether this JexlArithmetic instance * strictly considers null as an error when used as operand unexpectedly. - * + * * @return true if strict, false if lenient */ public boolean isStrict() { @@ -285,7 +317,7 @@ public boolean isStrict() { /** * The MathContext instance used for +,-,/,*,% operations on big decimals. - * + * * @return the math context */ public MathContext getMathContext() { @@ -294,7 +326,7 @@ public MathContext getMathContext() { /** * The BigDecimal scale used for comparison and coericion operations. - * + * * @return the scale */ public int getMathScale() { @@ -303,7 +335,7 @@ public int getMathScale() { /** * Ensure a big decimal is rounded by this arithmetic scale and rounding mode. - * + * * @param number the big decimal to round * @return the rounded big decimal */ @@ -318,7 +350,7 @@ protected BigDecimal roundBigDecimal(final BigDecimal number) { /** * The result of +,/,-,*,% when both operands are null. - * + * * @return Integer(0) if lenient * @throws ArithmeticException if strict */ @@ -331,7 +363,7 @@ protected Object controlNullNullOperands() { /** * Throw a NPE if arithmetic is strict. - * + * * @throws ArithmeticException if strict */ protected void controlNullOperand() { @@ -342,8 +374,11 @@ protected void controlNullOperand() { /** * The float regular expression pattern. + *

    + * The decimal and exponent parts are optional and captured allowing to determine if the number is a real + * by checking whether one of these 2 capturing groups is not empty. */ - public static final Pattern FLOAT_PATTERN = Pattern.compile("^[+-]?\\d*(\\.\\d*)?([eE]?[+-]?\\d*)?$"); + public static final Pattern FLOAT_PATTERN = Pattern.compile("^[+-]?\\d*(\\.\\d*)?([eE][+-]?\\d+)?$"); /** * Test if the passed value is a floating point number, i.e. a float, double @@ -356,19 +391,11 @@ protected boolean isFloatingPointNumber(Object val) { if (val instanceof Float || val instanceof Double) { return true; } - if (val instanceof String) { - String str = (String) val; - for(int c = 0; c < str.length(); ++c) { - char ch = str.charAt(c); - // we need at least a marker that says it is a float - if (ch == '.' || ch == 'E' || ch == 'e') { - return FLOAT_PATTERN.matcher(str).matches(); - } - // and it must be a number - if (ch != '+' && ch != '-' && ch < '0' && ch > '9') { - break; - } - } + if (val instanceof CharSequence) { + final Matcher m = FLOAT_PATTERN.matcher((CharSequence) val); + // first group is decimal, second is exponent; + // one of them must exist hence start({1,2}) >= 0 + return m.matches() && (m.start(1) >= 0 || m.start(2) >= 0); } return false; } @@ -414,7 +441,7 @@ public Number narrow(Number original) { /** * Whether we consider the narrow class as a potential candidate for narrowing the source. - * + * * @param narrow the target narrow class * @param source the orginal source class * @return true if attempt to narrow source to target is accepted @@ -425,7 +452,7 @@ protected boolean narrowAccept(Class narrow, Class source) { /** * Given a Number, return back the value attempting to narrow it to a target class. - * + * * @param original the original number * @param narrow the attempted target class * @return the narrowed number or the source if no narrowing was possible @@ -502,7 +529,7 @@ public Number narrowNumber(Number original, Class narrow) { * if either arguments is a BigInteger, no narrowing will occur * if either arguments is a Long, no narrowing to Integer will occur *

    - * + * * @param lhs the left hand side operand that lead to the bigi result * @param rhs the right hand side operand that lead to the bigi result * @param bigi the BigInteger to narrow @@ -554,21 +581,23 @@ protected Number narrowBigDecimal(Object lhs, Object rhs, BigDecimal bigd) { /** * Replace all numbers in an arguments array with the smallest type that will fit. - * + * * @param args the argument array * @return true if some arguments were narrowed and args array is modified, * false if no narrowing occurred and args array has not been modified */ public boolean narrowArguments(Object[] args) { boolean narrowed = false; - for (int a = 0; a < args.length; ++a) { - Object arg = args[a]; - if (arg instanceof Number) { - Number narg = (Number) arg; - Number narrow = narrow(narg); - if (!narg.equals(narrow)) { - args[a] = narrow; - narrowed = true; + if (args != null) { + for (int a = 0; a < args.length; ++a) { + Object arg = args[a]; + if (arg instanceof Number) { + Number narg = (Number) arg; + Number narrow = narrow(narg); + if (!narg.equals(narrow)) { + args[a] = narrow; + narrowed = true; + } } } } @@ -581,7 +610,7 @@ public boolean narrowArguments(Object[] args) { * If any numeric add fails on coercion to the appropriate type, * treat as Strings and do concatenation. *

    - * + * * @param left left argument * @param right right argument * @return left + right. @@ -624,7 +653,7 @@ public Object add(Object left, Object right) { /** * Divide the left value by the right. - * + * * @param left left argument * @param right right argument * @return left / right @@ -665,7 +694,7 @@ public Object divide(Object left, Object right) { /** * left value modulo right. - * + * * @param left left argument * @param right right argument * @return left % right @@ -697,16 +726,16 @@ public Object mod(Object left, Object right) { // otherwise treat as integers BigInteger l = toBigInteger(left); BigInteger r = toBigInteger(right); - BigInteger result = l.mod(r); if (BigInteger.ZERO.equals(r)) { throw new ArithmeticException("%"); } + BigInteger result = l.mod(r); return narrowBigInteger(left, right, result); } /** * Multiply the left value by the right. - * + * * @param left left argument * @param right right argument * @return left * right. @@ -737,7 +766,7 @@ public Object multiply(Object left, Object right) { /** * Subtract the right value from the left. - * + * * @param left left argument * @param right right argument * @return left - right. @@ -768,7 +797,7 @@ public Object subtract(Object left, Object right) { /** * Negates a value (unary minus for numbers). - * + * * @param val the value to negate * @return the negated value */ @@ -791,6 +820,8 @@ public Object negate(Object val) { return (byte) -((Byte) val); } else if (val instanceof Boolean) { return ((Boolean) val) ? Boolean.FALSE : Boolean.TRUE; + } else if (val instanceof AtomicBoolean) { + return ((AtomicBoolean) val).get() ? Boolean.FALSE : Boolean.TRUE; } throw new ArithmeticException("Object negation:(" + val + ")"); } @@ -799,7 +830,7 @@ public Object negate(Object val) { * Test if left contains right (right matches/in left). *

    Beware that this method arguments are the opposite of the operator arguments. * 'x in y' means 'y contains x'.

    - * + * * @param container the container * @param value the value * @return test result or null if there is no arithmetic solution @@ -817,7 +848,7 @@ public Boolean contains(Object container, Object value) { if (container instanceof java.util.regex.Pattern) { return ((java.util.regex.Pattern) container).matcher(value.toString()).matches(); } - if (container instanceof String) { + if (container instanceof CharSequence) { return value.toString().matches(container.toString()); } // try contains on map key @@ -854,8 +885,8 @@ public Boolean endsWith(Object left, Object right) { // we know both aren't null, therefore L != R return false; } - if (left instanceof String) { - return ((String) left).endsWith(toString(right)); + if (left instanceof CharSequence) { + return (toString(left)).endsWith(toString(right)); } return null; } @@ -876,25 +907,25 @@ public Boolean startsWith(Object left, Object right) { // we know both aren't null, therefore L != R return false; } - if (left instanceof String) { - return ((String) left).startsWith(toString(right)); + if (left instanceof CharSequence) { + return (toString(left)).startsWith(toString(right)); } return null; } /** - * Check for emptyness of various types: Number, Collection, Array, Map, String. + * Check for emptiness of various types: Number, Collection, Array, Map, String. * - * @param object the object to check the emptyness of - * @return the boolean or null of there is no arithmetic solution + * @param object the object to check the emptiness of + * @return the boolean or null if there is no arithmetic solution */ public Boolean isEmpty(Object object) { if (object instanceof Number) { double d = ((Number) object).doubleValue(); return Double.isNaN(d) || d == 0.d ? Boolean.TRUE : Boolean.FALSE; } - if (object instanceof String) { - return "".equals(object) ? Boolean.TRUE : Boolean.FALSE; + if (object instanceof CharSequence) { + return ((CharSequence) object).length() == 0 ? Boolean.TRUE : Boolean.FALSE; } if (object.getClass().isArray()) { return Array.getLength(object) == 0 ? Boolean.TRUE : Boolean.FALSE; @@ -916,8 +947,8 @@ public Boolean isEmpty(Object object) { * @return the size of object or null if there is no arithmetic solution */ public Integer size(Object object) { - if (object instanceof String) { - return ((String) object).length(); + if (object instanceof CharSequence) { + return ((CharSequence) object).length(); } if (object.getClass().isArray()) { return Array.getLength(object); @@ -933,7 +964,7 @@ public Integer size(Object object) { /** * Performs a bitwise and. - * + * * @param left the left operand * @param right the right operator * @return left & right @@ -946,7 +977,7 @@ public Object and(Object left, Object right) { /** * Performs a bitwise or. - * + * * @param left the left operand * @param right the right operator * @return left | right @@ -959,7 +990,7 @@ public Object or(Object left, Object right) { /** * Performs a bitwise xor. - * + * * @param left the left operand * @param right the right operator * @return left ^ right @@ -972,7 +1003,7 @@ public Object xor(Object left, Object right) { /** * Performs a bitwise complement. - * + * * @param val the operand * @return ~val */ @@ -983,7 +1014,7 @@ public Object complement(Object val) { /** * Performs a logical not. - * + * * @param val the operand * @return !val */ @@ -993,7 +1024,7 @@ public Object not(Object val) { /** * Performs a comparison. - * + * * @param left the left operand * @param right the right operator * @param operator the operator @@ -1156,6 +1187,8 @@ public boolean toBoolean(Object val) { } else if (val instanceof Number) { double number = toDouble(val); return !Double.isNaN(number) && number != 0.d; + } else if (val instanceof AtomicBoolean) { + return ((AtomicBoolean) val).get(); } else if (val instanceof String) { String strval = val.toString(); return strval.length() > 0 && !"false".equals(strval); @@ -1194,6 +1227,8 @@ public int toInteger(Object val) { return Integer.parseInt((String) val); } else if (val instanceof Boolean) { return ((Boolean) val) ? 1 : 0; + } else if (val instanceof AtomicBoolean) { + return ((AtomicBoolean) val).get() ? 1 : 0; } else if (val instanceof Character) { return ((Character) val); } @@ -1232,6 +1267,8 @@ public long toLong(Object val) { } } else if (val instanceof Boolean) { return ((Boolean) val) ? 1L : 0L; + } else if (val instanceof AtomicBoolean) { + return ((AtomicBoolean) val).get() ? 1L : 0L; } else if (val instanceof Character) { return ((Character) val); } @@ -1268,6 +1305,8 @@ public BigInteger toBigInteger(Object val) { return BigInteger.valueOf(((Number) val).longValue()); } else if (val instanceof Boolean) { return BigInteger.valueOf(((Boolean) val) ? 1L : 0L); + } else if (val instanceof AtomicBoolean) { + return BigInteger.valueOf(((AtomicBoolean) val).get() ? 1L : 0L); } else if (val instanceof String) { String string = (String) val; if ("".equals(string)) { @@ -1309,6 +1348,8 @@ public BigDecimal toBigDecimal(Object val) { return roundBigDecimal(new BigDecimal(val.toString(), getMathContext())); } else if (val instanceof Boolean) { return BigDecimal.valueOf(((Boolean) val) ? 1. : 0.); + } else if (val instanceof AtomicBoolean) { + return BigDecimal.valueOf(((AtomicBoolean) val).get() ? 1L : 0L); } else if (val instanceof String) { String string = (String) val; if ("".equals(string)) { @@ -1327,7 +1368,7 @@ public BigDecimal toBigDecimal(Object val) { * Coerce to a primitive double. *

    Double.NaN, null and empty string coerce to zero.

    *

    Boolean false is 0, true is 1.

    - * + * * @param val value to coerce. * @return The double coerced value. * @throws ArithmeticException if val is null and mode is strict or if coercion is not possible @@ -1344,6 +1385,8 @@ public double toDouble(Object val) { return Double.parseDouble(String.valueOf(val)); } else if (val instanceof Boolean) { return ((Boolean) val) ? 1. : 0.; + } else if (val instanceof AtomicBoolean) { + return ((AtomicBoolean) val).get() ? 1. : 0.; } else if (val instanceof String) { String string = (String) val; if ("".equals(string)) { @@ -1399,7 +1442,7 @@ public final Object bitwiseAnd(Object lhs, Object rhs) { /** * Use or overload or() instead. - * + * * @param lhs left hand side * @param rhs right hand side * @return lhs | rhs @@ -1413,7 +1456,7 @@ public final Object bitwiseOr(Object lhs, Object rhs) { /** * Use or overload xor() instead. - * + * * @param lhs left hand side * @param rhs right hand side * @return lhs ^ rhs @@ -1427,7 +1470,7 @@ public final Object bitwiseXor(Object lhs, Object rhs) { /** * Use or overload not() instead. - * + * * @param arg argument * @return !arg * @see JexlArithmetic#not @@ -1440,7 +1483,7 @@ public final Object logicalNot(Object arg) { /** * Use or overload contains() instead. - * + * * @param lhs left hand side * @param rhs right hand side * @return contains(rhs, lhs) diff --git a/src/main/java/org/apache/commons/jexl3/JexlBuilder.java b/src/main/java/org/apache/commons/jexl3/JexlBuilder.java index 16914c772..7332f576b 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlBuilder.java +++ b/src/main/java/org/apache/commons/jexl3/JexlBuilder.java @@ -27,16 +27,16 @@ /** * Configure and builds a JexlEngine. - * + * *

    The setSilent and setStrict methods allow to fine-tune an engine instance behavior * according to various error control needs. The strict flag tells the engine when and if null as operand is * considered an error, the silent flag tells the engine what to do with the error * (log as warning or throw exception).

    - * + * *
      *
    • When "silent" & "not-strict": *

      0 & null should be indicators of "default" values so that even in an case of error, - * something meaningfull can still be inferred; may be convenient for configurations. + * something meaningful can still be inferred; may be convenient for configurations. *

      *
    • *
    • When "silent" & "strict": @@ -46,7 +46,7 @@ * Use case could be configuration with no implicit values or defaults. *

      *
    • - *
    • When "not-silent" & "not-stict": + *
    • When "not-silent" & "not-strict": *

      The error control grain is roughly on par with JEXL 1.0

      *
    • *
    • When "not-silent" & "strict": @@ -85,6 +85,9 @@ public class JexlBuilder { /** Whether error messages will carry debugging information. */ private Boolean debug = null; + /** Whether interrupt throws JexlException.Cancel. */ + private Boolean cancellable = null; + /** The map of 'prefix:function' to object implementing the namespaces. */ private Map namespaces = null; @@ -103,9 +106,12 @@ public class JexlBuilder { /** The class loader. */ private ClassLoader loader = null; + /** The features. */ + private JexlFeatures features = null; + /** * Sets the JexlUberspect instance the engine will use. - * + * * @param u the uberspect * @return this builder */ @@ -122,7 +128,7 @@ public JexlUberspect uberspect() { /** * Sets the JexlUberspect strategy strategy the engine will use. *

      This is ignored if the uberspect has been set. - * + * * @param rs the strategy * @return this builder */ @@ -138,7 +144,7 @@ public JexlUberspect.ResolverStrategy strategy() { /** * Sets the JexlArithmetic instance the engine will use. - * + * * @param a the arithmetic * @return this builder */ @@ -154,7 +160,7 @@ public JexlArithmetic arithmetic() { /** * Sets the sandbox the engine will use. - * + * * @param box the sandbox * @return this builder */ @@ -168,9 +174,26 @@ public JexlSandbox sandbox() { return this.sandbox; } + /** + * Sets the features the engine will use as a base by default. + *

      Note that the script flag will be ignored; the engine will be able to parse expressions and scripts. + *

      Note also that these will apply to template expressions and scripts. + * @param f the features + * @return this builder + */ + public JexlBuilder features(JexlFeatures f) { + this.features = f; + return this; + } + + /** @return the features */ + public JexlFeatures features() { + return this.features; + } + /** * Sets the o.a.c.Log instance to use. - * + * * @param l the logger * @return this builder */ @@ -186,7 +209,7 @@ public Log logger() { /** * Sets the class loader to use. - * + * * @param l the class loader * @return this builder */ @@ -202,11 +225,24 @@ public ClassLoader loader() { /** * Sets the charset to use. - * + * * @param arg the charset * @return this builder + * @deprecated since 3.1 use {@link #charset(Charset)} instead */ + @Deprecated public JexlBuilder loader(Charset arg) { + return charset(arg); + } + + /** + * Sets the charset to use. + * + * @param arg the charset + * @return this builder + * @since 3.1 + */ + public JexlBuilder charset(Charset arg) { this.charset = arg; return this; } @@ -218,7 +254,7 @@ public Charset charset() { /** * Sets whether the engine will throw JexlException during evaluation when an error is triggered. - * + * * @param flag true means no JexlException will occur, false allows them * @return this builder */ @@ -235,7 +271,7 @@ public Boolean silent() { /** * Sets whether the engine considers unknown variables, methods, functions and constructors as errors or * evaluates them as null. - * + * * @param flag true means strict error reporting, false allows them to be evaluated as null * @return this builder */ @@ -251,7 +287,7 @@ public Boolean strict() { /** * Sets whether the engine will report debugging information when error occurs. - * + * * @param flag true implies debug is on, false implies debug is off. * @return this builder */ @@ -265,6 +301,27 @@ public Boolean debug() { return this.debug; } + /** + * Sets the engine behavior upon interruption: throw an JexlException.Cancel or terminates the current evaluation + * and return null. + * + * @param flag true implies the engine throws the exception, false makes the engine return null. + * @return this builder + * @since 3.1 + */ + public JexlBuilder cancellable(boolean flag) { + this.cancellable = flag; + return this; + } + + /** + * @return the cancellable information flag + * @since 3.1 + */ + public Boolean cancellable() { + return this.cancellable; + } + /** * Sets the default namespaces map the engine will use. *

      @@ -286,7 +343,7 @@ public Boolean debug() { *

      *

      Note that the JexlContext is also used to try to solve top-level namespaces. This allows ObjectContext * derived instances to call methods on the wrapped object.

      - * + * * @param ns the map of namespaces * @return this builder */ @@ -306,7 +363,7 @@ public Map namespaces() { * Sets the expression cache size the engine will use. *

      The cache will contain at most size expressions of at most cacheThreshold length. * Note that all JEXL caches are held through SoftReferences and may be garbage-collected.

      - * + * * @param size if not strictly positive, no cache is used. * @return this builder */ @@ -328,7 +385,7 @@ public int cache() { * bypass the cache.

      *

      It is expected that a "long" script will be parsed once and its reference kept * around in user-space structures; the jexl expression cache has no added-value in this case.

      - * + * * @param length if not strictly positive, the value is silently replaced by the default value (64). * @return this builder */ diff --git a/src/main/java/org/apache/commons/jexl3/JexlContext.java b/src/main/java/org/apache/commons/jexl3/JexlContext.java index d47fe2fd3..0ab91944e 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlContext.java +++ b/src/main/java/org/apache/commons/jexl3/JexlContext.java @@ -17,28 +17,30 @@ package org.apache.commons.jexl3; +import java.util.concurrent.Callable; + /** * Manages variables which can be referenced in a JEXL expression. - * + * *

      JEXL variable names in their simplest form are 'java-like' identifiers. * JEXL also considers 'ant' inspired variables expressions as valid. * For instance, the expression 'x.y.z' is an 'antish' variable and will be resolved as a whole by the context, * i.e. using the key "x.y.z". This proves to be useful to solve "fully qualified class names".

      - * + * *

      The interpreter variable resolution algorithm will try the different sequences of identifiers till it finds * one that exists in the context; if "x" is an object known in the context (JexlContext.has("x") returns true), * "x.y" will not be looked up in the context but will most likely refer to "x.getY()".

      - * + * *

      Note that JEXL may use '$jexl' and '$ujexl' variables for internal purpose; setting or getting those * variables may lead to unexpected results unless specified otherwise.

      - * + * * @since 1.0 */ public interface JexlContext { /** * Gets the value of a variable. - * + * * @param name the variable's name * @return the value */ @@ -46,7 +48,7 @@ public interface JexlContext { /** * Sets the value of a variable. - * + * * @param name the variable's name * @param value the variable's value */ @@ -54,27 +56,27 @@ public interface JexlContext { /** * Checks whether a variable is defined in this context. - * + * *

      A variable may be defined with a null value; this method checks whether the * value is null or if the variable is undefined.

      - * + * * @param name the variable's name * @return true if it exists, false otherwise */ boolean has(String name); /** - * This interface declares how to resolve a namespace from its name; it is used by the interpreter during - * evalutation. - * + * A marker interface of the JexlContext that declares how to resolve a namespace from its name; + * it is used by the interpreter during evaluation. + * *

      In JEXL, a namespace is an object that serves the purpose of encapsulating functions; for instance, * the "math" namespace would be the proper object to expose functions like "log(...)", "sinus(...)", etc.

      - * + * * In expressions like "ns:function(...)", the resolver is called with resolveNamespace("ns"). - * + * *

      JEXL itself reserves 'jexl' and 'ujexl' as namespaces for internal purpose; resolving those may lead to * unexpected results.

      - * + * * @since 3.0 */ interface NamespaceResolver { @@ -88,8 +90,9 @@ interface NamespaceResolver { } /** - * Namespace type that allows creating an instance to delegate namespace methods calls to. - * + * A marker interface of the JexlContext, NamespaceFunctor allows creating an instance + * to delegate namespace methods calls to. + * *

      The functor is created once during the lifetime of a script evaluation.

      */ interface NamespaceFunctor { @@ -102,14 +105,14 @@ interface NamespaceFunctor { } /** - * A marker interface that indicates the interpreter to put this context in the JexlEngine thread local context - * instance during evaluation. + * A marker interface of the JexlContext that indicates the interpreter to put this context + * in the JexlEngine thread local context instance during evaluation. * This allows user functions or methods to access the context during a call. * Note that the usual caveats wrt using thread local apply (caching/leaking references, etc.); in particular, * keeping a reference to such a context is to be considered with great care and caution. * It should also be noted that sharing such a context between threads should implicate synchronizing variable * accessing the implementation class. - * + * * @see JexlEngine#setThreadContext(JexlContext.ThreadLocal) * @see JexlEngine#getThreadContext() */ @@ -117,4 +120,28 @@ interface ThreadLocal extends JexlContext { // no specific method } + /** + * A marker interface of the JexlContext that allows to process annotations. + * It is used by the interpreter during evaluation to execute annotation evaluations. + *

      If the JexlContext is not an instance of an AnnotationProcessor, encountering an annotation will generate + * an error or a warning depending on the engine strictness. + * @since 3.1 + */ + interface AnnotationProcessor { + /** + * Processes an annotation. + *

      All annotations are processed through this method; the statement 'call' is to be performed within + * the processAnnotation method. The implementation must perform the call explicitly. + *

      The arguments and the statement must not be referenced or cached for longer than the duration + * of the processAnnotation call. + * + * @param name the annotation name + * @param args the arguments of the annotation, evaluated as arguments of this call + * @param statement the statement that was annotated; the processor should invoke this statement 'call' method + * @return the result of statement.call() + * @throws Exception if annotation processing fails + */ + Object processAnnotation(String name, Object[] args, Callable statement) throws Exception; + } + } diff --git a/src/main/java/org/apache/commons/jexl3/JexlEngine.java b/src/main/java/org/apache/commons/jexl3/JexlEngine.java index d4d5c5804..bccf7e436 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlEngine.java +++ b/src/main/java/org/apache/commons/jexl3/JexlEngine.java @@ -37,17 +37,25 @@ *
    • Error reporting
    • *
    • Logging
    • * - * + * *

      Note that methods that evaluate expressions may throw unchecked exceptions; * The {@link JexlException} are thrown in "non-silent" mode but since these are * RuntimeException, user-code should catch them wherever most appropriate.

      - * + * * @since 2.0 */ public abstract class JexlEngine { - /** A marker for invocation failures in tryInvoke. */ - public static final Object TRY_FAILED = new Object() { + /** A marker singleton for invocation failures in tryInvoke. */ + public static final Object TRY_FAILED = new FailObject(); + + /** The failure marker class. */ + private static final class FailObject { + /** + * Default ctor. + */ + private FailObject() {} + @Override public String toString() { return "tryExecute failed"; @@ -58,27 +66,38 @@ public String toString() { * The thread local context. */ protected static final java.lang.ThreadLocal CONTEXT = - new java.lang.ThreadLocal() { - @Override - protected JexlContext.ThreadLocal initialValue() { - return null; - } - }; + new java.lang.ThreadLocal(); /** * Accesses the current thread local context. - * + * * @return the context or null */ public static JexlContext.ThreadLocal getThreadContext() { return CONTEXT.get(); } + /** + * The thread local engine. + */ + protected static final java.lang.ThreadLocal ENGINE = + new java.lang.ThreadLocal(); + + /** + * Accesses the current thread local engine. + *

      Advanced: you should only use this to retrieve the engine within a method/ctor called through the evaluation + * of a script/expression.

      + * @return the engine or null + */ + public static JexlEngine getThreadEngine() { + return ENGINE.get(); + } + /** * Sets the current thread local context. *

      This should only be used carefully, for instance when re-evaluating a "stored" script that requires a * given Namespace resolver. Remember to synchronize access if context is shared between threads. - * + * * @param tls the thread local context to set */ public static void setThreadContext(JexlContext.ThreadLocal tls) { @@ -93,13 +112,13 @@ public interface Options { /** * The charset used for parsing. - * + * * @return the charset */ Charset getCharset(); /** * Sets whether the engine will throw a {@link JexlException} when an error is encountered during evaluation. - * + * * @return true if silent, false otherwise */ Boolean isSilent(); @@ -107,37 +126,57 @@ public interface Options { /** * Checks whether the engine considers unknown variables, methods, functions and constructors as errors or * evaluates them as null. - * + * * @return true if strict, false otherwise */ Boolean isStrict(); /** * Checks whether the arithmetic triggers errors during evaluation when null is used as an operand. - * + * * @return true if strict, false otherwise */ Boolean isStrictArithmetic(); + /** + * Whether evaluation will throw JexlException.Cancel (true) or return null (false) when interrupted. + * @return true when cancellable, false otherwise + * @since 3.1 + */ + Boolean isCancellable(); + /** * The MathContext instance used for +,-,/,*,% operations on big decimals. - * + * * @return the math context */ MathContext getArithmeticMathContext(); /** * The BigDecimal scale used for comparison and coercion operations. - * + * * @return the scale */ int getArithmeticMathScale(); } + /** Default features. */ + public static final JexlFeatures DEFAULT_FEATURES = new JexlFeatures(); + /** - * An empty/static/non-mutable JexlContext used instead of null context. + * An empty/static/non-mutable JexlContext singleton used instead of null context. */ - public static final JexlContext EMPTY_CONTEXT = new JexlContext() { + public static final JexlContext EMPTY_CONTEXT = new EmptyContext(); + + /** + * The empty context class, public for instrospection. + */ + public static final class EmptyContext implements JexlContext { + /** + * Default ctor. + */ + private EmptyContext() {} + @Override public Object get(String name) { return null; @@ -155,9 +194,19 @@ public void set(String name, Object value) { }; /** - * An empty/static/non-mutable JexlNamesapce used instead of null namespace. + * An empty/static/non-mutable JexlNamespace singleton used instead of null namespace. */ - public static final JexlContext.NamespaceResolver EMPTY_NS = new JexlContext.NamespaceResolver() { + public static final JexlContext.NamespaceResolver EMPTY_NS = new EmptyNamespaceResolver(); + + /** + * The empty/static/non-mutable JexlNamespace class, public for instrospection. + */ + public static final class EmptyNamespaceResolver implements JexlContext.NamespaceResolver { + /** + * Default ctor. + */ + private EmptyNamespaceResolver() {} + @Override public Object resolveNamespace(String name) { return null; @@ -169,58 +218,66 @@ public Object resolveNamespace(String name) { /** * Gets the charset used for parsing. - * + * * @return the charset */ public abstract Charset getCharset(); /** * Gets this engine underlying {@link JexlUberspect}. - * + * * @return the uberspect */ public abstract JexlUberspect getUberspect(); /** * Gets this engine underlying {@link JexlArithmetic}. - * + * * @return the arithmetic */ public abstract JexlArithmetic getArithmetic(); /** * Checks whether this engine is in debug mode. - * + * * @return true if debug is on, false otherwise */ public abstract boolean isDebug(); /** * Checks whether this engine throws JexlException during evaluation. - * + * * @return true if silent, false (default) otherwise */ public abstract boolean isSilent(); /** - * Checks whether the engine considers unknown variables, methods, functions and constructors as errors. - * + * Checks whether this engine considers unknown variables, methods, functions and constructors as errors. + * * @return true if strict, false otherwise */ public abstract boolean isStrict(); + /** + * Checks whether this engine will throw JexlException.Cancel (true) or return null (false) when interrupted + * during an execution. + * + * @return true if cancellable, false otherwise + */ + public abstract boolean isCancellable(); + /** * Sets the class loader used to discover classes in 'new' expressions. *

      This method is not thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.

      - * + * * @param loader the class loader to use */ public abstract void setClassLoader(ClassLoader loader); /** * Creates a new {@link JxltEngine} instance using this engine. - * + * * @return a JEXL Template engine */ public JxltEngine createJxltEngine() { @@ -229,7 +286,7 @@ public JxltEngine createJxltEngine() { /** * Creates a new {@link JxltEngine} instance using this engine. - * + * * @param noScript whether the JxltEngine only allows Jexl expressions or scripts * @return a JEXL Template engine */ @@ -239,7 +296,7 @@ public JxltEngine createJxltEngine(boolean noScript) { /** * Creates a new instance of {@link JxltEngine} using this engine. - * + * * @param noScript whether the JxltEngine only allows JEXL expressions or scripts * @param cacheSize the number of expressions in this cache, default is 256 * @param immediate the immediate template expression character, default is '$' @@ -264,10 +321,22 @@ public JxltEngine createJxltEngine(boolean noScript) { */ public abstract JexlExpression createExpression(JexlInfo info, String expression); + /** + * Creates a JexlExpression from a String containing valid JEXL syntax. + * This method parses the expression which must contain either a reference or an expression. + * + * @param expression A String containing valid JEXL syntax + * @return An {@link JexlExpression} which can be evaluated using a {@link JexlContext} + * @throws JexlException if there is a problem parsing the script + */ + public final JexlExpression createExpression(String expression) { + return createExpression(null, expression); + } /** * Creates a JexlScript from a String containing valid JEXL syntax. * This method parses the script and validates the syntax. * + * @param features A set of features that will be enforced during parsing * @param info An info structure to carry debugging information if needed * @param source A string containing valid JEXL syntax * @param names The script parameter names used during parsing; a corresponding array of arguments containing @@ -275,18 +344,21 @@ public JxltEngine createJxltEngine(boolean noScript) { * @return A {@link JexlScript} which can be executed using a {@link JexlContext} * @throws JexlException if there is a problem parsing the script */ - public abstract JexlScript createScript(JexlInfo info, String source, String[] names); + public abstract JexlScript createScript(JexlFeatures features, JexlInfo info, String source, String[] names); /** - * Creates a JexlExpression from a String containing valid JEXL syntax. - * This method parses the expression which must contain either a reference or an expression. + * Creates a JexlScript from a String containing valid JEXL syntax. + * This method parses the script and validates the syntax. * - * @param expression A String containing valid JEXL syntax - * @return An {@link JexlExpression} which can be evaluated using a {@link JexlContext} + * @param info An info structure to carry debugging information if needed + * @param source A string containing valid JEXL syntax + * @param names The script parameter names used during parsing; a corresponding array of arguments containing + * values should be used during evaluation + * @return A {@link JexlScript} which can be executed using a {@link JexlContext} * @throws JexlException if there is a problem parsing the script */ - public final JexlExpression createExpression(String expression) { - return createExpression(null, expression); + public final JexlScript createScript(JexlInfo info, String source, String[] names) { + return createScript(null, null, source, names); } /** @@ -298,7 +370,7 @@ public final JexlExpression createExpression(String expression) { * @throws JexlException if there is a problem parsing the script. */ public final JexlScript createScript(String scriptText) { - return createScript(null, scriptText, null); + return createScript(null, null, scriptText, null); } /** @@ -312,7 +384,7 @@ public final JexlScript createScript(String scriptText) { * @throws JexlException if there is a problem parsing the script */ public final JexlScript createScript(String scriptText, String... names) { - return createScript(null, scriptText, names); + return createScript(null, null, scriptText, names); } /** @@ -324,7 +396,7 @@ public final JexlScript createScript(String scriptText, String... names) { * @throws JexlException if there is a problem reading or parsing the script. */ public final JexlScript createScript(File scriptFile) { - return createScript(null, readSource(scriptFile), null); + return createScript(null, null, readSource(scriptFile), null); } /** @@ -338,7 +410,7 @@ public final JexlScript createScript(File scriptFile) { * @throws JexlException if there is a problem reading or parsing the script. */ public final JexlScript createScript(File scriptFile, String... names) { - return createScript(null, readSource(scriptFile), names); + return createScript(null, null, readSource(scriptFile), names); } /** @@ -353,7 +425,7 @@ public final JexlScript createScript(File scriptFile, String... names) { * @throws JexlException if there is a problem reading or parsing the script. */ public final JexlScript createScript(JexlInfo info, File scriptFile, String[] names) { - return createScript(info, readSource(scriptFile), names); + return createScript(null, info, readSource(scriptFile), names); } /** @@ -379,7 +451,7 @@ public final JexlScript createScript(URL scriptUrl) { * @throws JexlException if there is a problem reading or parsing the script. */ public final JexlScript createScript(URL scriptUrl, String[] names) { - return createScript(null, readSource(scriptUrl), names); + return createScript(null, null, readSource(scriptUrl), names); } /** @@ -394,7 +466,7 @@ public final JexlScript createScript(URL scriptUrl, String[] names) { * @throws JexlException if there is a problem reading or parsing the script. */ public final JexlScript createScript(JexlInfo info, URL scriptUrl, String[] names) { - return createScript(info, readSource(scriptUrl), names); + return createScript(null, info, readSource(scriptUrl), names); } /** @@ -459,7 +531,7 @@ public final JexlScript createScript(JexlInfo info, URL scriptUrl, String[] name /** * Invokes an object's method by name and arguments. - * + * * @param obj the method's invoker object * @param meth the method's name * @param args the method's arguments @@ -470,7 +542,7 @@ public final JexlScript createScript(JexlInfo info, URL scriptUrl, String[] name /** * Creates a new instance of an object using the most appropriate constructor based on the arguments. - * + * * @param the type of object * @param clazz the class to instantiate * @param args the constructor arguments @@ -480,7 +552,7 @@ public final JexlScript createScript(JexlInfo info, URL scriptUrl, String[] name /** * Creates a new instance of an object using the most appropriate constructor based on the arguments. - * + * * @param clazz the name of the class to instantiate resolved through this engine's class loader * @param args the constructor arguments * @return the created object instance or null on failure when silent @@ -489,7 +561,7 @@ public final JexlScript createScript(JexlInfo info, URL scriptUrl, String[] name /** * Creates a JexlInfo instance. - * + * * @param fn url/file/template/script user given name * @param l line number * @param c column number @@ -503,14 +575,12 @@ public JexlInfo createInfo(String fn, int l, int c) { * Create an information structure for dynamic set/get/invoke/new. *

      This gathers the class, method and line number of the first calling method * outside of o.a.c.jexl3.

      - * + * * @return a JexlInfo instance */ public JexlInfo createInfo() { JexlInfo info = null; - Throwable xinfo = new Throwable(); - xinfo.fillInStackTrace(); - StackTraceElement[] stack = xinfo.getStackTrace(); + StackTraceElement[] stack = new Throwable().getStackTrace(); StackTraceElement se = null; String name = getClass().getName(); for (int s = 1; s < stack.length; ++s) { @@ -534,7 +604,7 @@ public JexlInfo createInfo() { /** * Creates a string from a reader. - * + * * @param reader to be read. * @return the contents of the reader as a String. * @throws IOException on any error reading the reader. @@ -550,7 +620,7 @@ protected static String toString(BufferedReader reader) throws IOException { /** * Reads a JEXL source from a File. - * + * * @param file the script file * @return the source */ @@ -577,7 +647,7 @@ protected String readSource(final File file) { /** * Reads a JEXL source from an URL. - * + * * @param url the script url * @return the source */ diff --git a/src/main/java/org/apache/commons/jexl3/JexlException.java b/src/main/java/org/apache/commons/jexl3/JexlException.java index 9b0706106..d23ecc090 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlException.java +++ b/src/main/java/org/apache/commons/jexl3/JexlException.java @@ -31,7 +31,7 @@ /** * Wraps any error that might occur during interpretation of a script or expression. - * + * * @since 2.0 */ public class JexlException extends RuntimeException { @@ -47,7 +47,7 @@ public class JexlException extends RuntimeException { /** * Creates a new JexlException. - * + * * @param node the node causing the error * @param msg the error message */ @@ -57,7 +57,7 @@ public JexlException(JexlNode node, String msg) { /** * Creates a new JexlException. - * + * * @param node the node causing the error * @param msg the error message * @param cause the exception causing the error @@ -75,7 +75,7 @@ public JexlException(JexlNode node, String msg, Throwable cause) { /** * Creates a new JexlException. - * + * * @param jinfo the debugging information associated * @param msg the error message * @param cause the exception causing the error @@ -88,7 +88,7 @@ public JexlException(JexlInfo jinfo, String msg, Throwable cause) { /** * Gets the specific information for this exception. - * + * * @return the information */ public JexlInfo getInfo() { @@ -97,7 +97,7 @@ public JexlInfo getInfo() { /** * Creates a string builder pre-filled with common error information (if possible). - * + * * @param node the node * @return a string builder */ @@ -115,7 +115,7 @@ private static StringBuilder errorAt(JexlNode node) { /** * Gets the most specific information attached to a node. - * + * * @param node the node * @param info the information * @return the information or null @@ -137,7 +137,7 @@ public JexlInfo.Detail getDetail() { /** * Cleans a JexlException from any org.apache.commons.jexl3.internal stack trace element. - * + * * @return this exception */ public JexlException clean() { @@ -146,7 +146,7 @@ public JexlException clean() { /** * Cleans a Throwable from any org.apache.commons.jexl3.internal stack trace element. - * + * * @param the throwable type * @param xthrow the thowable * @return the throwable @@ -168,7 +168,7 @@ private static X clean(X xthrow) { /** * Unwraps the cause of a throwable due to reflection. - * + * * @param xthrow the throwable * @return the cause */ @@ -184,7 +184,7 @@ private static Throwable unwrap(Throwable xthrow) { /** * Merge the node info and the cause info to obtain best possible location. - * + * * @param info the node * @param cause the cause * @return the info to use @@ -202,7 +202,7 @@ private static JexlInfo merge(JexlInfo info, JavaccError cause) { /** * Accesses detailed message. - * + * * @return the message */ protected String detailedMessage() { @@ -211,7 +211,7 @@ protected String detailedMessage() { /** * Formats an error message from the parser. - * + * * @param prefix the prefix to the message * @param expr the expression in error * @return the formatted message @@ -235,7 +235,7 @@ protected String parserError(String prefix, String expr) { /** * Thrown when tokenization fails. - * + * * @since 3.0 */ public static class Tokenization extends JexlException { @@ -263,13 +263,13 @@ protected String detailedMessage() { /** * Thrown when parsing fails. - * + * * @since 3.0 */ public static class Parsing extends JexlException { /** * Creates a new Parsing exception instance. - * + * * @param info the location information * @param cause the javacc cause */ @@ -279,7 +279,7 @@ public Parsing(JexlInfo info, ParseException cause) { /** * Creates a new Parsing exception instance. - * + * * @param info the location information * @param msg the message */ @@ -302,7 +302,7 @@ protected String detailedMessage() { /** * Thrown when parsing fails due to an ambiguous statement. - * + * * @since 3.0 */ public static class Ambiguous extends Parsing { @@ -323,13 +323,13 @@ protected String detailedMessage() { /** * Thrown when parsing fails due to an invalid assigment. - * + * * @since 3.0 */ public static class Assignment extends Parsing { /** * Creates a new Assignment statement exception instance. - * + * * @param info the location information * @param expr the source expression line */ @@ -339,13 +339,38 @@ public Assignment(JexlInfo info, String expr) { @Override protected String detailedMessage() { - return parserError("assignement", getDetail()); + return parserError("assignment", getDetail()); + } + } + + /** + * Thrown when parsing fails due to a disallowed feature. + * + * @since 3.2 + */ + public static class Feature extends Parsing { + /** The feature code. */ + private final int code; + /** + * Creates a new Ambiguous statement exception instance. + * @param info the location information + * @param feature the feature code + * @param expr the source expression line + */ + public Feature(JexlInfo info, int feature, String expr) { + super(info, expr); + this.code = feature; + } + + @Override + protected String detailedMessage() { + return parserError(JexlFeatures.stringify(code), getDetail()); } } /** * Thrown when a variable is unknown. - * + * * @since 3.0 */ public static class Variable extends JexlException { @@ -355,7 +380,7 @@ public static class Variable extends JexlException { private final boolean undefined; /** * Creates a new Variable exception instance. - * + * * @param node the offending ASTnode * @param var the unknown variable * @param undef whether the variable is undefined or evaluated as null @@ -367,7 +392,7 @@ public Variable(JexlNode node, String var, boolean undef) { /** * Whether the variable causing an error is undefined or evaluated as null. - * + * * @return true if undefined, false otherwise */ public boolean isUndefined() { @@ -389,7 +414,7 @@ protected String detailedMessage() { /** * Generates a message for a variable error. - * + * * @param node the node where the error occurred * @param variable the variable * @param undef whether the variable is null or undefined @@ -409,13 +434,13 @@ public static String variableError(JexlNode node, String variable, boolean undef /** * Thrown when a property is unknown. - * + * * @since 3.0 */ public static class Property extends JexlException { /** * Creates a new Property exception instance. - * + * * @param node the offending ASTnode * @param var the unknown variable */ @@ -425,7 +450,7 @@ public Property(JexlNode node, String var) { /** * Creates a new Property exception instance. - * + * * @param node the offending ASTnode * @param var the unknown variable * @param cause the exception causing the error @@ -449,7 +474,7 @@ protected String detailedMessage() { /** * Generates a message for an unsolvable property error. - * + * * @param node the node where the error occurred * @param var the variable * @return the error message @@ -464,13 +489,13 @@ public static String propertyError(JexlNode node, String var) { /** * Thrown when a method or ctor is unknown, ambiguous or inaccessible. - * + * * @since 3.0 */ public static class Method extends JexlException { /** * Creates a new Method exception instance. - * + * * @param node the offending ASTnode * @param name the method name */ @@ -480,7 +505,7 @@ public Method(JexlNode node, String name) { /** * Creates a new Method exception instance. - * + * * @param info the location information * @param name the unknown method * @param cause the exception causing the error @@ -504,7 +529,7 @@ protected String detailedMessage() { /** * Generates a message for a unsolvable method error. - * + * * @param node the node where the error occurred * @param method the method name * @return the error message @@ -519,13 +544,13 @@ public static String methodError(JexlNode node, String method) { /** * Thrown when an operator fails. - * + * * @since 3.0 */ public static class Operator extends JexlException { /** * Creates a new Operator exception instance. - * + * * @param node the location information * @param symbol the operator name * @param cause the exception causing the error @@ -549,7 +574,7 @@ protected String detailedMessage() { /** * Generates a message for an operator error. - * + * * @param node the node where the error occurred * @param symbol the operator name * @return the error message @@ -562,9 +587,55 @@ public static String operatorError(JexlNode node, String symbol) { return msg.toString(); } + /** + * Thrown when an annotation handler throws an exception. + * + * @since 3.1 + */ + public static class Annotation extends JexlException { + /** + * Creates a new Annotation exception instance. + * + * @param node the annotated statement node + * @param name the annotation name + * @param cause the exception causing the error + */ + public Annotation(JexlNode node, String name, Throwable cause) { + super(node, name, cause); + } + + /** + * @return the annotation name + */ + public String getAnnotation() { + return super.detailedMessage(); + } + + @Override + protected String detailedMessage() { + return "error processing annotation '" + getAnnotation() + "'"; + } + } + + /** + * Generates a message for an annotation error. + * + * @param node the node where the error occurred + * @param annotation the annotation name + * @return the error message + * @since 3.1 + */ + public static String annotationError(JexlNode node, String annotation) { + StringBuilder msg = errorAt(node); + msg.append("error processing annotation '"); + msg.append(annotation); + msg.append('\''); + return msg.toString(); + } + /** * Thrown to return a value. - * + * * @since 3.0 */ public static class Return extends JexlException { @@ -574,7 +645,7 @@ public static class Return extends JexlException { /** * Creates a new instance of Return. - * + * * @param node the return node * @param msg the message * @param value the returned value @@ -594,13 +665,13 @@ public Object getValue() { /** * Thrown to cancel a script execution. - * + * * @since 3.0 */ public static class Cancel extends JexlException { /** * Creates a new instance of Cancel. - * + * * @param node the node where the interruption was detected */ public Cancel(JexlNode node) { @@ -610,13 +681,13 @@ public Cancel(JexlNode node) { /** * Thrown to break a loop. - * + * * @since 3.0 */ public static class Break extends JexlException { /** * Creates a new instance of Break. - * + * * @param node the break */ public Break(JexlNode node) { @@ -626,13 +697,13 @@ public Break(JexlNode node) { /** * Thrown to continue a loop. - * + * * @since 3.0 */ public static class Continue extends JexlException { /** * Creates a new instance of Continue. - * + * * @param node the continue */ public Continue(JexlNode node) { @@ -643,12 +714,12 @@ public Continue(JexlNode node) { /** * Detailed info message about this error. * Format is "debug![begin,end]: string \n msg" where: - *
        + * * - debug is the debugging information if it exists (@link JexlEngine.setDebug) * - begin, end are character offsets in the string for the precise location of the error * - string is the string representation of the offending expression * - msg is the actual explanation message for this error - * + * * @return this error as a string */ @Override diff --git a/src/main/java/org/apache/commons/jexl3/JexlExpression.java b/src/main/java/org/apache/commons/jexl3/JexlExpression.java index 7bdc29b64..7f88c5b2a 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlExpression.java +++ b/src/main/java/org/apache/commons/jexl3/JexlExpression.java @@ -17,6 +17,8 @@ package org.apache.commons.jexl3; +import java.util.concurrent.Callable; + /** * Represents a single JEXL expression. *

        @@ -27,13 +29,13 @@ *

        * An expression is different than a script - it is simply a reference to * a single expression, not to multiple statements. - * This implies 'if','for','while','var' and blocks '{'... '}'are NOT allowed in expressions. + * This implies 'if','for','while','var' and blocks '{'... '}'are not allowed in expressions. *

        + *

        Do not create classes that implement this interface; delegate or compose instead.

        * * @since 1.0 */ public interface JexlExpression { - /** * Evaluates the expression with the variables contained in the * supplied {@link JexlContext}. @@ -46,15 +48,27 @@ public interface JexlExpression { /** * Returns the source text of this expression. - * + * * @return the source text */ String getSourceText(); /** - * Recreates the source text of this expression from the internal synactic tree. - * + * Recreates the source text of this expression from the internal syntactic tree. + * * @return the source text */ String getParsedText(); + + /** + * Creates a Callable from this expression. + * + *

        This allows to submit it to an executor pool and provides support for asynchronous calls.

        + *

        The interpreter will handle interruption/cancellation gracefully if needed.

        + * + * @param context the context + * @return the callable + * @since 3.1 + */ + Callable callable(JexlContext context); } diff --git a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java new file mode 100644 index 000000000..8e1a6cf3b --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java @@ -0,0 +1,474 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +/** + * A set of language feature options. + * These control syntactical constructs that will throw JexlException.Feature exceptions (a + * subclass of JexlException.Parsing) when disabled. + *
          + *
        • Registers: register syntax (#number), used internally for {g,s}etProperty + *
        • Reserved Names: a set of reserved variable names that can not be used as local variable (or parameter) names + *
        • Global Side Effect : assigning/modifying values on global variables (=, += , -=, ...) + *
        • Side Effect : assigning/modifying values on any variables or left-value + *
        • Constant Array Reference: ensures array references only use constants;they should be statically solvable. + *
        • New Instance: creating an instance using new(...) + *
        • Loops: loop constructs (while(true), for(...)) + *
        • Lambda: function definitions (()->{...}, function(...) ). + *
        • Method calls: calling methods (obj.method(...) or obj['method'](...)); when disabled, leaves function calls + * - including namespace prefixes - available + *
        • Structured literals: arrays, lists, maps, sets, ranges + *
        • Pragmas: #pragma x y + *
        • Annotation: @annotation statement; + *
        + * @since 3.2 + */ +public final class JexlFeatures { + /** The feature flags. */ + private long flags; + /** The set of reserved names, aka global variables that can not be masked by local variables or parameters. */ + private Set reservedNames; + /** Te feature names (for toString()). */ + private static final String[] F_NAMES = { + "register", "reserved variable", "local variable", "assign/modify", + "global assign/modify", "array reference", "create instance", "loop", "function", + "method call", "set/map/array literal", "pragma", "annotation", "script" + }; + /** Registers feature ordinal. */ + private static final int REGISTER = 0; + /** Reserved name feature ordinal. */ + public static final int RESERVED = 1; + /** Locals feature ordinal. */ + public static final int LOCAL_VAR = 2; + /** Side-effects feature ordinal. */ + public static final int SIDE_EFFECT = 3; + /** Global side-effects feature ordinal. */ + public static final int SIDE_EFFECT_GLOBAL = 4; + /** Array get is allowed on expr. */ + public static final int ARRAY_REF_EXPR = 5; + /** New-instance feature ordinal. */ + public static final int NEW_INSTANCE = 6; + /** Loops feature ordinal. */ + public static final int LOOP = 7; + /** Lambda feature ordinal. */ + public static final int LAMBDA = 8; + /** Lambda feature ordinal. */ + public static final int METHOD_CALL = 9; + /** Structured literal feature ordinal. */ + public static final int STRUCTURED_LITERAL = 10; + /** Pragma feature ordinal. */ + public static final int PRAGMA = 11; + /** Annotation feature ordinal. */ + public static final int ANNOTATION = 12; + /** Script feature ordinal. */ + public static final int SCRIPT = 13; + + /** + * Creates an all-features-enabled instance. + */ + public JexlFeatures() { + flags = (1L << LOCAL_VAR) + | (1L << SIDE_EFFECT) + | (1L << SIDE_EFFECT_GLOBAL) + | (1L << ARRAY_REF_EXPR) + | (1L << NEW_INSTANCE) + | (1L << LOOP) + | (1L << LAMBDA) + | (1L << METHOD_CALL) + | (1L << STRUCTURED_LITERAL) + | (1L << PRAGMA) + | (1L << ANNOTATION) + | (1L << SCRIPT); + reservedNames = Collections.emptySet(); + } + + /** + * Copy constructor. + * @param features the feature to copy from + */ + public JexlFeatures(JexlFeatures features) { + this.flags = features.flags; + this.reservedNames = features.reservedNames; + } + + @Override + public int hashCode() { //CSOFF: MagicNumber + int hash = 3; + hash = 53 * hash + (int) (this.flags ^ (this.flags >>> 32)); + hash = 53 * hash + (this.reservedNames != null ? this.reservedNames.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final JexlFeatures other = (JexlFeatures) obj; + if (this.flags != other.flags) { + return false; + } + if (this.reservedNames != other.reservedNames + && (this.reservedNames == null || !this.reservedNames.equals(other.reservedNames))) { + return false; + } + return true; + } + + /** + * The text corresponding to a feature code. + * @param feature the feature number + * @return the feature name + */ + public static String stringify(int feature) { + return feature >= 0 && feature < F_NAMES.length ? F_NAMES[feature] : "unsupported feature"; + } + + /** + * Sets a collection of reserved names precluding those to be used as local variables or parameter names. + * @param names the names to reserve + * @return this features instance + */ + public JexlFeatures reservedNames(Collection names) { + if (names == null || names.isEmpty()) { + reservedNames = Collections.emptySet(); + } else { + reservedNames = Collections.unmodifiableSet(new TreeSet(names)); + } + setFeature(RESERVED, !reservedNames.isEmpty()); + return this; + } + + /** + * @return the (unmodifiable) set of reserved names. + */ + public Set getReservedNames() { + return reservedNames; + } + + /** + * Checks whether a name is reserved. + * @param name the name to check + * @return true if reserved, false otherwise + */ + public boolean isReservedName(String name) { + return name != null && reservedNames.contains(name); + } + + /** + * Sets a feature flag. + * @param feature the feature ordinal + * @param flag turn-on, turn off + */ + private void setFeature(int feature, boolean flag) { + if (flag) { + flags |= (1 << feature); + } else { + flags &= ~(1L << feature); + } + } + + /** + * Gets a feature flag value. + * @param feature feature ordinal + * @return true if on, false if off + */ + private boolean getFeature(int feature) { + return (flags & (1L << feature)) != 0L; + } + + /** + * Sets whether register are enabled. + *

        + * This is mostly used internally during execution of JexlEngine.{g,s}etProperty. + *

        + * When disabled, parsing a script/expression using the register syntax will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures register(boolean flag) { + setFeature(REGISTER, flag); + return this; + } + + /** + * @return true if register syntax is enabled + */ + public boolean supportsRegister() { + return getFeature(REGISTER); + } + + /** + * Sets whether local variables are enabled. + *

        + * When disabled, parsing a script/expression using a local variable or parameter syntax + * will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures localVar(boolean flag) { + setFeature(LOCAL_VAR, flag); + return this; + } + + /** + * @return true if local variables syntax is enabled + */ + public boolean supportsLocalVar() { + return getFeature(LOCAL_VAR); + } + + /** + * Sets whether side effect expressions on global variables (aka non local) are enabled. + *

        + * When disabled, parsing a script/expression using syntactical constructs modifying variables + * including all potentially ant-ish variables will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures sideEffectGlobal(boolean flag) { + setFeature(SIDE_EFFECT_GLOBAL, flag); + return this; + } + + /** + * @return true if global variables can be assigned + */ + public boolean supportsSideEffectGlobal() { + return getFeature(SIDE_EFFECT_GLOBAL); + } + + /** + * Sets whether side effect expressions are enabled. + *

        + * When disabled, parsing a script/expression using syntactical constructs modifying variables + * or members will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures sideEffect(boolean flag) { + setFeature(SIDE_EFFECT, flag); + return this; + } + + /** + * @return true if side effects are enabled, false otherwise + */ + public boolean supportsSideEffect() { + return getFeature(SIDE_EFFECT); + } + + /** + * Sets whether array references expressions are enabled. + *

        + * When disabled, parsing a script/expression using 'obj[ ref ]' where ref is not a string or integer literal + * will throw a parsing exception; + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures arrayReferenceExpr(boolean flag) { + setFeature(ARRAY_REF_EXPR, flag); + return this; + } + + /** + * @return true if array references can contain method call expressions, false otherwise + */ + public boolean supportsArrayReferenceExpr() { + return getFeature(ARRAY_REF_EXPR); + } + + /** + * Sets whether method calls expressions are enabled. + *

        + * When disabled, parsing a script/expression using 'obj.method()' + * will throw a parsing exception; + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures methodCall(boolean flag) { + setFeature(METHOD_CALL, flag); + return this; + } + + /** + * @return true if array references can contain expressions, false otherwise + */ + public boolean supportsMethodCall() { + return getFeature(METHOD_CALL); + } + + /** + * Sets whether array/map/set literal expressions are enabled. + *

        + * When disabled, parsing a script/expression creating one of these literals + * will throw a parsing exception; + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures structuredLiteral(boolean flag) { + setFeature(STRUCTURED_LITERAL, flag); + return this; + } + + /** + * @return true if array/map/set literal expressions are supported, false otherwise + */ + public boolean supportsStructuredLiteral() { + return getFeature(STRUCTURED_LITERAL); + } + + /** + * Sets whether creating new instances is enabled. + *

        + * When disabled, parsing a script/expression using 'new(...)' will throw a parsing exception; + * using a class as functor will fail at runtime. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures newInstance(boolean flag) { + setFeature(NEW_INSTANCE, flag); + return this; + } + + /** + * @return true if creating new instances is enabled, false otherwise + */ + public boolean supportsNewInstance() { + return getFeature(NEW_INSTANCE); + } + + /** + * Sets whether looping constructs are enabled. + *

        + * When disabled, parsing a script/expression using syntactic looping constructs (for,while) + * will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures loops(boolean flag) { + setFeature(LOOP, flag); + return this; + } + + /** + * @return true if loops are enabled, false otherwise + */ + public boolean supportsLoops() { + return getFeature(LOOP); + } + + /** + * Sets whether lambda/function constructs are enabled. + *

        + * When disabled, parsing a script/expression using syntactic lambda constructs (->,function) + * will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures lambda(boolean flag) { + setFeature(LAMBDA, flag); + return this; + } + + /** + * @return true if lambda are enabled, false otherwise + */ + public boolean supportsLambda() { + return getFeature(LAMBDA); + } + + /** + * Sets whether pragma constructs are enabled. + *

        + * When disabled, parsing a script/expression using syntactic pragma constructs (#pragma) + * will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures pragma(boolean flag) { + setFeature(PRAGMA, flag); + return this; + } + + /** + * @return true if pragma are enabled, false otherwise + */ + public boolean supportsPragma() { + return getFeature(PRAGMA); + } + + /** + * Sets whether annotation constructs are enabled. + *

        + * When disabled, parsing a script/expression using syntactic annotation constructs (@annotation) + * will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures annotation(boolean flag) { + setFeature(ANNOTATION, flag); + return this; + } + + /** + * @return true if annotation are enabled, false otherwise + */ + public boolean supportsAnnotation() { + return getFeature(ANNOTATION); + } + + /** + * Sets whether scripts constructs are enabled. + *

        + * When disabled, parsing a script using syntactic script constructs (statements, ...) + * will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures script(boolean flag) { + setFeature(SCRIPT, flag); + return this; + } + + /** + * @return true if scripts are enabled, false otherwise + */ + public boolean supportsScript() { + return getFeature(SCRIPT); + } + + /** + * + * @return true if expressions (aka not scripts) are enabled, false otherwise + */ + public boolean supportsExpression() { + return !getFeature(SCRIPT); + } + +} diff --git a/src/main/java/org/apache/commons/jexl3/JexlOperator.java b/src/main/java/org/apache/commons/jexl3/JexlOperator.java index dd1a8ee14..1332d1b1a 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlOperator.java +++ b/src/main/java/org/apache/commons/jexl3/JexlOperator.java @@ -21,211 +21,240 @@ * The JEXL operators. * * These are the operators that are executed by JexlArithmetic methods. - * + * *

        Each of them associates a symbol to a method signature. * For instance, '+' is associated to 'T add(L x, R y)'.

        - * + * *

        The default JexlArithmetic implements generic versions of these methods using Object as arguments. * You can use your own derived JexlArithmetic that override and/or overload those operator methods; these methods * must be public, * must respect the return type when primitive * and may be overloaded multiple times with different signatures.

        - * + * * @since 3.0 */ public enum JexlOperator { /** - * Syntax: x + y + * Add operator. + *
        Syntax: x + y *
        Method: T add(L x, R y);. * @see JexlArithmetic#add */ ADD("+", "add", 2), /** - * Syntax: x - y + * Subtract operator. + *
        Syntax: x - y *
        Method: T subtract(L x, R y);. * @see JexlArithmetic#subtract */ SUBTRACT("-", "subtract", 2), /** - * Syntax: x * y + * Multiply operator. + *
        Syntax: x * y *
        Method: T multiply(L x, R y);. * @see JexlArithmetic#multiply */ MULTIPLY("*", "multiply", 2), /** - * Syntax: x / y + * Divide operator. + *
        Syntax: x / y *
        Method: T divide(L x, R y);. * @see JexlArithmetic#divide */ DIVIDE("/", "divide", 2), /** - * Syntax: x % y + * Modulo operator. + *
        Syntax: x % y *
        Method: T mod(L x, R y);. * @see JexlArithmetic#mod */ MOD("%", "mod", 2), /** - * Syntax: x & y + * Bitwise-and operator. + *
        Syntax: x & y *
        Method: T and(L x, R y);. * @see JexlArithmetic#and */ AND("&", "and", 2), /** - * Syntax: x | y + * Bitwise-or operator. + *
        Syntax: x | y *
        Method: T or(L x, R y);. * @see JexlArithmetic#or */ OR("|", "or", 2), /** - * Syntax: x ^ y + * Bitwise-xor operator. + *
        Syntax: x ^ y *
        Method: T xor(L x, R y);. * @see JexlArithmetic#xor */ XOR("^", "xor", 2), /** - * Syntax: x == y + * Equals operator. + *
        Syntax: x == y *
        Method: boolean equals(L x, R y);. * @see JexlArithmetic#equals */ EQ("==", "equals", 2), /** - * Syntax: x < y + * Less-than operator. + *
        Syntax: x < y *
        Method: boolean lessThan(L x, R y);. * @see JexlArithmetic#lessThan */ LT("<", "lessThan", 2), /** - * Syntax: x <= y + * Less-than-or-equal operator. + *
        Syntax: x <= y *
        Method: boolean lessThanOrEqual(L x, R y);. * @see JexlArithmetic#lessThanOrEqual */ LTE("<=", "lessThanOrEqual", 2), /** - * Syntax: x > y + * Greater-than operator. + *
        Syntax: x > y *
        Method: boolean greaterThan(L x, R y);. * @see JexlArithmetic#greaterThan */ GT(">", "greaterThan", 2), /** - * Syntax: x >= y + * Greater-than-or-equal operator. + *
        Syntax: x >= y *
        Method: boolean greaterThanOrEqual(L x, R y);. * @see JexlArithmetic#greaterThanOrEqual */ GTE(">=", "greaterThanOrEqual", 2), /** - * Syntax: x =~ y + * Contains operator. + *
        Syntax: x =~ y *
        Method: boolean contains(L x, R y);. * @see JexlArithmetic#contains */ CONTAINS("=~", "contains", 2), /** - * Syntax: x =^ y + * Starts-with operator. + *
        Syntax: x =^ y *
        Method: boolean startsWith(L x, R y);. * @see JexlArithmetic#startsWith */ STARTSWITH("=^", "startsWith", 2), /** - * Syntax: x =$ y + * Ends-with operator. + *
        Syntax: x =$ y *
        Method: boolean endsWith(L x, R y);. * @see JexlArithmetic#endsWith */ ENDSWITH("=$", "endsWith", 2), /** - * Syntax: !x + * Not operator. + *
        Syntax: !x *
        Method: T not(L x);. * @see JexlArithmetic#not */ NOT("!", "not", 1), /** - * Syntax: ~x + * Complement operator. + *
        Syntax: ~x *
        Method: T complement(L x);. * @see JexlArithmetic#complement */ COMPLEMENT("~", "complement", 1), /** - * Syntax: -x + * Negate operator. + *
        Syntax: -x *
        Method: T negate(L x);. * @see JexlArithmetic#negate */ NEGATE("-", "negate", 1), /** - * Syntax: empty x or empty(x) + * Empty operator. + *
        Syntax: empty x or empty(x) *
        Method: boolean isEmpty(L x);. * @see JexlArithmetic#isEmpty */ EMPTY("empty", "empty", 1), /** - * Syntax: size x or size(x) + * Size operator. + *
        Syntax: size x or size(x) *
        Method: int size(L x);. * @see JexlArithmetic#size */ SIZE("size", "size", 1), /** - * Syntax: x += y + * Self-add operator. + *
        Syntax: x += y *
        Method: T selfAdd(L x, R y);. */ SELF_ADD("+=", "selfAdd", ADD), /** - * Syntax: x -= y + * Self-subtract operator. + *
        Syntax: x -= y *
        Method: T selfSubtract(L x, R y);. */ SELF_SUBTRACT("-=", "selfSubtract", SUBTRACT), /** - * Syntax: x *= y + * Self-multiply operator. + *
        Syntax: x *= y *
        Method: T selfMultiply(L x, R y);. */ SELF_MULTIPLY("*=", "selfMultiply", MULTIPLY), /** - * Syntax: x /= y + * Self-divide operator. + *
        Syntax: x /= y *
        Method: T selfDivide(L x, R y);. */ SELF_DIVIDE("/=", "selfDivide", DIVIDE), /** - * Syntax: x %= y + * Self-modulo operator. + *
        Syntax: x %= y *
        Method: T selfMod(L x, R y);. */ SELF_MOD("%=", "selfMod", MOD), /** - * Syntax: x &= y + * Self-and operator. + *
        Syntax: x &= y *
        Method: T selfAnd(L x, R y);. */ SELF_AND("&=", "selfAnd", AND), /** - * Syntax: x |= y + * Self-or operator. + *
        Syntax: x |= y *
        Method: T selfOr(L x, R y);. */ SELF_OR("|=", "selfOr", OR), /** - * Syntax: x ^= y + * Self-xor operator. + *
        Syntax: x ^= y *
        Method: T selfXor(L x, R y);. */ SELF_XOR("^=", "selfXor", XOR), @@ -239,23 +268,40 @@ public enum JexlOperator { /** * Property get operator as in: x.y. + *
        Syntax: x.y + *
        Method: Object propertyGet(L x, R y);. */ PROPERTY_GET(".", "propertyGet", 2), /** * Property set operator as in: x.y = z. + *
        Syntax: x.y = z + *
        Method: void propertySet(L x, R y, V z);. */ PROPERTY_SET(".=", "propertySet", 3), /** * Array get operator as in: x[y]. + *
        Syntax: x.y + *
        Method: Object arrayGet(L x, R y);. */ ARRAY_GET("[]", "arrayGet", 2), /** * Array set operator as in: x[y] = z. + *
        Syntax: x[y] = z + *
        Method: void arraySet(L x, R y, V z);. + */ + ARRAY_SET("[]=", "arraySet", 3), + + /** + * Iterator generator as in for(var x : y). + * If the returned Iterator is AutoCloseable, close will be called after the last execution of the loop block. + *
        Syntax: for(var x : y){...} + *
        Method: Iterator<Object> forEach(R y);. + * @since 3.1 */ - ARRAY_SET("[]=", "arraySet", 3); + FOR_EACH("for(...)", "forEach", 1); /** * The operator symbol. @@ -279,7 +325,7 @@ public enum JexlOperator { /** * Creates a base operator. - * + * * @param o the operator name * @param m the method name associated to this operator in a JexlArithmetic * @param argc the number of parameters for the method @@ -293,7 +339,7 @@ public enum JexlOperator { /** * Creates a side-effect operator. - * + * * @param o the operator name * @param m the method name associated to this operator in a JexlArithmetic * @param b the base operator, ie + for += @@ -307,7 +353,7 @@ public enum JexlOperator { /** * Gets this operator symbol. - * + * * @return the symbol */ public final String getOperatorSymbol() { @@ -316,7 +362,7 @@ public final String getOperatorSymbol() { /** * Gets this operator method name in a JexlArithmetic. - * + * * @return the method name */ public final String getMethodName() { @@ -325,7 +371,7 @@ public final String getMethodName() { /** * Gets this operator number of parameters. - * + * * @return the method arity */ public int getArity() { @@ -334,7 +380,7 @@ public int getArity() { /** * Gets the base operator. - * + * * @return the base operator */ public final JexlOperator getBaseOperator() { diff --git a/src/main/java/org/apache/commons/jexl3/JexlScript.java b/src/main/java/org/apache/commons/jexl3/JexlScript.java index a3124c7f5..e7c041d3a 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlScript.java +++ b/src/main/java/org/apache/commons/jexl3/JexlScript.java @@ -32,6 +32,8 @@ *

        The statements can be blocks (curly braces containing code), * Control statements such as if and while * as well as expressions and assignment statements.

        + * + *

        Do not create classes that implement this interface; delegate or compose instead.

        * * @since 1.1 */ diff --git a/src/main/java/org/apache/commons/jexl3/JxltEngine.java b/src/main/java/org/apache/commons/jexl3/JxltEngine.java index 618ea5158..f32b551f7 100644 --- a/src/main/java/org/apache/commons/jexl3/JxltEngine.java +++ b/src/main/java/org/apache/commons/jexl3/JxltEngine.java @@ -21,20 +21,21 @@ import java.io.StringReader; import java.io.Writer; import java.util.List; +import java.util.Map; import java.util.Set; /** * A simple "JeXL Template" engine. - * + * *

        At the base is an evaluator similar to the Unified EL evaluator used in JSP/JSF based on JEXL. * At the top is a template engine inspired by Velocity that uses JEXL (instead of OGNL/VTL) as the scripting * language.

        - * + * *

        The evaluator is intended to be used in configuration modules, XML based frameworks or JSP taglibs * and facilitate the implementation of expression evaluation.

        - * + * *

        The template engine is intended to output any form of text; html, XML, CSV...

        - * + * * @since 3.0 */ public abstract class JxltEngine { @@ -49,7 +50,7 @@ public static class Exception extends JexlException { /** * Creates an Exception. - * + * * @param info the contextual information * @param msg the exception message * @param cause the exception cause @@ -67,57 +68,57 @@ public Exception(JexlInfo info, String msg, Throwable cause) { *
      • The "nested" syntax is of the form "...#{...${jexl-expr0}...}..."
      • *
      • The "composite" syntax is of the form "...${jexl-expr0}... #{jexl-expr1}..."
      • * - * + * *

        Deferred and immediate expression carry different intentions:

        - * + * *
          *
        • An immediate expression indicate that evaluation is intended to be performed close to * the definition/parsing point.
        • *
        • A deferred expression indicate that evaluation is intended to occur at a later stage.
        • *
        - * + * *

        For instance: "Hello ${name}, now is #{time}" is a composite "deferred" expression since one * of its subexpressions is deferred. Furthermore, this (composite) expression intent is * to perform two evaluations; one close to its definition and another one in a later * phase.

        - * + * *

        The API reflects this feature in 2 methods, prepare and evaluate. The prepare method * will evaluate the immediate subexpression and return an expression that contains only * the deferred subexpressions (and constants), a prepared expression. Such a prepared expression * is suitable for a later phase evaluation that may occur with a different JexlContext. * Note that it is valid to call evaluate without prepare in which case the same JexlContext * is used for the 2 evaluation phases.

        - * + * *

        In the most common use-case where deferred expressions are to be kept around as properties of objects, * one should createExpression and prepare an expression before storing it and evaluate it each time * the property storing it is accessed.

        - * + * *

        Note that nested expression use the JEXL syntax as in:

        - * + * *
        "#{${bar}+'.charAt(2)'}"
        - * + * *

        The most common mistake leading to an invalid expression being the following:

        - * + * *
        "#{${bar}charAt(2)}"
        - * + * *

        Also note that methods that createExpression evaluate expressions may throw unchecked exceptions; * The {@link JxltEngine.Exception} are thrown when the engine instance is in "non-silent" mode * but since these are RuntimeException, user-code should catch them where appropriate.

        - * + * * @since 2.0 */ public interface Expression { /** * Generates this expression's string representation. - * + * * @return the string representation */ String asString(); /** * Adds this expression's string representation to a StringBuilder. - * + * * @param strb the builder to fill * @return the builder argument */ @@ -125,9 +126,9 @@ public interface Expression { /** * Evaluates this expression. - * + * *

        If the underlying JEXL engine is silent, errors will be logged through its logger as warning.

        - * + * * @param context the variable context * @return the result of this expression evaluation or null if an error occurs and the {@link JexlEngine} is * running in silent mode @@ -142,7 +143,7 @@ public interface Expression { * If this expression was prepared, this allows to retrieve the * original expression that lead to it.

        *

        Other expressions return themselves.

        - * + * * @return the source expression */ Expression getSource(); @@ -151,7 +152,7 @@ public interface Expression { * Gets the list of variables accessed by this expression. *

        This method will visit all nodes of the sub-expressions and extract all variables whether they * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).

        - * + * * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string) * or the empty set if no variables are used */ @@ -159,32 +160,32 @@ public interface Expression { /** * Checks whether this expression is deferred. - * + * * @return true if deferred, false otherwise */ boolean isDeferred(); /** * Checks whether this expression is immediate. - * + * * @return true if immediate, false otherwise */ boolean isImmediate(); /** * Evaluates the immediate sub-expressions. - * + * *

        When the expression is dependant upon immediate and deferred sub-expressions, * evaluates the immediate sub-expressions with the context passed as parameter * and returns this expression deferred form.

        - * + * *

        In effect, this binds the result of the immediate sub-expressions evaluation in the * context, allowing to differ evaluation of the remaining (deferred) expression within another context. * This only has an effect to nested and composite expressions that contain differed and * immediate sub-expressions.

        - * + * *

        If the underlying JEXL engine is silent, errors will be logged through its logger as warning.*

        - * + * * @param context the context to use for immediate expression evaluations * @return an {@link Expression} or null if an error occurs and the {@link JexlEngine} is running * in silent mode @@ -195,7 +196,7 @@ public interface Expression { /** * Formats this expression, adding its source string representation in * comments if available: 'expression /*= source *\/'' . - * + * * @return the formatted expression string */ @Override @@ -205,9 +206,9 @@ public interface Expression { /** * Creates a a {@link Expression} from an expression string. * Uses and fills up the expression cache if any. - * + * *

        If the underlying JEXL engine is silent, errors will be logged through its logger as warnings.

        - * + * * @param expression the {@link Template} string expression * @return the {@link Expression}, null if silent and an error occurred * @throws Exception if an error occurs and the {@link JexlEngine} is not silent @@ -219,9 +220,9 @@ public Expression createExpression(String expression) { /** * Creates a a {@link Expression} from an expression string. * Uses and fills up the expression cache if any. - * + * *

        If the underlying JEXL engine is silent, errors will be logged through its logger as warnings.

        - * + * * @param info the {@link JexlInfo} source information * @param expression the {@link Template} string expression * @return the {@link Expression}, null if silent and an error occured @@ -249,9 +250,9 @@ public Expression createExpression(String expression) { * $$ } * $$ } * - * + * *

        Will evaluate as:

        - * + * *
              * The value 1 is under fourty-two
              * The value 3 is under fourty-two
        @@ -259,34 +260,34 @@ public Expression createExpression(String expression) {
              * Life, the universe, and everything
              * The value 169 is over fourty-two
              * 
        - * + * *

        During evaluation, the template context exposes its writer as '$jexl' which is safe to use in this case. * This allows writing directly through the writer without adding new-lines as in:

        - * + * *
              * $$ for(var cell : cells) { $jexl.print(cell); $jexl.print(';') }
              * 
        - * + * *

        A template is expanded as one JEXL script and a list of template expressions; each template expression is * being replaced in the script by a call to jexl:print(expr) (the expr is in fact the expr number in the template). * This integration uses a specialized JexlContext (TemplateContext) that serves as a namespace (for jexl:) * and stores the template expression array and the writer (java.io.Writer) that the 'jexl:print(...)' * delegates the output generation to.

        - * + * * @since 3.0 */ public interface Template { /** * Recreate the template source from its inner components. - * + * * @return the template source rewritten */ String asString(); /** * Evaluates this template. - * + * * @param context the context to use during evaluation * @param writer the writer to use for output */ @@ -294,7 +295,7 @@ public interface Template { /** * Evaluates this template. - * + * * @param context the context to use during evaluation * @param writer the writer to use for output * @param args the arguments @@ -303,7 +304,7 @@ public interface Template { /** * Prepares this template by expanding any contained deferred TemplateExpression. - * + * * @param context the context to prepare against * @return the prepared version of the template */ @@ -313,7 +314,7 @@ public interface Template { * Gets the list of variables accessed by this template. *

        This method will visit all nodes of the sub-expressions and extract all variables whether they * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).

        - * + * * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string) * or the empty set if no variables are used */ @@ -321,15 +322,23 @@ public interface Template { /** * Gets the list of parameters expected by this template. - * + * * @return the parameter names array */ String[] getParameters(); + + /** + * Gets this script pragmas. + * + * @return the (non null, may be empty) pragmas map + * @since 3.1 + */ + Map getPragmas(); } /** * Creates a new template. - * + * * @param info the jexl info (file, line, column) * @param prefix the directive prefix * @param source the source @@ -340,7 +349,7 @@ public interface Template { /** * Creates a new template. - * + * * @param info the source info * @param parms the parameter names * @param source the source @@ -352,7 +361,7 @@ public Template createTemplate(JexlInfo info, String source, String... parms) { /** * Creates a new template. - * + * * @param info the source info * @param source the source * @return the template @@ -363,7 +372,7 @@ public Template createTemplate(JexlInfo info, String source) { /** * Creates a new template. - * + * * @param prefix the directive prefix * @param source the source * @param parms the parameter names @@ -375,7 +384,7 @@ public Template createTemplate(String prefix, Reader source, String... parms) { /** * Creates a new template. - * + * * @param source the source * @param parms the parameter names * @return the template @@ -386,7 +395,7 @@ public Template createTemplate(String source, String... parms) { /** * Creates a new template. - * + * * @param source the source * @return the template */ @@ -396,7 +405,7 @@ public Template createTemplate(String source) { /** * Gets the {@link JexlEngine} underlying this template engine. - * + * * @return the JexlEngine */ public abstract JexlEngine getEngine(); diff --git a/src/main/java/org/apache/commons/jexl3/ObjectContext.java b/src/main/java/org/apache/commons/jexl3/ObjectContext.java index 2656dec84..05f89c908 100644 --- a/src/main/java/org/apache/commons/jexl3/ObjectContext.java +++ b/src/main/java/org/apache/commons/jexl3/ObjectContext.java @@ -14,12 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.commons.jexl3; +import org.apache.commons.jexl3.introspection.JexlPropertyGet; +import org.apache.commons.jexl3.introspection.JexlPropertySet; + /** * Wraps an Object as a JEXL context and NamespaceResolver. - * + * * @param the wrapped object type to use * @since 3.0 */ @@ -31,10 +33,24 @@ public class ObjectContext implements JexlContext, JexlContext.NamespaceResol /** The object serving as context provider. */ private final T object; + /** + * @return the Jexl engine + */ + protected JexlEngine getJexl() { + return jexl; + } + + /** + * @return the object exposed by this context + */ + protected T getObject() { + return object; + } + /** * Creates a new ObjectContext. - * - * @param engine the jexl engine to use to solve properties + * + * @param engine the jexl engine to use to solve properties * @param wrapped the object to wrap in this context */ public ObjectContext(JexlEngine engine, T wrapped) { @@ -44,17 +60,42 @@ public ObjectContext(JexlEngine engine, T wrapped) { @Override public Object get(String name) { - return jexl.getProperty(object, name); + JexlPropertyGet jget = jexl.getUberspect().getPropertyGet(object, name); + if (jget != null) { + try { + return jget.invoke(object); + } catch (Exception xany) { + if (jexl.isStrict()) { + throw new JexlException.Property(null, name, xany); + } + } + } + return null; } @Override public void set(String name, Object value) { - jexl.setProperty(object, name, value); + JexlPropertySet jset = jexl.getUberspect().getPropertySet(object, name, value); + if (jset != null) { + try { + jset.invoke(object, value); + } catch (Exception xany) { + // ignore + if (jexl.isStrict()) { + throw new JexlException.Property(null, name, xany); + } + } + } } @Override public boolean has(String name) { - return jexl.getUberspect().getPropertyGet(object, name) != null; + JexlPropertyGet jget = jexl.getUberspect().getPropertyGet(object, name); + try { + return jget != null && jget.invoke(object) != null; + } catch (Exception xany) { + return false; + } } @Override diff --git a/src/main/java/org/apache/commons/jexl3/internal/ArrayBuilder.java b/src/main/java/org/apache/commons/jexl3/internal/ArrayBuilder.java index d22ace90f..6075c9db6 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/ArrayBuilder.java +++ b/src/main/java/org/apache/commons/jexl3/internal/ArrayBuilder.java @@ -85,7 +85,7 @@ public void add(Object value) { // base common class on first non-null entry if (commonClass == null) { commonClass = eclass; - isNumber &= Number.class.isAssignableFrom(commonClass); + isNumber = isNumber && Number.class.isAssignableFrom(commonClass); } else if (!commonClass.equals(eclass)) { // if both are numbers... if (isNumber && Number.class.isAssignableFrom(eclass)) { diff --git a/src/main/java/org/apache/commons/jexl3/internal/Closure.java b/src/main/java/org/apache/commons/jexl3/internal/Closure.java index eae83fc4d..863adad56 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Closure.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Closure.java @@ -20,7 +20,6 @@ import org.apache.commons.jexl3.parser.ASTJexlLambda; import org.apache.commons.jexl3.parser.JexlNode; -import java.util.concurrent.Callable; /** * A Script closure. @@ -125,23 +124,16 @@ public Object execute(JexlContext context, Object... args) { } @Override - public Callable callable(JexlContext context, Object... args) { + public Callable callable(JexlContext context, Object... args) { Scope.Frame local = null; if (frame != null) { local = frame.assign(args); } - final Interpreter interpreter = jexl.createInterpreter(context, local); - return new Callable() { - /** Use interpreter as marker for not having run. */ - private Object result = interpreter; - + return new Callable(jexl.createInterpreter(context, local)) { @Override - public Object call() throws Exception { - if (result == interpreter) { - JexlNode block = script.jjtGetChild(script.jjtGetNumChildren() - 1); - result = interpreter.interpret(block); - } - return result; + public Object interpret() { + JexlNode block = script.jjtGetChild(script.jjtGetNumChildren() - 1); + return interpreter.interpret(block); } }; } diff --git a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java index 4f2bee150..03134b293 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java @@ -90,6 +90,10 @@ import org.apache.commons.jexl3.parser.ASTUnaryMinusNode; import org.apache.commons.jexl3.parser.ASTVar; import org.apache.commons.jexl3.parser.ASTWhileStatement; +import org.apache.commons.jexl3.parser.ASTAnnotatedStatement; +import org.apache.commons.jexl3.parser.ASTAnnotation; +import org.apache.commons.jexl3.parser.ASTNullpNode; + import org.apache.commons.jexl3.parser.JexlNode; import org.apache.commons.jexl3.parser.ParserVisitor; @@ -276,7 +280,8 @@ protected Object acceptStatement(JexlNode child, Object data) { || child instanceof ASTBlock || child instanceof ASTIfStatement || child instanceof ASTForeachStatement - || child instanceof ASTWhileStatement)) { + || child instanceof ASTWhileStatement + || child instanceof ASTAnnotation)) { builder.append(';'); if (indent > 0) { builder.append('\n'); @@ -576,13 +581,28 @@ protected Object visit(ASTGENode node, Object data) { protected Object visit(ASTGTNode node, Object data) { return infixChildren(node, " > ", false, data); } - /** Checks identifiers that contain space, quote, double-quotes or backspace. */ - protected static final Pattern QUOTED_IDENTIFIER = Pattern.compile("['\"\\s\\\\]"); + + /** Checks identifiers that contain spaces or punctuation + * (but underscore, at-sign, sharp-sign and dollar). + */ + protected static final Pattern QUOTED_IDENTIFIER = + Pattern.compile("[\\s]|[\\p{Punct}&&[^@#\\$_]]"); + + /** + * Checks whether an identifier should be quoted or not. + * @param str the identifier + * @return true if needing quotes, false otherwise + */ + protected boolean needQuotes(String str) { + return QUOTED_IDENTIFIER.matcher(str).find() + || "size".equals(str) + || "empty".equals(str); + } @Override protected Object visit(ASTIdentifier node, Object data) { String image = node.getName(); - if (QUOTED_IDENTIFIER.matcher(image).find()) { + if (needQuotes(image)) { // quote it image = "'" + image.replace("'", "\\'") + "'"; } @@ -593,7 +613,7 @@ protected Object visit(ASTIdentifier node, Object data) { protected Object visit(ASTIdentifierAccess node, Object data) { builder.append("."); String image = node.getName(); - if (QUOTED_IDENTIFIER.matcher(image).find()) { + if (needQuotes(image)) { // quote it image = "'" + image.replace("'", "\\'") + "'"; } @@ -603,15 +623,23 @@ protected Object visit(ASTIdentifierAccess node, Object data) { @Override protected Object visit(ASTIfStatement node, Object data) { + final int numChildren = node.jjtGetNumChildren(); + // if (...) ... builder.append("if ("); accept(node.jjtGetChild(0), data); builder.append(") "); - if (node.jjtGetNumChildren() > 1) { - acceptStatement(node.jjtGetChild(1), data); - if (node.jjtGetNumChildren() > 2) { - builder.append(" else "); - acceptStatement(node.jjtGetChild(2), data); - } + acceptStatement(node.jjtGetChild(1), data); + //.. else if (...) ... + for(int c = 2; c < numChildren - 1; c += 2) { + builder.append(" else if ("); + accept(node.jjtGetChild(c), data); + builder.append(") "); + acceptStatement(node.jjtGetChild(c + 1), data); + } + // else... (if odd) + if ((numChildren & 1) == 1) { + builder.append(" else "); + acceptStatement(node.jjtGetChild(numChildren - 1), data); } else { builder.append(';'); } @@ -883,6 +911,14 @@ protected Object visit(ASTTernaryNode node, Object data) { return data; } + @Override + protected Object visit(ASTNullpNode node, Object data) { + accept(node.jjtGetChild(0), data); + builder.append("??"); + accept(node.jjtGetChild(1), data); + return data; + } + @Override protected Object visit(ASTTrueNode node, Object data) { check(node, "true", data); @@ -959,4 +995,28 @@ protected Object visit(ASTJxltLiteral node, Object data) { String img = node.getLiteral().replace("`", "\\`"); return check(node, "`" + img + "`", data); } + + @Override + protected Object visit(ASTAnnotation node, Object data) { + int num = node.jjtGetNumChildren(); + builder.append('@'); + builder.append(node.getName()); + if (num > 0) { + accept(node.jjtGetChild(0), data); // zut + } + return null; + } + + @Override + protected Object visit(ASTAnnotatedStatement node, Object data) { + int num = node.jjtGetNumChildren(); + for (int i = 0; i < num; ++i) { + if (i > 0) {// && child instanceof ASTBlock) { + builder.append(' '); + } + JexlNode child = node.jjtGetChild(i); + acceptStatement(child, data); + } + return data; + } } \ No newline at end of file diff --git a/src/main/java/org/apache/commons/jexl3/internal/Engine.java b/src/main/java/org/apache/commons/jexl3/internal/Engine.java index eae70b551..0ae5ddf64 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java @@ -21,6 +21,7 @@ import org.apache.commons.jexl3.JexlContext; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JexlInfo; import org.apache.commons.jexl3.JexlScript; import org.apache.commons.jexl3.internal.introspection.SandboxUberspect; @@ -43,11 +44,10 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -import java.lang.ref.SoftReference; import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -73,6 +73,10 @@ private static final class UberspectHolder { /** Non-instantiable. */ private UberspectHolder() {} } + /** + * The Log to which all JexlEngine messages will be logged. + */ + protected final Log logger; /** * The JexlUberspect instance. */ @@ -82,13 +86,9 @@ private UberspectHolder() {} */ protected final JexlArithmetic arithmetic; /** - * The Log to which all JexlEngine messages will be logged. - */ - protected final Log logger; - /** - * The {@link Parser}; when parsing expressions, this engine synchronizes on the parser. + * The map of 'prefix:function' to object implementing the namespaces. */ - protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$ + protected final Map functions; /** * Whether this engine considers unknown variables, methods and constructors as errors. */ @@ -98,34 +98,49 @@ private UberspectHolder() {} * Default is false. */ protected final boolean silent; + /** + * Whether expressions evaluated by this engine will throw JexlException.Cancel (true) or return null (false) when + * interrupted. + * Default is true when not silent and strict. + */ + protected final boolean cancellable; /** * Whether error messages will carry debugging information. */ protected final boolean debug; /** - * The map of 'prefix:function' to object implementing the namespaces. + * The atomic parsing flag; true whilst parsing. */ - protected final Map functions; + protected final AtomicBoolean parsing = new AtomicBoolean(false); /** - * The expression cache. + * The default charset. + */ + protected final Charset charset; + /** + * The set of default script parsing features. */ - protected final SoftCache cache; + protected final JexlFeatures scriptFeatures; + /** + * The set of default expression parsing features. + */ + protected final JexlFeatures expressionFeatures; + /** + * The {@link Parser}; when parsing expressions, this engine uses the parser if it + * is not already in use otherwise it will create a new temporary one. + */ + protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$ /** * The expression max length to hit the cache. */ protected final int cacheThreshold; /** - * The default charset. + * The expression cache. */ - protected final Charset charset; + protected final SoftCache cache; /** * The default jxlt engine. */ protected volatile TemplateEngine jxlt = null; - /** - * The default cache load factor. - */ - private static final float LOAD_FACTOR = 0.75f; /** * Creates an engine with default arguments. @@ -139,26 +154,34 @@ public Engine() { * @param conf the builder */ public Engine(JexlBuilder conf) { - JexlSandbox sandbox = conf.sandbox(); + // options: + this.strict = conf.strict() == null ? true : conf.strict(); + this.silent = conf.silent() == null ? false : conf.silent(); + this.cancellable = conf.cancellable() == null ? !silent && strict : conf.cancellable(); + this.debug = conf.debug() == null ? true : conf.debug(); + // core properties: JexlUberspect uber = conf.uberspect() == null ? getUberspect(conf.logger(), conf.strategy()) : conf.uberspect(); ClassLoader loader = conf.loader(); if (loader != null) { uber.setClassLoader(loader); } + JexlSandbox sandbox = conf.sandbox(); if (sandbox == null) { this.uberspect = uber; } else { this.uberspect = new SandboxUberspect(uber, sandbox); } this.logger = conf.logger() == null ? LogFactory.getLog(JexlEngine.class) : conf.logger(); - this.functions = conf.namespaces() == null ? Collections.emptyMap() : conf.namespaces(); - this.silent = conf.silent() == null ? false : conf.silent(); - this.debug = conf.debug() == null ? true : conf.debug(); - this.strict = conf.strict() == null ? true : conf.strict(); this.arithmetic = conf.arithmetic() == null ? new JexlArithmetic(this.strict) : conf.arithmetic(); - this.cache = conf.cache() <= 0 ? null : new SoftCache(conf.cache()); - this.cacheThreshold = conf.cacheThreshold(); + this.functions = conf.namespaces() == null ? Collections.emptyMap() : conf.namespaces(); + // parsing & features: + JexlFeatures features = conf.features() == null? DEFAULT_FEATURES : conf.features(); + this.expressionFeatures = new JexlFeatures(features).script(false); + this.scriptFeatures = new JexlFeatures(features).script(true); this.charset = conf.charset(); + // caching: + this.cache = conf.cache() <= 0 ? null : new SoftCache(conf.cache()); + this.cacheThreshold = conf.cacheThreshold(); if (uberspect == null) { throw new IllegalArgumentException("uberspect can not be null"); } @@ -207,6 +230,11 @@ public boolean isStrict() { return strict; } + @Override + public boolean isCancellable() { + return this.cancellable; + } + @Override public void setClassLoader(ClassLoader loader) { uberspect.setClassLoader(loader); @@ -222,116 +250,10 @@ public TemplateEngine createJxltEngine(boolean noScript, int cacheSize, char imm return new TemplateEngine(this, noScript, cacheSize, immediate, deferred); } - /** - * Swaps the current thread local context. - * @param tls the context or null - * @return the previous thread local context - */ - protected JexlContext.ThreadLocal putThreadLocal(JexlContext.ThreadLocal tls) { - JexlContext.ThreadLocal local = CONTEXT.get(); - CONTEXT.set(tls); - return local; - } - - /** - * A soft referenced cache. - *

        The actual cache is held through a soft reference, allowing it to be GCed under - * memory pressure.

        - * @param the cache key entry type - * @param the cache key value type - */ - protected class SoftCache { - /** - * The cache size. - */ - private final int size; - /** - * The soft reference to the cache map. - */ - private SoftReference> ref = null; - - /** - * Creates a new instance of a soft cache. - * @param theSize the cache size - */ - SoftCache(int theSize) { - size = theSize; - } - - /** - * Returns the cache size. - * @return the cache size - */ - int size() { - return size; - } - - /** - * Clears the cache. - */ - void clear() { - ref = null; - } - - /** - * Produces the cache entry set. - * @return the cache entry set - */ - Set> entrySet() { - Map map = ref != null ? ref.get() : null; - return map != null ? map.entrySet() : Collections.>emptySet(); - } - - /** - * Gets a value from cache. - * @param key the cache entry key - * @return the cache entry value - */ - V get(K key) { - final Map map = ref != null ? ref.get() : null; - return map != null ? map.get(key) : null; - } - - /** - * Puts a value in cache. - * @param key the cache entry key - * @param script the cache entry value - */ - void put(K key, V script) { - Map map = ref != null ? ref.get() : null; - if (map == null) { - map = createCache(size); - ref = new SoftReference>(map); - } - map.put(key, script); - } - } - - /** - * Creates a cache. - * @param the key type - * @param the value type - * @param cacheSize the cache size, must be > 0 - * @return a Map usable as a cache bounded to the given size - */ - protected Map createCache(final int cacheSize) { - return new java.util.LinkedHashMap(cacheSize, LOAD_FACTOR, true) { - /** Serial version UID. */ - private static final long serialVersionUID = 1L; - - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > cacheSize; - } - }; - } - @Override public void clearCache() { - synchronized (parser) { - if (cache != null) { - cache.clear(); - } + if (cache != null) { + cache.clear(); } } @@ -345,32 +267,34 @@ protected Interpreter createInterpreter(JexlContext context, Scope.Frame frame) return new Interpreter(this, context, frame); } + @Override - public Script createScript(JexlInfo info, String scriptText, String[] names) { + public Script createExpression(JexlInfo info, String expression) { + return createScript(expressionFeatures, info, expression, null); + } + + @Override + public Script createScript(JexlFeatures features, JexlInfo info, String scriptText, String[] names) { if (scriptText == null) { throw new NullPointerException("source is null"); } - if (info == null && debug) { - info = createInfo(); - } String source = trimSource(scriptText); Scope scope = names == null ? null : new Scope(null, names); - ASTJexlScript tree = parse(info, source, scope, false, false); + ASTJexlScript tree = parse(info, features == null? scriptFeatures : features, source, scope); return new Script(this, source, tree); } - @Override - public Script createExpression(JexlInfo info, String expression) { - if (expression == null) { - throw new NullPointerException("source is null"); - } - if (info == null && debug) { - info = createInfo(); - } - String source = trimSource(expression); - ASTJexlScript tree = parse(info, source, null, false, true); - return new Script(this, source, tree); - } + /** + * The features allowed for property set/get methods. + */ + protected static final JexlFeatures PROPERTY_FEATURES = new JexlFeatures() + .localVar(false) + .loops(false) + .lambda(false) + .script(false) + .arrayReferenceExpr(false) + .methodCall(false) + .register(true); @Override public Object getProperty(Object bean, String expr) { @@ -386,9 +310,8 @@ public Object getProperty(JexlContext context, Object bean, String expr) { String src = trimSource(expr); src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src; try { - final JexlInfo info = debug ? createInfo() : null; final Scope scope = new Scope(null, "#0"); - final ASTJexlScript script = parse(info, src, scope, true, true); + final ASTJexlScript script = parse(null, PROPERTY_FEATURES, src, scope); final JexlNode node = script.jjtGetChild(0); final Scope.Frame frame = script.createFrame(bean); final Interpreter interpreter = createInterpreter(context, frame); @@ -412,13 +335,12 @@ public void setProperty(JexlContext context, Object bean, String expr, Object va if (context == null) { context = EMPTY_CONTEXT; } - // synthetize expr using registers + // synthetize expr using register String src = trimSource(expr); src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src + "=" + "#1"; try { - final JexlInfo info = debug ? createInfo() : null; final Scope scope = new Scope(null, "#0", "#1"); - final ASTJexlScript script = parse(info, src, scope, true, true); + final ASTJexlScript script = parse(null, PROPERTY_FEATURES, src, scope); final JexlNode node = script.jjtGetChild(0); final Scope.Frame frame = script.createFrame(bean, value); final Interpreter interpreter = createInterpreter(context, frame); @@ -509,6 +431,28 @@ protected Object doCreateInstance(Object clazz, Object... args) { return result; } + /** + * Swaps the current thread local context. + * @param tls the context or null + * @return the previous thread local context + */ + protected JexlContext.ThreadLocal putThreadLocal(JexlContext.ThreadLocal tls) { + JexlContext.ThreadLocal local = CONTEXT.get(); + CONTEXT.set(tls); + return local; + } + + /** + * Swaps the current thread local engine. + * @param jexl the engine or null + * @return the previous thread local engine + */ + protected JexlEngine putThreadEngine(JexlEngine jexl) { + JexlEngine pjexl = ENGINE.get(); + ENGINE.set(jexl); + return pjexl; + } + /** * Gets the list of variables accessed by a script. *

        This method will visit all nodes of a script and extract all variables whether they @@ -618,9 +562,6 @@ protected void getVariables(final ASTJexlScript script, JexlNode node, VarCollec JexlNode child = node.jjtGetChild(i); if (collecting && child.isConstant()) { String image = child.toString(); - if (image == null) { - image = new Debugger().data(child); - } collector.add(image); } else { collecting = false; @@ -661,30 +602,57 @@ protected String[] getLocalVariables(JexlScript script) { * Parses an expression. * * @param info information structure + * @param expr whether we parse an expression or a feature * @param src the expression to parse * @param scope the script frame - * @param registers whether the parser should allow the unnamed '#number' syntax for 'registers' - * @param expression whether the parser allows scripts or only expressions * @return the parsed tree * @throws JexlException if any error occurred during parsing */ - protected ASTJexlScript parse(JexlInfo info, String src, Scope scope, boolean registers, boolean expression) { + protected ASTJexlScript parse(JexlInfo info, boolean expr, String src, Scope scope) { + return parse(info, expr? this.expressionFeatures : this.scriptFeatures, src, scope); + } + + /** + * Parses an expression. + * + * @param info information structure + * @param parsingf the set of parsing features + * @param src the expression to parse + * @param scope the script frame + * @return the parsed tree + * @throws JexlException if any error occurred during parsing + */ + protected ASTJexlScript parse(JexlInfo info, JexlFeatures parsingf, String src, Scope scope) { final boolean cached = src.length() < cacheThreshold && cache != null; - ASTJexlScript script; - synchronized (parser) { - if (cached) { - script = cache.get(src); - if (script != null) { - Scope f = script.getScope(); - if ((f == null && scope == null) || (f != null && f.equals(scope))) { - return script; - } + final JexlFeatures features = parsingf != null? parsingf : DEFAULT_FEATURES; + final Source source = cached? new Source(features, src) : null; + ASTJexlScript script = null; + if (source != null) { + script = cache.get(source); + if (script != null) { + Scope f = script.getScope(); + if ((f == null && scope == null) || (f != null && f.equals(scope))) { + return script; } } - script = parser.parse(info, src, scope, registers, expression); - if (cached) { - cache.put(src, script); + } + final JexlInfo ninfo = info == null && debug ? createInfo() : info; + // if parser not in use... + if (parsing.compareAndSet(false, true)) { + try { + // lets parse + script = parser.parse(ninfo, features, src, scope); + } finally { + // no longer in use + parsing.set(false); } + } else { + // ...otherwise parser was in use, create a new temporary one + Parser lparser = new Parser(new StringReader(";")); + script = lparser.parse(ninfo, features, src, scope); + } + if (source != null) { + cache.put(source, script); } return script; } @@ -722,7 +690,8 @@ protected TemplateEngine jxlt() { TemplateEngine e = jxlt; if (e == null) { synchronized(this) { - if (jxlt == null) { + e = jxlt; + if (e == null) { e = new TemplateEngine(this, true, 0, '$', '#'); jxlt = e; } diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index 967c77978..63ce0d3b9 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -16,21 +16,23 @@ */ package org.apache.commons.jexl3.internal; + import org.apache.commons.jexl3.JexlArithmetic; -import org.apache.commons.jexl3.JexlOperator; import org.apache.commons.jexl3.JexlContext; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlOperator; import org.apache.commons.jexl3.JexlScript; import org.apache.commons.jexl3.introspection.JexlMethod; import org.apache.commons.jexl3.introspection.JexlPropertyGet; import org.apache.commons.jexl3.introspection.JexlPropertySet; -import org.apache.commons.jexl3.introspection.JexlUberspect; import org.apache.commons.jexl3.introspection.JexlUberspect.PropertyResolver; import org.apache.commons.jexl3.parser.ASTAddNode; import org.apache.commons.jexl3.parser.ASTAndNode; +import org.apache.commons.jexl3.parser.ASTAnnotatedStatement; +import org.apache.commons.jexl3.parser.ASTAnnotation; import org.apache.commons.jexl3.parser.ASTArguments; import org.apache.commons.jexl3.parser.ASTArrayAccess; import org.apache.commons.jexl3.parser.ASTArrayLiteral; @@ -74,6 +76,7 @@ import org.apache.commons.jexl3.parser.ASTNSWNode; import org.apache.commons.jexl3.parser.ASTNotNode; import org.apache.commons.jexl3.parser.ASTNullLiteral; +import org.apache.commons.jexl3.parser.ASTNullpNode; import org.apache.commons.jexl3.parser.ASTNumberLiteral; import org.apache.commons.jexl3.parser.ASTOrNode; import org.apache.commons.jexl3.parser.ASTRangeNode; @@ -101,51 +104,32 @@ import org.apache.commons.jexl3.parser.ASTWhileStatement; import org.apache.commons.jexl3.parser.JexlNode; import org.apache.commons.jexl3.parser.Node; -import org.apache.commons.jexl3.parser.ParserVisitor; + import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import org.apache.commons.logging.Log; +import java.util.concurrent.Callable; + /** * An interpreter of JEXL syntax. * * @since 2.0 */ -public class Interpreter extends ParserVisitor { - /** The JEXL engine. */ - protected final Engine jexl; - /** The logger. */ - protected final Log logger; - /** The uberspect. */ - protected final JexlUberspect uberspect; - /** The arithmetic handler. */ - protected final JexlArithmetic arithmetic; +public class Interpreter extends InterpreterBase { /** The operators evaluation delegate. */ protected final Operators operators; - /** The map of symboled functions. */ - protected final Map functions; - /** The map of symboled functions. */ - protected Map functors; - /** The context to store/retrieve variables. */ - protected final JexlContext context; - /** The context to store/retrieve variables. */ - protected final JexlContext.NamespaceResolver ns; - /** Strict interpreter flag (may temporarily change when calling size and empty as functions). */ - protected boolean strictEngine; - /** Strict interpreter flag. */ - protected final boolean strictArithmetic; - /** Silent interpreter flag. */ - protected final boolean silent; /** Cache executors. */ protected final boolean cache; - /** symbol values. */ + /** Symbol values. */ protected final Scope.Frame frame; - /** Cancellation support. */ - protected volatile boolean cancelled = false; - /** Empty parameters for method matching. */ - protected static final Object[] EMPTY_PARAMS = new Object[0]; + /** The context to store/retrieve variables. */ + protected final JexlContext.NamespaceResolver ns; + /** The map of 'prefix:function' to object resolving as namespaces. */ + protected final Map functions; + /** The map of dynamically creates namespaces, NamespaceFunctor or duck-types of those. */ + protected Map functors; /** * Creates an interpreter. @@ -154,33 +138,32 @@ public class Interpreter extends ParserVisitor { * @param eFrame the interpreter evaluation frame */ protected Interpreter(Engine engine, JexlContext aContext, Scope.Frame eFrame) { - this.jexl = engine; - this.logger = jexl.logger; - this.uberspect = jexl.uberspect; - this.context = aContext != null ? aContext : Engine.EMPTY_CONTEXT; - if (this.context instanceof JexlEngine.Options) { - JexlEngine.Options opts = (JexlEngine.Options) context; - Boolean ostrict = opts.isStrict(); - Boolean osilent = opts.isSilent(); - this.strictEngine = ostrict == null ? jexl.isStrict() : ostrict; - this.silent = osilent == null ? jexl.isSilent() : osilent; - this.arithmetic = jexl.arithmetic.options(opts); - } else { - this.strictEngine = jexl.isStrict(); - this.silent = jexl.isSilent(); - this.arithmetic = jexl.arithmetic; - } + super(engine, aContext); + this.operators = new Operators(this); + this.cache = jexl.cache != null; + this.frame = eFrame; if (this.context instanceof JexlContext.NamespaceResolver) { ns = ((JexlContext.NamespaceResolver) context); } else { ns = Engine.EMPTY_NS; } this.functions = jexl.functions; - this.strictArithmetic = this.arithmetic.isStrict(); - this.cache = jexl.cache != null; - this.frame = eFrame; this.functors = null; - this.operators = new Operators(this); + } + + /** + * Copy constructor. + * @param ii the interpreter to copy + * @param jexla the arithmetic instance to use (or null) + */ + protected Interpreter(Interpreter ii, JexlArithmetic jexla) { + super(ii, jexla); + operators = ii.operators; + cache = ii.cache; + frame = ii.frame; + ns = ii.ns; + functions = ii.functions; + functors = ii.functors; } /** @@ -193,171 +176,55 @@ protected Interpreter(Engine engine, JexlContext aContext, Scope.Frame eFrame) { * @throws JexlException if any error occurs during interpretation. */ public Object interpret(JexlNode node) { - JexlContext.ThreadLocal local = null; + JexlContext.ThreadLocal tcontext = null; + JexlEngine tjexl = null; try { + if (isCancelled()) { + throw new JexlException.Cancel(node); + } if (context instanceof JexlContext.ThreadLocal) { - local = jexl.putThreadLocal((JexlContext.ThreadLocal) context); + tcontext = jexl.putThreadLocal((JexlContext.ThreadLocal) context); } + tjexl = jexl.putThreadEngine(jexl); return node.jjtAccept(this, null); } catch (JexlException.Return xreturn) { - Object value = xreturn.getValue(); - return value; + return xreturn.getValue(); + } catch (JexlException.Cancel xcancel) { + cancelled |= Thread.interrupted(); + if (isCancellable()) { + throw xcancel.clean(); + } } catch (JexlException xjexl) { - if (silent) { + if (!isSilent()) { + throw xjexl.clean(); + } + if (logger.isWarnEnabled()) { logger.warn(xjexl.getMessage(), xjexl.getCause()); - return null; } - throw xjexl.clean(); } finally { - if (functors != null && AUTOCLOSEABLE != null) { - for (Object functor : functors.values()) { - if (functor != null && AUTOCLOSEABLE.isAssignableFrom(functor.getClass())) { - try { - jexl.invokeMethod(functor, "close", EMPTY_PARAMS); - } catch (Exception xclose) { - logger.warn(xclose.getMessage(), xclose.getCause()); + synchronized(this) { + if (functors != null) { + if (AUTOCLOSEABLE != null) { + for (Object functor : functors.values()) { + closeIfSupported(functor); } } + functors.clear(); + functors = null; } } - functors = null; + jexl.putThreadEngine(tjexl); if (context instanceof JexlContext.ThreadLocal) { - jexl.putThreadLocal(local); - } - } - } - - /** Java7 AutoCloseable interface defined?. */ - private static final Class AUTOCLOSEABLE; - static { - Class c; - try { - c = Class.forName("java.lang.AutoCloseable"); - } catch (ClassNotFoundException xclass) { - c = null; - } - AUTOCLOSEABLE = c; - } - - /** - * Finds the node causing a NPE for diadic operators. - * @param xrt the RuntimeException - * @param node the parent node - * @param left the left argument - * @param right the right argument - * @return the left, right or parent node - */ - protected JexlNode findNullOperand(RuntimeException xrt, JexlNode node, Object left, Object right) { - if (xrt instanceof JexlArithmetic.NullOperand) { - if (left == null) { - return node.jjtGetChild(0); - } - if (right == null) { - return node.jjtGetChild(1); + jexl.putThreadLocal(tcontext); } } - return node; - } - - /** - * Triggered when a variable can not be resolved. - * @param node the node where the error originated from - * @param var the variable name - * @param undef whether the variable is undefined or null - * @return throws JexlException if isStrict, null otherwise - */ - protected Object unsolvableVariable(JexlNode node, String var, boolean undef) { - if (!silent) { - logger.warn(JexlException.variableError(node, var, undef)); - } - if (strictEngine && (undef || arithmetic.isStrict())) { - throw new JexlException.Variable(node, var, undef); - } return null; } - /** - * Triggered when a method can not be resolved. - * @param node the node where the error originated from - * @param method the method name - * @return throws JexlException if isStrict, null otherwise - */ - protected Object unsolvableMethod(JexlNode node, String method) { - if (!silent) { - logger.warn(JexlException.methodError(node, method)); - } - if (strictEngine) { - throw new JexlException.Method(node, method); - } - return null; - } - - /** - * Triggered when a property can not be resolved. - * @param node the node where the error originated from - * @param var the property name - * @param cause the cause if any - * @return throws JexlException if isStrict, null otherwise - */ - protected Object unsolvableProperty(JexlNode node, String var, Throwable cause) { - if (!silent) { - logger.warn(JexlException.propertyError(node, var), cause); - } - if (strictEngine) { - throw new JexlException.Property(node, var, cause); - } - return null; - } - - /** - * Triggered when an operator fails. - * @param node the node where the error originated from - * @param operator the method name - * @param cause the cause of error (if any) - * @throws JexlException if isStrict - */ - protected void operatorError(JexlNode node, JexlOperator operator, Throwable cause) { - if (cause != null) { - if (!silent) { - logger.warn(JexlException.operatorError(node, operator.getOperatorSymbol()), cause); - } - if (strictEngine) { - throw new JexlException.Operator(node, operator.getOperatorSymbol(), cause); - } - } - } - - /** - * Triggered when method, function or constructor invocation fails. - * @param xjexl the JexlException wrapping the original error - * @return throws JexlException if isStrict, null otherwise - */ - protected Object invocationFailed(JexlException xjexl) { - if (!silent) { - logger.warn(xjexl.getMessage(), xjexl.getCause()); - } - if (strictEngine || xjexl instanceof JexlException.Return) { - throw xjexl; - } - return null; - } - - /** - * Checks whether this interpreter execution was cancelled due to thread interruption. - * @return true if cancelled, false otherwise - */ - protected boolean isCancelled() { - if (cancelled | Thread.interrupted()) { - cancelled = true; - } - return cancelled; - } - /** * Resolves a namespace, eventually allocating an instance using context as constructor argument. *

        - * The lifetime of - * such instances span the current expression or script evaluation.

        + * The lifetime of such instances span the current expression or script evaluation.

        * @param prefix the prefix name (may be null for global namespace) * @param node the AST node * @return the namespace instance @@ -365,10 +232,12 @@ protected boolean isCancelled() { protected Object resolveNamespace(String prefix, JexlNode node) { Object namespace; // check whether this namespace is a functor - if (functors != null) { - namespace = functors.get(prefix); - if (namespace != null) { - return namespace; + synchronized (this) { + if (functors != null) { + namespace = functors.get(prefix); + if (namespace != null) { + return namespace; + } } } // check if namespace is a resolver @@ -396,10 +265,12 @@ protected Object resolveNamespace(String prefix, JexlNode node) { } // got a functor, store it and return it if (functor != null) { - if (functors == null) { - functors = new HashMap(); + synchronized (this) { + if (functors == null) { + functors = new HashMap(); + } + functors.put(prefix, functor); } - functors.put(prefix, functor); return functor; } else { return namespace; @@ -451,7 +322,7 @@ protected Object visit(ASTDivNode node, Object data) { Object result = operators.tryOverload(node, JexlOperator.DIVIDE, left, right); return result != JexlEngine.TRY_FAILED ? result : arithmetic.divide(left, right); } catch (ArithmeticException xrt) { - if (!strictArithmetic) { + if (!arithmetic.isStrict()) { return 0.0d; } JexlNode xnode = findNullOperand(xrt, node, left, right); @@ -467,7 +338,7 @@ protected Object visit(ASTModNode node, Object data) { Object result = operators.tryOverload(node, JexlOperator.MOD, left, right); return result != JexlEngine.TRY_FAILED ? result : arithmetic.mod(left, right); } catch (ArithmeticException xrt) { - if (!strictArithmetic) { + if (!arithmetic.isStrict()) { return 0.0d; } JexlNode xnode = findNullOperand(xrt, node, left, right); @@ -695,22 +566,23 @@ protected Object visit(ASTNotNode node, Object data) { @Override protected Object visit(ASTIfStatement node, Object data) { int n = 0; + final int numChildren = node.jjtGetNumChildren(); try { Object result = null; - // first objectNode is the condition - Object expression = node.jjtGetChild(0).jjtAccept(this, null); - if (arithmetic.toBoolean(expression)) { - // first objectNode is true statement - n = 1; - result = node.jjtGetChild(n).jjtAccept(this, null); - } else { - // if there is a false, execute it. false statement is the second - // objectNode - if (node.jjtGetNumChildren() == 3) { - n = 2; - result = node.jjtGetChild(n).jjtAccept(this, null); + // pairs of { conditions , 'then' statement } + for(int ifElse = 0; ifElse < (numChildren - 1); ifElse += 2) { + Object condition = node.jjtGetChild(ifElse).jjtAccept(this, null); + if (arithmetic.toBoolean(condition)) { + // first objectNode is true statement + return node.jjtGetChild(ifElse + 1).jjtAccept(this, null); } } + // if odd... + if ((numChildren & 1) == 1) { + // if there is an else, there are an odd number of children in the statement and it is the last child, + // execute it. + result = node.jjtGetChild(numChildren - 1).jjtAccept(this, null); + } return result; } catch (ArithmeticException xrt) { throw new JexlException(node.jjtGetChild(n), "if error", xrt); @@ -722,6 +594,9 @@ protected Object visit(ASTBlock node, Object data) { int numChildren = node.jjtGetNumChildren(); Object result = null; for (int i = 0; i < numChildren; i++) { + if (isCancelled()) { + throw new JexlException.Cancel(node); + } result = node.jjtGetChild(i).jjtAccept(this, data); } return result; @@ -730,6 +605,9 @@ protected Object visit(ASTBlock node, Object data) { @Override protected Object visit(ASTReturnStatement node, Object data) { Object val = node.jjtGetChild(0).jjtAccept(this, data); + if (isCancelled()) { + throw new JexlException.Cancel(node); + } throw new JexlException.Return(node, null, val); } @@ -756,30 +634,38 @@ protected Object visit(ASTForeachStatement node, Object data) { if (iterableValue != null && node.jjtGetNumChildren() >= 3) { /* third objectNode is the statement to execute */ JexlNode statement = node.jjtGetChild(2); - // get an iterator for the collection/array etc via the - // introspector. - Iterator itemsIterator = uberspect.getIterator(iterableValue); - if (itemsIterator != null) { - while (itemsIterator.hasNext()) { - if (isCancelled()) { - throw new JexlException.Cancel(node); - } - // set loopVariable to value of iterator - Object value = itemsIterator.next(); - if (symbol < 0) { - context.set(loopVariable.getName(), value); - } else { - frame.set(symbol, value); - } - try { - // execute statement - result = statement.jjtAccept(this, data); - } catch (JexlException.Break stmtBreak) { - break; - } catch (JexlException.Continue stmtContinue) { - //continue; + // get an iterator for the collection/array etc via the introspector. + Object forEach = null; + try { + forEach = operators.tryOverload(node, JexlOperator.FOR_EACH, iterableValue); + Iterator itemsIterator = forEach instanceof Iterator + ? (Iterator) forEach + : uberspect.getIterator(iterableValue); + if (itemsIterator != null) { + while (itemsIterator.hasNext()) { + if (isCancelled()) { + throw new JexlException.Cancel(node); + } + // set loopVariable to value of iterator + Object value = itemsIterator.next(); + if (symbol < 0) { + context.set(loopVariable.getName(), value); + } else { + frame.set(symbol, value); + } + try { + // execute statement + result = statement.jjtAccept(this, data); + } catch (JexlException.Break stmtBreak) { + break; + } catch (JexlException.Continue stmtContinue) { + //continue; + } } } + } finally { + // closeable iterator handling + closeIfSupported(forEach); } } return result; @@ -821,7 +707,7 @@ protected Object visit(ASTAndNode node, Object data) { if (!leftValue) { return Boolean.FALSE; } - } catch (RuntimeException xrt) { + } catch (ArithmeticException xrt) { throw new JexlException(node.jjtGetChild(0), "boolean coercion error", xrt); } Object right = node.jjtGetChild(1).jjtAccept(this, data); @@ -894,21 +780,20 @@ protected Object visit(ASTStringLiteral node, Object data) { protected Object visit(ASTArrayLiteral node, Object data) { int childCount = node.jjtGetNumChildren(); JexlArithmetic.ArrayBuilder ab = arithmetic.arrayBuilder(childCount); - if (ab != null) { - boolean extended = false; - for (int i = 0; i < childCount; i++) { - JexlNode child = node.jjtGetChild(i); - if (child instanceof ASTExtendedLiteral) { - extended = true; - } else { - Object entry = node.jjtGetChild(i).jjtAccept(this, data); - ab.add(entry); - } + boolean extended = false; + for (int i = 0; i < childCount; i++) { + if (isCancelled()) { + throw new JexlException.Cancel(node); + } + JexlNode child = node.jjtGetChild(i); + if (child instanceof ASTExtendedLiteral) { + extended = true; + } else { + Object entry = node.jjtGetChild(i).jjtAccept(this, data); + ab.add(entry); } - return ab.create(extended); - } else { - return null; } + return ab.create(extended); } @Override @@ -920,30 +805,28 @@ protected Object visit(ASTExtendedLiteral node, Object data) { protected Object visit(ASTSetLiteral node, Object data) { int childCount = node.jjtGetNumChildren(); JexlArithmetic.SetBuilder mb = arithmetic.setBuilder(childCount); - if (mb != null) { - for (int i = 0; i < childCount; i++) { - Object entry = node.jjtGetChild(i).jjtAccept(this, data); - mb.add(entry); + for (int i = 0; i < childCount; i++) { + if (isCancelled()) { + throw new JexlException.Cancel(node); } - return mb.create(); - } else { - return null; + Object entry = node.jjtGetChild(i).jjtAccept(this, data); + mb.add(entry); } + return mb.create(); } @Override protected Object visit(ASTMapLiteral node, Object data) { int childCount = node.jjtGetNumChildren(); JexlArithmetic.MapBuilder mb = arithmetic.mapBuilder(childCount); - if (mb != null) { - for (int i = 0; i < childCount; i++) { - Object[] entry = (Object[]) (node.jjtGetChild(i)).jjtAccept(this, data); - mb.put(entry[0], entry[1]); + for (int i = 0; i < childCount; i++) { + if (isCancelled()) { + throw new JexlException.Cancel(node); } - return mb.create(); - } else { - return null; + Object[] entry = (Object[]) (node.jjtGetChild(i)).jjtAccept(this, data); + mb.put(entry[0], entry[1]); } + return mb.create(); } @Override @@ -970,15 +853,19 @@ protected Object visit(ASTTernaryNode node, Object data) { } } + @Override + protected Object visit(ASTNullpNode node, Object data) { + Object lhs = node.jjtGetChild(0).jjtAccept(this, data); + return lhs != null? lhs : node.jjtGetChild(1).jjtAccept(this, data); + } + @Override protected Object visit(ASTSizeFunction node, Object data) { - boolean isStrict = this.strictEngine; try { - strictEngine = false; Object val = node.jjtGetChild(0).jjtAccept(this, data); return operators.size(node, val); - } finally { - strictEngine = isStrict; + } catch(JexlException xany) { + return 0; } } @@ -990,13 +877,11 @@ protected Object visit(ASTSizeMethod node, Object data) { @Override protected Object visit(ASTEmptyFunction node, Object data) { - boolean isStrict = this.strictEngine; try { - strictEngine = false; Object value = node.jjtGetChild(0).jjtAccept(this, data); return operators.empty(node, value); - } finally { - strictEngine = isStrict; + } catch(JexlException xany) { + return true; } } @@ -1016,6 +901,9 @@ protected Object visit(ASTJexlScript node, Object data) { for (int i = 0; i < numChildren; i++) { JexlNode child = node.jjtGetChild(i); result = child.jjtAccept(this, data); + if (isCancelled()) { + throw new JexlException.Cancel(child); + } } return result; } @@ -1067,6 +955,9 @@ protected Object visit(ASTArrayAccess node, Object data) { return null; } Object index = nindex.jjtAccept(this, null); + if (isCancelled()) { + throw new JexlException.Cancel(node); + } object = getAttribute(object, index, nindex); } return object; @@ -1076,7 +967,7 @@ protected Object visit(ASTArrayAccess node, Object data) { * Check if a null evaluated expression is protected by a ternary expression. *

        * The rationale is that the ternary / elvis expressions are meant for the user to explictly take control - * over the error generation; ie, ternaries can return null even if the engine in isStrict mode + * over the error generation; ie, ternaries can return null even if the engine in strict mode * would normally throw an exception. *

        * @param node the expression node @@ -1086,7 +977,11 @@ protected boolean isTernaryProtected(JexlNode node) { for (JexlNode walk = node.jjtGetParent(); walk != null; walk = walk.jjtGetParent()) { if (walk instanceof ASTTernaryNode) { return true; - } else if (!(walk instanceof ASTReference || walk instanceof ASTArrayAccess)) { + } + if (walk instanceof ASTNullpNode) { + return true; + } + if (!(walk instanceof ASTReference || walk instanceof ASTArrayAccess)) { break; } } @@ -1120,26 +1015,52 @@ protected Object visit(ASTReference node, Object data) { // pass first piece of data in and loop through children Object object = null; JexlNode objectNode; + JexlNode ptyNode = null; StringBuilder ant = null; boolean antish = !(parent instanceof ASTReference); int v = 1; main: for (int c = 0; c < numChildren; c++) { objectNode = node.jjtGetChild(c); - if (objectNode instanceof ASTMethodNode && object == null) { - break; + if (objectNode instanceof ASTMethodNode) { + if (object == null) { + // we may be performing a method call on an antish var + if (ant != null) { + JexlNode child = objectNode.jjtGetChild(0); + if (child instanceof ASTIdentifierAccess) { + ant.append('.'); + ant.append(((ASTIdentifierAccess) child).getName()); + object = context.get(ant.toString()); + if (object != null) { + object = visit((ASTMethodNode) objectNode, object, context); + } + continue; + } + } + break; + } else { + antish = false; + } } // attempt to evaluate the property within the object (visit(ASTIdentifierAccess node)) object = objectNode.jjtAccept(this, object); + if (isCancelled()) { + throw new JexlException.Cancel(node); + } if (object != null) { // disallow mixing antish variable & bean with same root; avoid ambiguity antish = false; } else if (antish) { // if we still have a null object, check for an antish variable if (ant == null) { JexlNode first = node.jjtGetChild(0); - if (first instanceof ASTIdentifier && ((ASTIdentifier) first).getSymbol() < 0) { - ant = new StringBuilder(((ASTIdentifier) first).getName()); + if (first instanceof ASTIdentifier) { + if (((ASTIdentifier) first).getSymbol() < 0) { + ant = new StringBuilder(((ASTIdentifier) first).getName()); + } else { + break; + } } else { + ptyNode = objectNode; break; } } @@ -1154,13 +1075,20 @@ protected Object visit(ASTReference node, Object data) { } object = context.get(ant.toString()); } else { - break; + // the last one may be null + ptyNode = c != numChildren - 1? objectNode : null; + break; // } } - if (object == null && antish && ant != null && !isTernaryProtected(node)) { - boolean undefined = !(context.has(ant.toString()) || isLocalVariable(node, 0)); - // variable unknown in context and not a local - return unsolvableVariable(node, ant.toString(), undefined); + if (object == null && !isTernaryProtected(node)) { + if (antish && ant != null) { + boolean undefined = !(context.has(ant.toString()) || isLocalVariable(node, 0)); + // variable unknown in context and not a local + return unsolvableVariable(node, ant.toString(), undefined); + } + if (ptyNode != null) { + return unsolvableProperty(node, ptyNode.toString(), null); + } } return object; } @@ -1353,11 +1281,11 @@ protected Object executeAssign(JexlNode node, JexlOperator assignop, Object data } if (property == null) { // no property, we fail - throw new JexlException(propertyNode, "property is null"); + return unsolvableProperty(propertyNode, ".", null); } if (object == null) { // no object, we fail - throw new JexlException(objectNode, "bean is null"); + return unsolvableProperty(objectNode, ".", null); } // 3: one before last, assign if (assignop != null) { @@ -1383,31 +1311,45 @@ protected Object[] visit(ASTArguments node, Object data) { @Override protected Object visit(final ASTMethodNode node, Object data) { + return visit(node, null, data); + } + + /** + * Execute a method call, ie syntactically written as name.call(...). + * @param node the actual method call node + * @param object non null when name.call is an antish variable + * @param data the context + * @return the method call result + */ + private Object visit(final ASTMethodNode node, Object object, Object data) { // left contains the reference to the method final JexlNode methodNode = node.jjtGetChild(0); - Object object = null; - JexlNode objectNode = null; Object method; // 1: determine object and method or functor if (methodNode instanceof ASTIdentifierAccess) { method = methodNode; - object = data; if (object == null) { - // no object, we fail - throw new JexlException(objectNode, "object is null"); + object = data; + if (object == null) { + // no object, we fail + return unsolvableMethod(methodNode, ".(...)"); + } + } else { + // edge case of antish var used as functor + method = object; } } else { - method = methodNode.jjtAccept(this, null); + method = methodNode.jjtAccept(this, data); } Object result = method; for (int a = 1; a < node.jjtGetNumChildren(); ++a) { if (result == null) { // no method, we fail - throw new JexlException(methodNode, "method is null"); + return unsolvableMethod(methodNode, ".(...)"); } ASTArguments argNode = (ASTArguments) node.jjtGetChild(a); result = call(node, object, result, argNode); - object = null; + object = result; } return result; } @@ -1416,13 +1358,9 @@ protected Object visit(final ASTMethodNode node, Object data) { protected Object visit(ASTFunctionNode node, Object data) { int argc = node.jjtGetNumChildren(); if (argc == 2) { - Object namespace = resolveNamespace(null, node); - if (namespace == null) { - namespace = context; - } ASTIdentifier functionNode = (ASTIdentifier) node.jjtGetChild(0); ASTArguments argNode = (ASTArguments) node.jjtGetChild(1); - return call(node, namespace, functionNode, argNode); + return call(node, context, functionNode, argNode); } else { // objectNode 0 is the prefix String prefix = ((ASTIdentifier) node.jjtGetChild(0)).getName(); @@ -1437,34 +1375,41 @@ protected Object visit(ASTFunctionNode node, Object data) { /** * Concatenate arguments in call(...). *

        When target == context, we are dealing with a global namespace function call - * @param target the pseudo-method owner, first to-be argument - * @param args the other arguments + * @param target the pseudo-method owner, first to-be argument + * @param narrow whether we should attempt to narrow number arguments + * @param args the other (non null) arguments * @return the arguments array */ - private Object[] functionArguments(Object target, Object[] args) { - if (args == null) { - return new Object[]{target}; - } - if (context == target) { + private Object[] functionArguments(Object target, boolean narrow, Object[] args) { + // when target == context, we are dealing with the null namespace + if (target == null || target == context) { + if (narrow) { + arithmetic.narrowArguments(args); + } return args; } + // makes target 1st args, copy others - optionally narrow numbers Object[] nargv = new Object[args.length + 1]; - nargv[0] = target; - System.arraycopy(args, 0, nargv, 1, args.length); + if (narrow) { + nargv[0] = functionArgument(true, target); + for (int a = 1; a <= args.length; ++a) { + nargv[a] = functionArgument(true, args[a - 1]); + } + } else { + nargv[0] = target; + System.arraycopy(args, 0, nargv, 1, args.length); + } return nargv; } /** - * Narrow arguments in call(...). - * @param narrow predicate - * @param args the arguments to narrow - * @return the narrowed arguments (or the original ones) + * Optionally narrows an argument for a function call. + * @param narrow whether narrowing should occur + * @param arg the argument + * @return the narrowed argument */ - private Object[] narrowArguments(boolean narrow, Object[] args) { - if (narrow) { - arithmetic.narrowArguments(args); - } - return args; + private Object functionArgument(boolean narrow, Object arg) { + return narrow && arg instanceof Number ? arithmetic.narrow((Number) arg) : arg; } /** @@ -1477,7 +1422,7 @@ private static class Funcall { protected final JexlMethod me; /** * Constructor. - * @param jme the method + * @param jme the method * @param flag the narrow flag */ protected Funcall(JexlMethod jme, boolean flag) { @@ -1487,14 +1432,14 @@ protected Funcall(JexlMethod jme, boolean flag) { /** * Try invocation. - * @param ii the interpreter - * @param name the method name + * @param ii the interpreter + * @param name the method name * @param target the method target - * @param args the method arguments + * @param args the method arguments * @return the method invocation result (or JexlEngine.TRY_FAILED) */ protected Object tryInvoke(Interpreter ii, String name, Object target, Object[] args) { - return me.tryInvoke(name, target, ii.narrowArguments(narrow, args)); + return me.tryInvoke(name, target, ii.functionArguments(null, narrow, args)); } } @@ -1504,7 +1449,7 @@ protected Object tryInvoke(Interpreter ii, String name, Object target, Object[] private static class ArithmeticFuncall extends Funcall { /** * Constructor. - * @param jme the method + * @param jme the method * @param flag the narrow flag */ protected ArithmeticFuncall(JexlMethod jme, boolean flag) { @@ -1513,17 +1458,17 @@ protected ArithmeticFuncall(JexlMethod jme, boolean flag) { @Override protected Object tryInvoke(Interpreter ii, String name, Object target, Object[] args) { - Object[] nargs = ii.functionArguments(target, args); - return me.tryInvoke(name, ii.arithmetic, ii.narrowArguments(narrow, nargs)); + return me.tryInvoke(name, ii.arithmetic, ii.functionArguments(target, narrow, args)); } } + /** * Cached context function call. */ private static class ContextFuncall extends Funcall { /** * Constructor. - * @param jme the method + * @param jme the method * @param flag the narrow flag */ protected ContextFuncall(JexlMethod jme, boolean flag) { @@ -1532,8 +1477,7 @@ protected ContextFuncall(JexlMethod jme, boolean flag) { @Override protected Object tryInvoke(Interpreter ii, String name, Object target, Object[] args) { - Object[] nargs = ii.functionArguments(target, args); - return me.tryInvoke(name, ii.context, ii.narrowArguments(narrow, nargs)); + return me.tryInvoke(name, ii.context, ii.functionArguments(target, narrow, args)); } } @@ -1558,12 +1502,11 @@ protected Object call(final JexlNode node, Object target, Object functor, final if (isCancelled()) { throw new JexlException.Cancel(node); } - JexlException xjexl; // evaluate the arguments Object[] argv = visit(argNode, null); // get the method name if identifier - String methodName = null; - int symbol = -1; + final int symbol; + final String methodName; if (functor instanceof ASTIdentifier) { ASTIdentifier methodIdentifier = (ASTIdentifier) functor; symbol = methodIdentifier.getSymbol(); @@ -1571,26 +1514,58 @@ protected Object call(final JexlNode node, Object target, Object functor, final functor = null; } else if (functor instanceof ASTIdentifierAccess) { methodName = ((ASTIdentifierAccess) functor).getName(); + symbol = -1; functor = null; + } else if (functor != null) { + symbol = -1 - 1; // -2; + methodName = null; + } else { + return unsolvableMethod(node, "?"); } + // at this point, either the functor is a non null (hopefully) 'invocable' object or we do have the methodName + Object caller = target; try { boolean cacheable = cache; - // attempt to reuse last funcall cached in volatile JexlNode.value - if (cache) { - Object cached = node.jjtGetValue(); - if (cached instanceof Funcall) { - Object eval = ((Funcall) cached).tryInvoke(this, methodName, target, argv); - if (JexlEngine.TRY_FAILED != eval) { - return eval; + // do we have a method/function name ? + if (methodName != null) { + // is it a global or local variable ? + if (target == context) { + boolean isavar = true; + if (symbol >= 0) { + functor = frame.get(symbol); + } else if (context.has(methodName)) { + functor = context.get(methodName); + } else { + isavar = false; + } + // name is a variable, must be a functor, cant be cached + if (isavar) { + if (functor == null) { + return unsolvableMethod(node, methodName); + } + cacheable = false; } } + // attempt to reuse last funcall cached in volatile JexlNode.value (if it was not a variable) + if (cacheable) { + Object cached = node.jjtGetValue(); + if (cached instanceof Funcall) { + Object eval = ((Funcall) cached).tryInvoke(this, methodName, target, argv); + if (JexlEngine.TRY_FAILED != eval) { + return eval; + } + } + } + } else { + // if no name, we should not cache + cacheable = false; } boolean narrow = false; JexlMethod vm = null; Funcall funcall = null; - // pseudo loop and a half + // pseudo loop and a half to try acquiring methods without and with argument narrowing while (true) { - if (methodName != null) { + if (functor == null) { // try a method vm = uberspect.getMethod(target, methodName, argv); if (vm != null) { @@ -1599,12 +1574,19 @@ protected Object call(final JexlNode node, Object target, Object functor, final } break; } - } - // could not find a method, try as a var (local, global) or property (performed once) - if (functor == null && !narrow) { - if (symbol >= 0) { - functor = frame.get(symbol); - } else { + // solve 'null' namespace + if (target == context) { + Object namespace = resolveNamespace(null, node); + if (namespace == context) { + // we can not solve it + break; + } else if (namespace != null) { + target = namespace; + caller = null; + continue; + } + // could not find a method, try as a property of a non-context target (performed once) + } else if (!narrow) { // the method may be a functor stored in a property of the target JexlPropertyGet get = uberspect.getPropertyGet(target, methodName); if (get != null) { @@ -1612,7 +1594,10 @@ protected Object call(final JexlNode node, Object target, Object functor, final } } } + final Object[] nargv; + final String mname; // this may happen without the above when we are chaining call like x(a)(b) + // or when a var/symbol or antish var is used as a "function" name if (functor != null) { // lambda, script or jexl method will do if (functor instanceof JexlScript) { @@ -1622,42 +1607,42 @@ protected Object call(final JexlNode node, Object target, Object functor, final return ((JexlMethod) functor).invoke(target, argv); } // a generic callable - vm = uberspect.getMethod(functor, "call", argv); + mname = "call"; + vm = uberspect.getMethod(functor, mname, argv); if (vm != null) { return vm.invoke(functor, argv); } + // prepend functor to arg to try JexlArithmetic or JexlContext function + nargv = functionArguments(functor, narrow, argv); + } else { + mname = methodName; + // no need to narrow since this has been performed in previous loop + nargv = functionArguments(caller, narrow, argv); } - // try JexlArithmetic or JexlContext function - if (methodName != null) { - // when target == context, we are dealing with a global namespace function call - Object[] nargv = functionArguments(target, argv); - vm = uberspect.getMethod(context, methodName, nargv); - if (vm != null) { - argv = nargv; - target = context; - if (cacheable && vm.isCacheable()) { - funcall = new ContextFuncall(vm, narrow); - } - break; - } - vm = uberspect.getMethod(arithmetic, methodName, nargv); - if (vm != null) { - argv = nargv; - target = arithmetic; - if (cacheable && vm.isCacheable()) { - funcall = new ArithmeticFuncall(vm, narrow); - } - break; + vm = uberspect.getMethod(context, mname, nargv); + if (vm != null) { + argv = nargv; + target = context; + if (cacheable && vm.isCacheable()) { + funcall = new ContextFuncall(vm, narrow); } - // if we did not find an exact method by name and we haven't tried yet, - // attempt to narrow the parameters and if this succeeds, try again in next loop - if (arithmetic.narrowArguments(argv)) { - narrow = true; - continue; + break; + } + vm = uberspect.getMethod(arithmetic, mname, nargv); + if (vm != null) { + argv = nargv; + target = arithmetic; + if (cacheable && vm.isCacheable()) { + funcall = new ArithmeticFuncall(vm, narrow); } + break; } - // we are done trying - break; + // if we did not find an exact method by name and we haven't tried yet, + // attempt to narrow the parameters and if this succeeds, try again in next loop + if (!arithmetic.narrowArguments(argv)) { + break; + } + narrow = true; } // we have either evaluated and returned or might have found a method if (vm != null) { @@ -1670,12 +1655,11 @@ protected Object call(final JexlNode node, Object target, Object functor, final return eval; } return unsolvableMethod(node, methodName); - } catch (JexlException.Method xmethod) { - throw xmethod; + } catch (JexlException xthru) { + throw xthru; } catch (Exception xany) { - xjexl = new JexlException(node, methodName, xany); + throw invocationException(node, methodName, xany); } - return invocationFailed(xjexl); } @Override @@ -1692,7 +1676,6 @@ protected Object visit(ASTConstructorNode node, Object data) { argv[i] = node.jjtGetChild(i + 1).jjtAccept(this, data); } - JexlException xjexl = null; try { // attempt to reuse last constructor cached in volatile JexlNode.value if (cache) { @@ -1722,13 +1705,12 @@ protected Object visit(ASTConstructorNode node, Object data) { node.jjtSetValue(ctor); } return instance; - } catch (JexlException.Method xmethod) { - throw xmethod; + } catch (JexlException xthru) { + throw xthru; } catch (Exception xany) { String dbgStr = cobject != null ? cobject.toString() : null; - xjexl = new JexlException(node, dbgStr, xany); + throw invocationException(node, dbgStr, xany); } - return invocationFailed(xjexl); } /** @@ -1758,37 +1740,37 @@ protected Object getAttribute(Object object, Object attribute, JexlNode node) { throw new JexlException.Cancel(node); } final JexlOperator operator = node != null && node.jjtGetParent() instanceof ASTArrayAccess - ? JexlOperator.ARRAY_GET : JexlOperator.PROPERTY_GET; + ? JexlOperator.ARRAY_GET : JexlOperator.PROPERTY_GET; Object result = operators.tryOverload(node, operator, object, attribute); if (result != JexlEngine.TRY_FAILED) { return result; } - // attempt to reuse last executor cached in volatile JexlNode.value - if (node != null && cache) { - Object cached = node.jjtGetValue(); - if (cached instanceof JexlPropertyGet) { - JexlPropertyGet vg = (JexlPropertyGet) cached; - Object value = vg.tryInvoke(object, attribute); - if (!vg.tryFailed(value)) { - return value; + Exception xcause = null; + try { + // attempt to reuse last executor cached in volatile JexlNode.value + if (node != null && cache) { + Object cached = node.jjtGetValue(); + if (cached instanceof JexlPropertyGet) { + JexlPropertyGet vg = (JexlPropertyGet) cached; + Object value = vg.tryInvoke(object, attribute); + if (!vg.tryFailed(value)) { + return value; + } } } - } - // resolve that property - Exception xcause = null; - List resolvers = uberspect.getResolvers(operator, object); - JexlPropertyGet vg = uberspect.getPropertyGet(resolvers, object, attribute); - if (vg != null) { - try { + // resolve that property + List resolvers = uberspect.getResolvers(operator, object); + JexlPropertyGet vg = uberspect.getPropertyGet(resolvers, object, attribute); + if (vg != null) { Object value = vg.invoke(object); // cache executor in volatile JexlNode.value if (node != null && cache && vg.isCacheable()) { node.jjtSetValue(vg); } return value; - } catch (Exception xany) { - xcause = xany; } + } catch (Exception xany) { + xcause = xany; } // lets fail if (node != null) { @@ -1827,44 +1809,44 @@ protected void setAttribute(Object object, Object attribute, Object value, JexlN throw new JexlException.Cancel(node); } final JexlOperator operator = node != null && node.jjtGetParent() instanceof ASTArrayAccess - ? JexlOperator.ARRAY_SET : JexlOperator.PROPERTY_SET; + ? JexlOperator.ARRAY_SET : JexlOperator.PROPERTY_SET; Object result = operators.tryOverload(node, operator, object, attribute, value); if (result != JexlEngine.TRY_FAILED) { return; } - // attempt to reuse last executor cached in volatile JexlNode.value - if (node != null && cache) { - Object cached = node.jjtGetValue(); - if (cached instanceof JexlPropertySet) { - JexlPropertySet setter = (JexlPropertySet) cached; - Object eval = setter.tryInvoke(object, attribute, value); - if (!setter.tryFailed(eval)) { - return; + Exception xcause = null; + try { + // attempt to reuse last executor cached in volatile JexlNode.value + if (node != null && cache) { + Object cached = node.jjtGetValue(); + if (cached instanceof JexlPropertySet) { + JexlPropertySet setter = (JexlPropertySet) cached; + Object eval = setter.tryInvoke(object, attribute, value); + if (!setter.tryFailed(eval)) { + return; + } } } - } - Exception xcause = null; - List resolvers = uberspect.getResolvers(operator, object); - JexlPropertySet vs = uberspect.getPropertySet(resolvers, object, attribute, value); - // if we can't find an exact match, narrow the value argument and try again - if (vs == null) { - // replace all numbers with the smallest type that will fit - Object[] narrow = {value}; - if (arithmetic.narrowArguments(narrow)) { - vs = uberspect.getPropertySet(resolvers, object, attribute, narrow[0]); + List resolvers = uberspect.getResolvers(operator, object); + JexlPropertySet vs = uberspect.getPropertySet(resolvers, object, attribute, value); + // if we can't find an exact match, narrow the value argument and try again + if (vs == null) { + // replace all numbers with the smallest type that will fit + Object[] narrow = {value}; + if (arithmetic.narrowArguments(narrow)) { + vs = uberspect.getPropertySet(resolvers, object, attribute, narrow[0]); + } } - } - if (vs != null) { - try { + if (vs != null) { // cache executor in volatile JexlNode.value vs.invoke(object, value); if (node != null && cache && vs.isCacheable()) { node.jjtSetValue(vs); } return; - } catch (Exception xany) { - xcause = xany; } + } catch (Exception xany) { + xcause = xany; } // lets fail if (node != null) { @@ -1884,14 +1866,98 @@ protected void setAttribute(Object object, Object attribute, Object value, JexlN protected Object visit(ASTJxltLiteral node, Object data) { TemplateEngine.TemplateExpression tp = (TemplateEngine.TemplateExpression) node.jjtGetValue(); if (tp == null) { - TemplateEngine jxlt = jexl.jxlt(); - tp = jxlt.parseExpression(node.jexlInfo(), node.getLiteral(), frame != null? frame.getScope() : null); - node.jjtSetValue(tp); + TemplateEngine jxlt = jexl.jxlt(); + tp = jxlt.parseExpression(node.jexlInfo(), node.getLiteral(), frame != null ? frame.getScope() : null); + node.jjtSetValue(tp); } if (tp != null) { - return tp.evaluate(frame, context); + return tp.evaluate(frame, context); } return null; } + @Override + protected Object visit(ASTAnnotation node, Object data) { + throw new UnsupportedOperationException(ASTAnnotation.class.getName() + ": Not supported."); + } + + @Override + protected Object visit(ASTAnnotatedStatement node, Object data) { + return processAnnotation(node, 0, data); + } + + /** + * Processes an annotated statement. + * @param stmt the statement + * @param index the index of the current annotation being processed + * @param data the contextual data + * @return the result of the statement block evaluation + */ + protected Object processAnnotation(final ASTAnnotatedStatement stmt, final int index, final Object data) { + // are we evaluating the block ? + final int last = stmt.jjtGetNumChildren() - 1; + if (index == last) { + JexlNode block = stmt.jjtGetChild(last); + // if the context has changed, might need a new interpreter + final JexlArithmetic jexla = arithmetic.options(context); + if (jexla != arithmetic) { + if (!arithmetic.getClass().equals(jexla.getClass())) { + logger.warn("expected arithmetic to be " + arithmetic.getClass().getSimpleName() + + ", got " + jexla.getClass().getSimpleName() + ); + } + Interpreter ii = new Interpreter(Interpreter.this, jexla); + Object r = block.jjtAccept(ii, data); + if (ii.isCancelled()) { + Interpreter.this.cancel(); + } + return r; + } else { + return block.jjtAccept(Interpreter.this, data); + } + } + // tracking whether we processed the annotation + final boolean[] processed = new boolean[]{false}; + final Callable jstmt = new Callable() { + @Override + public Object call() throws Exception { + processed[0] = true; + return processAnnotation(stmt, index + 1, data); + } + }; + // the annotation node and name + final ASTAnnotation anode = (ASTAnnotation) stmt.jjtGetChild(index); + final String aname = anode.getName(); + // evaluate the arguments + Object[] argv = anode.jjtGetNumChildren() > 0 + ? visit((ASTArguments) anode.jjtGetChild(0), null) : null; + // wrap the future, will recurse through annotation processor + try { + Object result = processAnnotation(aname, argv, jstmt); + // not processing an annotation is an error + if (!processed[0]) { + return annotationError(anode, aname, null); + } else { + return result; + } + } catch(JexlException xjexl) { + throw xjexl; + } catch(Exception xany) { + return annotationError(anode, aname, xany); + } + } + + /** + * Delegates the annotation processing to the JexlContext if it is an AnnotationProcessor. + * @param annotation the annotation name + * @param args the annotation arguments + * @param stmt the statement / block that was annotated + * @return the result of statement.call() + * @throws Exception if anything goes wrong + */ + protected Object processAnnotation(String annotation, Object[] args, Callable stmt) throws Exception { + return context instanceof JexlContext.AnnotationProcessor + ? ((JexlContext.AnnotationProcessor) context).processAnnotation(annotation, args, stmt) + : stmt.call(); + } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java new file mode 100644 index 000000000..eaadf32e0 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3.internal; + + +import org.apache.commons.jexl3.JexlArithmetic; +import org.apache.commons.jexl3.JexlContext; +import org.apache.commons.jexl3.JexlEngine; +import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlOperator; +import org.apache.commons.jexl3.introspection.JexlMethod; +import org.apache.commons.jexl3.introspection.JexlUberspect; +import org.apache.commons.jexl3.parser.JexlNode; +import org.apache.commons.jexl3.parser.ParserVisitor; + + +import org.apache.commons.logging.Log; + +/** + * The helper base of an interpreter of JEXL syntax. + * @since 3.0 + */ +public abstract class InterpreterBase extends ParserVisitor { + /** The JEXL engine. */ + protected final Engine jexl; + /** The logger. */ + protected final Log logger; + /** The uberspect. */ + protected final JexlUberspect uberspect; + /** The arithmetic handler. */ + protected final JexlArithmetic arithmetic; + /** The context to store/retrieve variables. */ + protected final JexlContext context; + /** Cancellation support. */ + protected volatile boolean cancelled = false; + /** Empty parameters for method matching. */ + protected static final Object[] EMPTY_PARAMS = new Object[0]; + + /** + * Creates an interpreter base. + * @param engine the engine creating this interpreter + * @param aContext the context to evaluate expression + */ + protected InterpreterBase(Engine engine, JexlContext aContext) { + this.jexl = engine; + this.logger = jexl.logger; + this.uberspect = jexl.uberspect; + this.context = aContext != null ? aContext : Engine.EMPTY_CONTEXT; + JexlArithmetic jexla = jexl.arithmetic; + this.arithmetic = jexla.options(context); + if (arithmetic != jexla && !arithmetic.getClass().equals(jexla.getClass())) { + logger.warn("expected arithmetic to be " + jexla.getClass().getSimpleName() + + ", got " + arithmetic.getClass().getSimpleName() + ); + } + } + + /** + * Copy constructor. + * @param ii the base to copy + * @param jexla the arithmetic instance to use (or null) + */ + protected InterpreterBase(InterpreterBase ii, JexlArithmetic jexla) { + jexl = ii.jexl; + logger = ii.logger; + uberspect = ii.uberspect; + context = ii.context; + arithmetic = ii.arithmetic; + } + + + /** Java7 AutoCloseable interface defined?. */ + protected static final Class AUTOCLOSEABLE; + static { + Class c; + try { + c = Class.forName("java.lang.AutoCloseable"); + } catch (ClassNotFoundException xclass) { + c = null; + } + AUTOCLOSEABLE = c; + } + + /** + * Attempt to call close() if supported. + *

        This is used when dealing with auto-closeable (duck-like) objects + * @param closeable the object we'd like to close + */ + protected void closeIfSupported(Object closeable) { + if (closeable != null) { + //if (AUTOCLOSEABLE == null || AUTOCLOSEABLE.isAssignableFrom(closeable.getClass())) { + JexlMethod mclose = uberspect.getMethod(closeable, "close", EMPTY_PARAMS); + if (mclose != null) { + try { + mclose.invoke(closeable, EMPTY_PARAMS); + } catch (Exception xignore) { + logger.warn(xignore); + } + } + //} + } + } + + /** + * Whether this interpreter is currently evaluating with a strict engine flag. + * @return true if strict engine, false otherwise + */ + protected boolean isStrictEngine() { + if (this.context instanceof JexlEngine.Options) { + JexlEngine.Options opts = (JexlEngine.Options) context; + Boolean strict = opts.isStrict(); + if (strict != null) { + return strict.booleanValue(); + } + } + return jexl.isStrict(); + } + + /** + * Whether this interpreter is currently evaluating with a silent mode. + * @return true if silent, false otherwise + */ + protected boolean isSilent() { + if (this.context instanceof JexlEngine.Options) { + JexlEngine.Options opts = (JexlEngine.Options) context; + Boolean silent = opts.isSilent(); + if (silent != null) { + return silent.booleanValue(); + } + } + return jexl.isSilent(); + } + + /** @return true if interrupt throws a JexlException.Cancel. */ + protected boolean isCancellable() { + if (this.context instanceof JexlEngine.Options) { + JexlEngine.Options opts = (JexlEngine.Options) context; + Boolean ocancellable = opts.isCancellable(); + if (ocancellable != null) { + return ocancellable.booleanValue(); + } + } + return jexl.isCancellable(); + } + + /** + * Finds the node causing a NPE for diadic operators. + * @param xrt the RuntimeException + * @param node the parent node + * @param left the left argument + * @param right the right argument + * @return the left, right or parent node + */ + protected JexlNode findNullOperand(RuntimeException xrt, JexlNode node, Object left, Object right) { + if (xrt instanceof JexlArithmetic.NullOperand) { + if (left == null) { + return node.jjtGetChild(0); + } + if (right == null) { + return node.jjtGetChild(1); + } + } + return node; + } + + /** + * Triggered when a variable can not be resolved. + * @param node the node where the error originated from + * @param var the variable name + * @param undef whether the variable is undefined or null + * @return throws JexlException if strict and not silent, null otherwise + */ + protected Object unsolvableVariable(JexlNode node, String var, boolean undef) { + if (isStrictEngine() && (undef || arithmetic.isStrict())) { + throw new JexlException.Variable(node, var, undef); + } else if (logger.isDebugEnabled()) { + logger.debug(JexlException.variableError(node, var, undef)); + } + return null; + } + + /** + * Triggered when a method can not be resolved. + * @param node the node where the error originated from + * @param method the method name + * @return throws JexlException if strict and not silent, null otherwise + */ + protected Object unsolvableMethod(JexlNode node, String method) { + if (isStrictEngine()) { + throw new JexlException.Method(node, method); + } else if (logger.isDebugEnabled()) { + logger.debug(JexlException.methodError(node, method)); + } + return null; + } + + /** + * Triggered when a property can not be resolved. + * @param node the node where the error originated from + * @param var the property name + * @param cause the cause if any + * @return throws JexlException if strict and not silent, null otherwise + */ + protected Object unsolvableProperty(JexlNode node, String var, Throwable cause) { + if (isStrictEngine()) { + throw new JexlException.Property(node, var, cause); + } else if (logger.isDebugEnabled()) { + logger.debug(JexlException.propertyError(node, var), cause); + } + return null; + } + + /** + * Triggered when an operator fails. + * @param node the node where the error originated from + * @param operator the method name + * @param cause the cause of error (if any) + * @return throws JexlException if strict and not silent, null otherwise + */ + protected Object operatorError(JexlNode node, JexlOperator operator, Throwable cause) { + if (isStrictEngine()) { + throw new JexlException.Operator(node, operator.getOperatorSymbol(), cause); + } else if (logger.isDebugEnabled()) { + logger.debug(JexlException.operatorError(node, operator.getOperatorSymbol()), cause); + } + return null; + } + + /** + * Triggered when an annotation processing fails. + * @param node the node where the error originated from + * @param annotation the annotation name + * @param cause the cause of error (if any) + * @return throws a JexlException if strict and not silent, null otherwise + */ + protected Object annotationError(JexlNode node, String annotation, Throwable cause) { + if (isStrictEngine()) { + throw new JexlException.Annotation(node, annotation, cause); + } else if (logger.isDebugEnabled()) { + logger.debug(JexlException.annotationError(node, annotation), cause); + } + return null; + } + + /** + * Triggered when method, function or constructor invocation fails with an exception. + * @param node the node triggering the exception + * @param methodName the method/function name + * @param xany the cause + * @return a JexlException that will be thrown + */ + protected JexlException invocationException(JexlNode node, String methodName, Exception xany) { + Throwable cause = xany.getCause(); + if (cause instanceof JexlException) { + return (JexlException) cause; + } + if (cause instanceof InterruptedException) { + cancelled = true; + return new JexlException.Cancel(node); + } + return new JexlException(node, methodName, xany); + } + + /** + * Cancels this evaluation, setting the cancel flag that will result in a JexlException.Cancel to be thrown. + * @return false if already cancelled, true otherwise + */ + protected synchronized boolean cancel() { + if (cancelled) { + return false; + } else { + cancelled = true; + return true; + } + } + + /** + * Checks whether this interpreter execution was cancelled due to thread interruption. + * @return true if cancelled, false otherwise + */ + protected synchronized boolean isCancelled() { + if (!cancelled) { + cancelled = Thread.currentThread().isInterrupted(); + } + return cancelled; + } +} diff --git a/src/main/java/org/apache/commons/jexl3/internal/Operators.java b/src/main/java/org/apache/commons/jexl3/internal/Operators.java index 036a7cbc8..d157445bc 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Operators.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Operators.java @@ -71,17 +71,17 @@ protected Object tryOverload(JexlNode node, JexlOperator operator, Object... arg if (operators != null && operators.overloads(operator)) { final JexlArithmetic arithmetic = interpreter.arithmetic; final boolean cache = interpreter.cache; - if (cache) { - Object cached = node.jjtGetValue(); - if (cached instanceof JexlMethod) { - JexlMethod me = (JexlMethod) cached; - Object eval = me.tryInvoke(operator.getMethodName(), arithmetic, args); - if (!me.tryFailed(eval)) { - return eval; + try { + if (cache) { + Object cached = node.jjtGetValue(); + if (cached instanceof JexlMethod) { + JexlMethod me = (JexlMethod) cached; + Object eval = me.tryInvoke(operator.getMethodName(), arithmetic, args); + if (!me.tryFailed(eval)) { + return eval; + } } } - } - try { JexlMethod vm = operators.getOperator(operator, args); if (vm != null) { Object result = vm.invoke(arithmetic, args); @@ -91,7 +91,7 @@ protected Object tryOverload(JexlNode node, JexlOperator operator, Object... arg return result; } } catch (Exception xany) { - interpreter.operatorError(node, operator, xany); + return interpreter.operatorError(node, operator, xany); } } return JexlEngine.TRY_FAILED; @@ -139,26 +139,32 @@ protected Object tryAssignOverload(JexlNode node, JexlOperator operator, Object. } } // base eval - switch (operator) { - case SELF_ADD: - return arithmetic.add(args[0], args[1]); - case SELF_SUBTRACT: - return arithmetic.subtract(args[0], args[1]); - case SELF_MULTIPLY: - return arithmetic.multiply(args[0], args[1]); - case SELF_DIVIDE: - return arithmetic.divide(args[0], args[1]); - case SELF_MOD: - return arithmetic.mod(args[0], args[1]); - case SELF_AND: - return arithmetic.and(args[0], args[1]); - case SELF_OR: - return arithmetic.or(args[0], args[1]); - case SELF_XOR: - return arithmetic.xor(args[0], args[1]); - default: - throw new JexlException.Operator(node, operator.getOperatorSymbol(), null); + try { + switch (operator) { + case SELF_ADD: + return arithmetic.add(args[0], args[1]); + case SELF_SUBTRACT: + return arithmetic.subtract(args[0], args[1]); + case SELF_MULTIPLY: + return arithmetic.multiply(args[0], args[1]); + case SELF_DIVIDE: + return arithmetic.divide(args[0], args[1]); + case SELF_MOD: + return arithmetic.mod(args[0], args[1]); + case SELF_AND: + return arithmetic.and(args[0], args[1]); + case SELF_OR: + return arithmetic.or(args[0], args[1]); + case SELF_XOR: + return arithmetic.xor(args[0], args[1]); + default: + // unexpected, new operator added? + throw new UnsupportedOperationException(operator.getOperatorSymbol()); + } + } catch (Exception xany) { + interpreter.operatorError(node, base, xany); } + return JexlEngine.TRY_FAILED; } /** diff --git a/src/main/java/org/apache/commons/jexl3/internal/Script.java b/src/main/java/org/apache/commons/jexl3/internal/Script.java index ec897216d..eeb727f9c 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Script.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Script.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.Callable; /** *

        A JexlScript implementation.

        @@ -50,6 +49,13 @@ public class Script implements JexlScript, JexlExpression { */ protected int version; + /** + * @return the script AST + */ + protected ASTJexlScript getScript() { + return script; + } + /** * Do not let this be generally instantiated with a 'new'. * @@ -75,7 +81,10 @@ protected Script(Engine engine, String expr, ASTJexlScript ref) { protected void checkCacheVersion() { int uberVersion = jexl.getUberspect().getVersion(); if (version != uberVersion) { - script.clearCache(); + // version 0 of the uberSpect is an illusion due to order of construction; no need to clear cache + if (version > 0) { + script.clearCache(); + } version = uberVersion; } } @@ -106,25 +115,16 @@ public JexlEngine getEngine() { return jexl; } - /** - * {@inheritDoc} - */ @Override public String getSourceText() { return source; } - /** - * {@inheritDoc} - */ @Override public String getParsedText() { return getParsedText(2); } - /** - * {@inheritDoc} - */ @Override public String getParsedText(int indent) { Debugger debug = new Debugger(); @@ -169,37 +169,22 @@ public String toString() { debug.debug(script); src = debug.toString(); } - return src == null ? "/*no source*/" : src.toString(); + return src.toString(); } - /** - * {@inheritDoc} - */ @Override public Object evaluate(JexlContext context) { - if (script.jjtGetNumChildren() < 1) { - return null; - } - checkCacheVersion(); - Scope.Frame frame = createFrame((Object[]) null); - Interpreter interpreter = createInterpreter(context, frame); - return interpreter.interpret(script.jjtGetChild(0)); + return execute(context); } - /** - * {@inheritDoc} - */ @Override public Object execute(JexlContext context) { checkCacheVersion(); - Scope.Frame frame = createFrame((Object[]) null); + Scope.Frame frame = createFrame(null); Interpreter interpreter = createInterpreter(context, frame); return interpreter.interpret(script); } - /** - * {@inheritDoc} - */ @Override public Object execute(JexlContext context, Object... args) { checkCacheVersion(); @@ -262,9 +247,6 @@ public Object execute(JexlContext context, Object... args) { } } - /** - * {@inheritDoc} - */ @Override public JexlScript curry(Object... args) { String[] parms = script.getParameters(); @@ -322,7 +304,7 @@ public Map getPragmas() { * @return the callable */ @Override - public Callable callable(JexlContext context) { + public Callable callable(JexlContext context) { return callable(context, (Object[]) null); } @@ -335,20 +317,67 @@ public Callable callable(JexlContext context) { * @return the callable */ @Override - public Callable callable(JexlContext context, Object... args) { - final Interpreter interpreter = jexl.createInterpreter(context, script.createFrame(args)); - return new Callable() { - /** Use interpreter as marker for not having run. */ - private Object result = interpreter; - - @Override - public Object call() throws Exception { + public Callable callable(JexlContext context, Object... args) { + return new Callable(jexl.createInterpreter(context, script.createFrame(args))); + } + + /** + * Implements the Future and Callable interfaces to help delegation. + */ + public class Callable implements java.util.concurrent.Callable { + /** The actual interpreter. */ + protected final Interpreter interpreter; + /** Use interpreter as marker for not having run. */ + protected volatile Object result; + + /** + * The base constructor. + * @param intrprtr the interpreter to use + */ + protected Callable(Interpreter intrprtr) { + this.interpreter = intrprtr; + this.result = intrprtr; + } + + /** + * Run the interpreter. + * @return the evaluation result + */ + protected Object interpret() { + return interpreter.interpret(script); + } + + @Override + public Object call() throws Exception { + synchronized(this) { if (result == interpreter) { checkCacheVersion(); - result = interpreter.interpret(script); + result = interpret(); } return result; } - }; + } + + /** + * Soft cancel the execution. + * @return true if cancel was successful, false otherwise + */ + public boolean cancel() { + return interpreter.cancel(); + } + + /** + * @return true if evaluation was cancelled, false otherwise + */ + public boolean isCancelled() { + return interpreter.isCancelled(); + } + + /** + * @return true if interruption will throw a JexlException.Cancel, false otherwise + */ + public boolean isCancellable() { + return interpreter.isCancellable(); + } } } \ No newline at end of file diff --git a/src/main/java/org/apache/commons/jexl3/internal/ScriptVisitor.java b/src/main/java/org/apache/commons/jexl3/internal/ScriptVisitor.java new file mode 100644 index 000000000..8f728007c --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/internal/ScriptVisitor.java @@ -0,0 +1,496 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3.internal; + +import org.apache.commons.jexl3.JexlExpression; +import org.apache.commons.jexl3.JexlScript; +import org.apache.commons.jexl3.parser.ASTAddNode; +import org.apache.commons.jexl3.parser.ASTAndNode; +import org.apache.commons.jexl3.parser.ASTAnnotatedStatement; +import org.apache.commons.jexl3.parser.ASTAnnotation; +import org.apache.commons.jexl3.parser.ASTArguments; +import org.apache.commons.jexl3.parser.ASTArrayAccess; +import org.apache.commons.jexl3.parser.ASTArrayLiteral; +import org.apache.commons.jexl3.parser.ASTAssignment; +import org.apache.commons.jexl3.parser.ASTBitwiseAndNode; +import org.apache.commons.jexl3.parser.ASTBitwiseComplNode; +import org.apache.commons.jexl3.parser.ASTBitwiseOrNode; +import org.apache.commons.jexl3.parser.ASTBitwiseXorNode; +import org.apache.commons.jexl3.parser.ASTBlock; +import org.apache.commons.jexl3.parser.ASTBreak; +import org.apache.commons.jexl3.parser.ASTConstructorNode; +import org.apache.commons.jexl3.parser.ASTContinue; +import org.apache.commons.jexl3.parser.ASTDivNode; +import org.apache.commons.jexl3.parser.ASTEQNode; +import org.apache.commons.jexl3.parser.ASTERNode; +import org.apache.commons.jexl3.parser.ASTEWNode; +import org.apache.commons.jexl3.parser.ASTEmptyFunction; +import org.apache.commons.jexl3.parser.ASTEmptyMethod; +import org.apache.commons.jexl3.parser.ASTExtendedLiteral; +import org.apache.commons.jexl3.parser.ASTFalseNode; +import org.apache.commons.jexl3.parser.ASTForeachStatement; +import org.apache.commons.jexl3.parser.ASTFunctionNode; +import org.apache.commons.jexl3.parser.ASTGENode; +import org.apache.commons.jexl3.parser.ASTGTNode; +import org.apache.commons.jexl3.parser.ASTIdentifier; +import org.apache.commons.jexl3.parser.ASTIdentifierAccess; +import org.apache.commons.jexl3.parser.ASTIfStatement; +import org.apache.commons.jexl3.parser.ASTJexlScript; +import org.apache.commons.jexl3.parser.ASTJxltLiteral; +import org.apache.commons.jexl3.parser.ASTLENode; +import org.apache.commons.jexl3.parser.ASTLTNode; +import org.apache.commons.jexl3.parser.ASTMapEntry; +import org.apache.commons.jexl3.parser.ASTMapLiteral; +import org.apache.commons.jexl3.parser.ASTMethodNode; +import org.apache.commons.jexl3.parser.ASTModNode; +import org.apache.commons.jexl3.parser.ASTMulNode; +import org.apache.commons.jexl3.parser.ASTNENode; +import org.apache.commons.jexl3.parser.ASTNEWNode; +import org.apache.commons.jexl3.parser.ASTNRNode; +import org.apache.commons.jexl3.parser.ASTNSWNode; +import org.apache.commons.jexl3.parser.ASTNotNode; +import org.apache.commons.jexl3.parser.ASTNullLiteral; +import org.apache.commons.jexl3.parser.ASTNullpNode; +import org.apache.commons.jexl3.parser.ASTNumberLiteral; +import org.apache.commons.jexl3.parser.ASTOrNode; +import org.apache.commons.jexl3.parser.ASTRangeNode; +import org.apache.commons.jexl3.parser.ASTReference; +import org.apache.commons.jexl3.parser.ASTReferenceExpression; +import org.apache.commons.jexl3.parser.ASTReturnStatement; +import org.apache.commons.jexl3.parser.ASTSWNode; +import org.apache.commons.jexl3.parser.ASTSetAddNode; +import org.apache.commons.jexl3.parser.ASTSetAndNode; +import org.apache.commons.jexl3.parser.ASTSetDivNode; +import org.apache.commons.jexl3.parser.ASTSetLiteral; +import org.apache.commons.jexl3.parser.ASTSetModNode; +import org.apache.commons.jexl3.parser.ASTSetMultNode; +import org.apache.commons.jexl3.parser.ASTSetOrNode; +import org.apache.commons.jexl3.parser.ASTSetSubNode; +import org.apache.commons.jexl3.parser.ASTSetXorNode; +import org.apache.commons.jexl3.parser.ASTSizeFunction; +import org.apache.commons.jexl3.parser.ASTSizeMethod; +import org.apache.commons.jexl3.parser.ASTStringLiteral; +import org.apache.commons.jexl3.parser.ASTSubNode; +import org.apache.commons.jexl3.parser.ASTTernaryNode; +import org.apache.commons.jexl3.parser.ASTTrueNode; +import org.apache.commons.jexl3.parser.ASTUnaryMinusNode; +import org.apache.commons.jexl3.parser.ASTVar; +import org.apache.commons.jexl3.parser.ASTWhileStatement; +import org.apache.commons.jexl3.parser.JexlNode; +import org.apache.commons.jexl3.parser.ParserVisitor; + +/** + * Fully abstract to avoid public interface exposition. + */ +public class ScriptVisitor extends ParserVisitor { + /** + * Visits all AST constituents of a JEXL expression. + * @param jscript the expression + * @param data some data context + * @return the visit result or null if jscript was not a Script implementation + */ + public Object visitExpression (JexlExpression jscript, Object data) { + if (jscript instanceof Script) { + return ((Script) jscript).getScript().jjtAccept(this, data); + } + return null; + } + + /** + * Visits all AST constituents of a JEXL script. + * @param jscript the expression + * @param data some data context + * @return the visit result or null if jscript was not a Script implementation + */ + public Object visitScript(JexlScript jscript, Object data) { + if (jscript instanceof Script) { + return ((Script) jscript).getScript().jjtAccept(this, data); + } + return null; + } + + /** + * Visits a node. + * Default implementation visits all its children. + * @param node the node to visit + * @param data visitor pattern argument + * @return visitor pattern value + */ + protected Object visitNode(JexlNode node, Object data) { + return node.childrenAccept(this, data); + } + + @Override + protected Object visit(ASTJexlScript node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTBlock node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTIfStatement node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTWhileStatement node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTContinue node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTBreak node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTForeachStatement node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTReturnStatement node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTAssignment node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTVar node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTReference node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTTernaryNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTNullpNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTOrNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTAndNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTBitwiseOrNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTBitwiseXorNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTBitwiseAndNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTEQNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTNENode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTLTNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTGTNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTLENode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTGENode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTERNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTNRNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSWNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTNSWNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTEWNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTNEWNode node, Object data) { + + return visitNode(node, data); } + + @Override + protected Object visit(ASTAddNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSubNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTMulNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTDivNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTModNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTUnaryMinusNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTBitwiseComplNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTNotNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTIdentifier node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTNullLiteral node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTTrueNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTFalseNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTNumberLiteral node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTStringLiteral node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetLiteral node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTExtendedLiteral node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTArrayLiteral node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTRangeNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTMapLiteral node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTMapEntry node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTEmptyFunction node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTEmptyMethod node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSizeFunction node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTFunctionNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTMethodNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSizeMethod node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTConstructorNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTArrayAccess node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTIdentifierAccess node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTArguments node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTReferenceExpression node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetAddNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetSubNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetMultNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetDivNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetModNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetAndNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetOrNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTSetXorNode node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTJxltLiteral node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTAnnotation node, Object data) { + return visitNode(node, data); + } + + @Override + protected Object visit(ASTAnnotatedStatement node, Object data) { + return visitNode(node, data); + } +} diff --git a/src/main/java/org/apache/commons/jexl3/internal/SoftCache.java b/src/main/java/org/apache/commons/jexl3/internal/SoftCache.java new file mode 100644 index 000000000..5328cab52 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/internal/SoftCache.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3.internal; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * A soft referenced cache. + *

        + * The actual cache is held through a soft reference, allowing it to be GCed + * under memory pressure.

        + * + * @param the cache key entry type + * @param the cache key value type + */ +public class SoftCache { + /** + * The default cache load factor. + */ + private static final float LOAD_FACTOR = 0.75f; + /** + * The cache size. + */ + private final int size; + /** + * The soft reference to the cache map. + */ + private SoftReference> ref = null; + /** + * The cache r/w lock. + */ + private final ReadWriteLock lock; + + /** + * Creates a new instance of a soft cache. + * + * @param theSize the cache size + */ + SoftCache(int theSize) { + size = theSize; + lock = new ReentrantReadWriteLock(); + } + + /** + * Returns the cache size. + * + * @return the cache size + */ + public int size() { + return size; + } + + /** + * Clears the cache. + */ + public void clear() { + lock.writeLock().lock(); + try { + ref = null; + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Gets a value from cache. + * + * @param key the cache entry key + * @return the cache entry value + */ + public V get(K key) { + lock.readLock().lock(); + try { + final Map map = ref != null ? ref.get() : null; + return map != null ? map.get(key) : null; + } finally { + lock.readLock().unlock(); + } + } + + /** + * Puts a value in cache. + * + * @param key the cache entry key + * @param script the cache entry value + */ + public void put(K key, V script) { + lock.writeLock().lock(); + try { + Map map = ref != null ? ref.get() : null; + if (map == null) { + map = createCache(size); + ref = new SoftReference>(map); + } + map.put(key, script); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Produces the cache entry set. + *

        + * For testing only, perform deep copy of cache entries + * + * @return the cache entry list + */ + public List> entries() { + lock.readLock().lock(); + try { + Map map = ref != null ? ref.get() : null; + if (map == null) { + return Collections.emptyList(); + } + final Set> set = map.entrySet(); + final List> entries = new ArrayList>(set.size()); + for (Map.Entry e : set) { + entries.add(new SoftCacheEntry(e)); + } + return entries; + } finally { + lock.readLock().unlock(); + } + } + + /** + * Creates the cache store. + * + * @param the key type + * @param the value type + * @param cacheSize the cache size, must be > 0 + * @return a Map usable as a cache bounded to the given size + */ + public Map createCache(final int cacheSize) { + return new java.util.LinkedHashMap(cacheSize, LOAD_FACTOR, true) { + /** + * Serial version UID. + */ + private static final long serialVersionUID = 1L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return super.size() > cacheSize; + } + }; + } +} + +/** + * A soft cache entry. + * + * @param key type + * @param value type + */ +class SoftCacheEntry implements Map.Entry { + /** + * The entry key. + */ + private final K key; + /** + * The entry value. + */ + private final V value; + + /** + * Creates an entry clone. + * + * @param e the entry to clone + */ + SoftCacheEntry(Map.Entry e) { + key = e.getKey(); + value = e.getValue(); + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V v) { + throw new UnsupportedOperationException("Not supported."); + } +} + diff --git a/src/main/java/org/apache/commons/jexl3/internal/Source.java b/src/main/java/org/apache/commons/jexl3/internal/Source.java new file mode 100644 index 000000000..f79b06c49 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/internal/Source.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3.internal; + +import org.apache.commons.jexl3.JexlFeatures; + +/** + * Maintains the set of allowed features associated with a script/expression source. + *

        This is meant for caching scripts using their 'source' as key but still distinguishing + * scripts with different features and prevent false sharing. + */ +public final class Source { + /** The hash code, pre-computed for fast op. */ + private final int hashCode; + /** The set of features. */ + private final JexlFeatures features; + /** The actual source script/expression. */ + private final String str; + + /** + * Default constructor. + * @param theFeatures the features + * @param theStr the script source + */ + Source(JexlFeatures theFeatures, String theStr) { // CSOFF: MagicNumber + this.features = theFeatures; + this.str = theStr; + int hash = 3; + hash = 37 * hash + features.hashCode(); + hash = 37 * hash + str.hashCode() ; + this.hashCode = hash; + } + + /** + * @return the length of the script source + */ + int length() { + return str.length(); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Source other = (Source) obj; + if (this.features != other.features + && (this.features == null || !this.features.equals(other.features))) { + return false; + } + if ((this.str == null) ? (other.str != null) : !this.str.equals(other.str)) { + return false; + } + return true; + } + + @Override + public String toString() { + return str; + } + + /** + * @return the features associated with the source + */ + public JexlFeatures getFeatures() { + return features; + } + +} diff --git a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java index b117bed47..da0516192 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java @@ -41,7 +41,7 @@ */ public final class TemplateEngine extends JxltEngine { /** The TemplateExpression cache. */ - private final Engine.SoftCache cache; + private final SoftCache cache; /** The JEXL engine instance. */ private final Engine jexl; /** The first character for immediate expressions. */ @@ -61,7 +61,7 @@ public final class TemplateEngine extends JxltEngine { */ public TemplateEngine(Engine aJexl, boolean noScript, int cacheSize, char immediate, char deferred) { this.jexl = aJexl; - this.cache = aJexl.new SoftCache(cacheSize); + this.cache = new SoftCache(cacheSize); immediateChar = immediate; deferredChar = deferred; noscript = noScript; @@ -86,7 +86,7 @@ char getDeferredChar() { * Each instance carries a counter index per (composite sub-) template expression type. * @see ExpressionBuilder */ - static enum ExpressionType { + enum ExpressionType { /** Constant TemplateExpression, count index 0. */ CONSTANT(0), /** Immediate TemplateExpression, count index 1. */ @@ -540,7 +540,7 @@ ExpressionType getType() { @Override protected TemplateExpression prepare(Interpreter interpreter) { String value = interpreter.interpret(node).toString(); - JexlNode dnode = jexl.parse(jexl.isDebug() ? node.jexlInfo() : null, value, null, false, noscript); + JexlNode dnode = jexl.parse(node.jexlInfo(), noscript, value, null); return new ImmediateExpression(value, dnode, this); } @@ -659,16 +659,10 @@ public JxltEngine.Expression createExpression(JexlInfo info, String expression) Exception xuel = null; TemplateExpression stmt = null; try { - if (cache == null) { + stmt = cache.get(expression); + if (stmt == null) { stmt = parseExpression(info, expression, null); - } else { - synchronized (cache) { - stmt = cache.get(expression); - if (stmt == null) { - stmt = parseExpression(info, expression, null); - cache.put(expression, stmt); - } - } + cache.put(expression, stmt); } } catch (JexlException xjexl) { xuel = new Exception(xjexl.getInfo(), "failed to parse '" + expression + "'", xjexl); @@ -712,7 +706,7 @@ static Exception createException(JexlInfo info, String action, TemplateExpressio } /** The different parsing states. */ - private static enum ParseState { + private enum ParseState { /** Parsing a constant. */ CONST, /** Parsing after $ . */ @@ -806,7 +800,7 @@ TemplateExpression parseExpression(JexlInfo info, String expr, Scope scope) { / String src = strb.toString(); TemplateExpression iexpr = new ImmediateExpression( src, - jexl.parse(info.at(lineno, column), src, scope, false, noscript), + jexl.parse(info.at(lineno, column), noscript, src, scope), null); builder.add(iexpr); strb.delete(0, Integer.MAX_VALUE); @@ -854,12 +848,12 @@ TemplateExpression parseExpression(JexlInfo info, String expr, Scope scope) { / if (nested) { dexpr = new NestedExpression( expr.substring(inested, column + 1), - jexl.parse(info.at(lineno, column), src, scope, false, noscript), + jexl.parse(info.at(lineno, column), noscript, src, scope), null); } else { dexpr = new DeferredExpression( strb.toString(), - jexl.parse(info.at(lineno, column), src, scope, false, noscript), + jexl.parse(info.at(lineno, column), noscript, src, scope), null); } builder.add(dexpr); @@ -902,11 +896,11 @@ TemplateExpression parseExpression(JexlInfo info, String expr, Scope scope) { / /** * The enum capturing the difference between verbatim and code source fragments. */ - static enum BlockType { + enum BlockType { /** Block is to be output "as is" but may be a unified expression. */ VERBATIM, /** Block is a directive, ie a fragment of JEXL code. */ - DIRECTIVE; + DIRECTIVE } /** diff --git a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java index 96e3c7ed8..321d2b8c1 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java @@ -27,6 +27,7 @@ import java.io.Writer; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -95,7 +96,7 @@ public TemplateScript(TemplateEngine engine, JexlInfo info, String directive, Re info = jxlt.getEngine().createInfo(); } // allow lambda defining params - script = jxlt.getEngine().parse(info.at(0, 0), strb.toString(), scope, false, false).script(); + script = jxlt.getEngine().parse(info.at(0, 0), false, strb.toString(), scope).script(); scope = script.getScope(); // create the exprs using the code frame for those appearing after the first block of code for (int b = 0; b < blocks.size(); ++b) { @@ -160,8 +161,7 @@ public String toString() { public String asString() { StringBuilder strb = new StringBuilder(); int e = 0; - for (int b = 0; b < source.length; ++b) { - Block block = source[b]; + for (Block block : source) { if (block.getType() == BlockType.DIRECTIVE) { strb.append(prefix); } else { @@ -207,4 +207,8 @@ public String[] getParameters() { return script.getParameters(); } + @Override + public Map getPragmas() { + return script.getPragmas(); + } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/AbstractExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/AbstractExecutor.java index 59895efee..fbe5c2b5f 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/AbstractExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/AbstractExecutor.java @@ -89,13 +89,11 @@ protected AbstractExecutor(Class theClass, java.lang.reflect.Method theMethod method = theMethod; } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { return this == obj || (obj instanceof AbstractExecutor && equals((AbstractExecutor) obj)); } - /** {@inheritDoc} */ @Override public int hashCode() { return method.hashCode(); @@ -236,7 +234,6 @@ protected Method(Class c, java.lang.reflect.Method m, MethodKey k) { key = k; } - /** {@inheritDoc} */ @Override public Object getTargetProperty() { return key; diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ArrayListWrapper.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ArrayListWrapper.java index d762b4205..6bc516793 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ArrayListWrapper.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ArrayListWrapper.java @@ -43,13 +43,11 @@ public ArrayListWrapper(Object anArray) { this.array = anArray; } - /** {@inheritDoc} */ @Override public Object get(int index) { return Array.get(array, index); } - /** {@inheritDoc} */ @Override public Object set(int index, Object element) { Object old = Array.get(array, index); @@ -57,7 +55,6 @@ public Object set(int index, Object element) { return old; } - /** {@inheritDoc} */ @Override public int size() { return Array.getLength(array); diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/BooleanGetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/BooleanGetExecutor.java index 370097b2b..4369e69ad 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/BooleanGetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/BooleanGetExecutor.java @@ -35,12 +35,13 @@ public final class BooleanGetExecutor extends AbstractExecutor.Get { * @return the executor if found, null otherwise */ public static BooleanGetExecutor discover(Introspector is, final Class clazz, String property) { - java.lang.reflect.Method m = PropertyGetExecutor.discoverGet(is, "is", clazz, property); - if (m != null && (m.getReturnType() == Boolean.TYPE || m.getReturnType() == Boolean.class)) { - return new BooleanGetExecutor(clazz, m, property); - } else { - return null; + if (property != null && !property.isEmpty()) { + java.lang.reflect.Method m = PropertyGetExecutor.discoverGet(is, "is", clazz, property); + if (m != null && (m.getReturnType() == Boolean.TYPE || m.getReturnType() == Boolean.class)) { + return new BooleanGetExecutor(clazz, m, property); + } } + return null; } /** diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java index 5a3de9a88..dd91c891f 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java @@ -63,12 +63,12 @@ public static Method cacheMiss() { * This is the cache to store and look up the method information. *

        * It stores the association between: - * - a key made of a method name and an array of argument types. - * - a method. + * - a key made of a method name and an array of argument types. + * - a method. *

        *

        * Since the invocation of the associated method is dynamic, there is no need (nor way) to differentiate between - * foo(int,int) and foo(Integer,Integer) since in practise, only the latter form will be used through a call. + * foo(int,int) and foo(Integer,Integer) since in practice only the latter form will be used through a call. * This of course, applies to all 8 primitive types. *

        * Uses ConcurrentMap since 3.0, marginally faster than 2.1 under contention. @@ -87,18 +87,19 @@ public static Method cacheMiss() { * Standard constructor. * * @param aClass the class to deconstruct. - * @param log the logger. + * @param permissions the permissions to apply during introspection + * @param log the logger. */ @SuppressWarnings("LeakingThisInConstructor") - ClassMap(Class aClass, Log log) { + ClassMap(Class aClass, Permissions permissions, Log log) { // eagerly cache methods - create(this, aClass, log); + create(this, permissions, aClass, log); // eagerly cache public fields Field[] fields = aClass.getFields(); if (fields.length > 0) { Map cache = new HashMap(); for (Field field : fields) { - if (Modifier.isPublic(field.getModifiers()) && Permissions.allow(field)) { + if (permissions.allow(field)) { cache.put(field.getName(), field); } } @@ -149,16 +150,16 @@ Method[] getMethods(final String methodName) { /** * Find a Method using the method name and parameter objects. - *

        - * Look in the methodMap for an entry. If found, + *

        + * Look in the methodMap for an entry. If found, * it'll either be a CACHE_MISS, in which case we * simply give up, or it'll be a Method, in which * case, we return it. - *

        + *

        *

        * If nothing is found, then we must actually go * and introspect the method from the MethodMap. - *

        + *

        * @param methodKey the method key * @return A Method object representing the method to invoke or null. * @throws MethodKey.AmbiguousException When more than one method is a match for the parameters. @@ -195,11 +196,12 @@ Method getMethod(final MethodKey methodKey) throws MethodKey.AmbiguousException /** * Populate the Map of direct hits. These are taken from all the public methods * that our class, its parents and their implemented interfaces provide. - * @param cache the ClassMap instance we create + * @param cache the ClassMap instance we create + * @param permissions the permissions to apply during introspection * @param classToReflect the class to cache - * @param log the Log + * @param log the Log */ - private static void create(ClassMap cache, Class classToReflect, Log log) { + private static void create(ClassMap cache, Permissions permissions, Class classToReflect, Log log) { // // Build a list of all elements in the class hierarchy. This one is bottom-first (i.e. we start // with the actual declaring class and its interfaces and then move up (superclass etc.) until we @@ -210,11 +212,11 @@ private static void create(ClassMap cache, Class classToReflect, Log log) { // for (; classToReflect != null; classToReflect = classToReflect.getSuperclass()) { if (Modifier.isPublic(classToReflect.getModifiers())) { - populateWithClass(cache, classToReflect, log); + populateWithClass(cache, permissions, classToReflect, log); } Class[] interfaces = classToReflect.getInterfaces(); for (int i = 0; i < interfaces.length; i++) { - populateWithInterface(cache, interfaces[i], log); + populateWithInterface(cache, permissions, interfaces[i], log); } } // now that we've got all methods keyed in, lets organize them by name @@ -253,33 +255,40 @@ public int compare(Method o1, Method o2) { /** * Recurses up interface hierarchy to get all super interfaces. * @param cache the cache to fill + * @param permissions the permissions to apply during introspection * @param iface the interface to populate the cache from - * @param log the Log + * @param log the Log */ - private static void populateWithInterface(ClassMap cache, Class iface, Log log) { + private static void populateWithInterface(ClassMap cache, Permissions permissions, Class iface, Log log) { if (Modifier.isPublic(iface.getModifiers())) { - populateWithClass(cache, iface, log); - } - Class[] supers = iface.getInterfaces(); - for (int i = 0; i < supers.length; i++) { - populateWithInterface(cache, supers[i], log); + populateWithClass(cache, permissions, iface, log); + Class[] supers = iface.getInterfaces(); + for (int i = 0; i < supers.length; i++) { + populateWithInterface(cache, permissions, supers[i], log); + } } } /** * Recurses up class hierarchy to get all super classes. * @param cache the cache to fill + * @param permissions the permissions to apply during introspection * @param clazz the class to populate the cache from - * @param log the Log + * @param log the Log */ - private static void populateWithClass(ClassMap cache, Class clazz, Log log) { + private static void populateWithClass(ClassMap cache, Permissions permissions, Class clazz, Log log) { try { Method[] methods = clazz.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { Method mi = methods[i]; - if (Modifier.isPublic(mi.getModifiers()) && Permissions.allow(mi)) { + if (permissions.allow(mi)) { // add method to byKey cache; do not override - cache.byKey.putIfAbsent(new MethodKey(mi), mi); + MethodKey key = new MethodKey(mi); + Method pmi = cache.byKey.putIfAbsent(key, mi); + if (pmi != null && log.isDebugEnabled() && !key.equals(new MethodKey(pmi))) { + // foo(int) and foo(Integer) have the same signature for JEXL + log.debug("Method "+ pmi + " is already registered, key: " + key.debugString()); + } } } } catch (SecurityException se) { diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckGetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckGetExecutor.java index 19fb5d990..04b6c06ce 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckGetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckGetExecutor.java @@ -30,7 +30,7 @@ * @since 2.0 */ public final class DuckGetExecutor extends AbstractExecutor.Get { - /** The property. */ + /** The property, may be null. */ private final Object property; /** @@ -69,10 +69,12 @@ public Object invoke(Object obj) throws IllegalAccessException, InvocationTarget @Override public Object tryInvoke(Object obj, Object key) { - if (obj != null && method != null - // ensure method name matches the property name - && property.equals(key) - && objectClass.equals(obj.getClass())) { + if (obj != null + && objectClass.equals(obj.getClass()) + // ensure method name matches the property name + && method != null + && ((property == null && key == null) + || (property != null && property.equals(key)))) { try { Object[] args = {property}; return method.invoke(obj, args); diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckSetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckSetExecutor.java index af898f73f..c1da6f5e4 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckSetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckSetExecutor.java @@ -36,7 +36,7 @@ * @since 2.0 */ public final class DuckSetExecutor extends AbstractExecutor.Set { - /** The property. */ + /** The property, may be null. */ private final Object property; /** @@ -83,10 +83,11 @@ public Object invoke(Object obj, Object value) throws IllegalAccessException, In @Override public Object tryInvoke(Object obj, Object key, Object value) { - if (obj != null && method != null - // ensure method name matches the property name - && property.equals(key) - && objectClass.equals(obj.getClass())) { + if (obj != null + && objectClass.equals(obj.getClass()) + && method != null + && ((property != null && property.equals(key)) + || (property == null && key == null))) { try { Object[] args = {property, value}; method.invoke(obj, args); diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/IndexedType.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/IndexedType.java index a5a0fe5df..5efda2838 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/IndexedType.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/IndexedType.java @@ -24,18 +24,23 @@ /** * Abstract an indexed property container. - * This stores the container name and owning class as well as the list of available getter and setter methods. + *

        This allows getting properties from expressions like var.container.property. + * This stores the container name and class as well as the list of available getter and setter methods. * It implements JexlPropertyGet since such a container can only be accessed from its owning instance (not set). */ public final class IndexedType implements JexlPropertyGet { - /** The container name. */ + /** The container name. */ private final String container; - /** The owning class. */ + /** The container class. */ private final Class clazz; /** The array of getter methods. */ private final Method[] getters; + /** Last get method used. */ + private volatile Method get = null; /** The array of setter methods. */ private final Method[] setters; + /** Last set method used. */ + private volatile Method set = null; /** * Attempts to find an indexed-property getter in an object. @@ -46,10 +51,10 @@ public final class IndexedType implements JexlPropertyGet { * @param is the introspector * @param object the object * @param name the container name - * @return a JexlPropertyGet is successfull, null otherwise + * @return a JexlPropertyGet is successful, null otherwise */ public static JexlPropertyGet discover(Introspector is, Object object, String name) { - if (object != null && name != null) { + if (object != null && name != null && !name.isEmpty()) { String base = name.substring(0, 1).toUpperCase() + name.substring(1); final String container = name; final Class clazz = object.getClass(); @@ -63,50 +68,66 @@ public static JexlPropertyGet discover(Introspector is, Object object, String na } /** - * A generic indexed property container, exposes get(key) and set(key, value) and solves method call dynamically - * based on arguments. + * A generic indexed property container, exposes get(key) and set(key, value) + * and solves method call dynamically based on arguments. *

        Must remain public for introspection purpose.

        */ public static final class IndexedContainer { - /** The instance owning the container. */ - private final Object object; + /** The container instance. */ + private final Object container; /** The container type instance. */ private final IndexedType type; /** * Creates a new duck container. * @param theType the container type - * @param theObject the instance owning the container + * @param theContainer the container instance */ - private IndexedContainer(IndexedType theType, Object theObject) { + private IndexedContainer(IndexedType theType, Object theContainer) { this.type = theType; - this.object = theObject; + this.container = theContainer; + } + + /** + * Gets the property container name. + * @return the container name + */ + public String getContainerName() { + return type.container; } /** - * Gets a property from a container. + * Gets the property container class. + * @return the container class + */ + public Class getContainerClass() { + return type.clazz; + } + + /** + * Gets a property from this indexed container. * @param key the property key * @return the property value * @throws Exception if inner invocation fails */ public Object get(Object key) throws Exception { - return type.invokeGet(object, key); + return type.invokeGet(container, key); } /** - * Sets a property in a container. + * Sets a property in this indexed container. * @param key the property key * @param value the property value * @return the invocation result (frequently null) * @throws Exception if inner invocation fails */ public Object set(Object key, Object value) throws Exception { - return type.invokeSet(object, key, value); + return type.invokeSet(container, key, value); } } /** - * Creates a new indexed type. + * Creates a new indexed property container type. * @param name the container name * @param c the owning class * @param gets the array of getter methods @@ -130,7 +151,9 @@ public Object invoke(Object obj) throws Exception { @Override public Object tryInvoke(Object obj, Object key) { - if (obj != null && key != null && clazz.equals(obj.getClass()) && container.equals(key.toString())) { + if (obj != null && key != null + && clazz.equals(obj.getClass()) + && container.equals(key.toString())) { return new IndexedContainer(this, obj); } else { return Uberspect.TRY_FAILED; @@ -149,49 +172,69 @@ public boolean isCacheable() { /** * Gets the value of a property from a container. - * @param object the instance owning the container (not null) + * @param object the container instance (not null) * @param key the property key (not null) * @return the property value - * @throws Exception if invocation failed; IntrospectionException if a property getter could not be found + * @throws Exception if invocation failed; + * IntrospectionException if a property getter could not be found */ private Object invokeGet(Object object, Object key) throws Exception { - if (getters != null) { - final Object[] args = {key}; - final Method jm; - if (getters.length == 1) { - jm = getters[0]; - } else { - jm = new MethodKey(getters[0].getName(), args).getMostSpecificMethod(getters); + if (getters != null && getters.length > 0) { + Method jm = get; + if (jm != null) { + final Class[] ptypes = jm.getParameterTypes(); + if (ptypes[0].isAssignableFrom(key.getClass())) { + return jm.invoke(object, key); + } } + final Object[] args = {key}; + final String mname = getters[0].getName(); + final MethodKey km = new MethodKey(mname, args); + jm = km.getMostSpecificMethod(getters); if (jm != null) { - return jm.invoke(object, args); + Object invoked = jm.invoke(object, args); + get = jm; + return invoked; } } - throw new IntrospectionException("property get error: " + object.getClass().toString() + "@" + key.toString()); + throw new IntrospectionException("property get error: " + + object.getClass().toString() + + "@" + key.toString()); } /** * Sets the value of a property in a container. - * @param object the instance owning the container (not null) + * @param object the container instance (not null) * @param key the property key (not null) * @param value the property value (not null) * @return the result of the method invocation (frequently null) - * @throws Exception if invocation failed; IntrospectionException if a property setter could not be found + * @throws Exception if invocation failed; + * IntrospectionException if a property setter could not be found */ private Object invokeSet(Object object, Object key, Object value) throws Exception { - if (setters != null) { - final Object[] args = {key, value}; - final Method jm; - if (setters.length == 1) { - jm = setters[0]; - } else { - jm = new MethodKey(setters[0].getName(), args).getMostSpecificMethod(setters); + if (setters != null && setters.length > 0) { + Method jm = set; + if (jm != null) { + final Class[] ptypes = jm.getParameterTypes(); + if (ptypes[0].isAssignableFrom(key.getClass()) + && (value == null + || ptypes[1].isAssignableFrom(value.getClass()))) { + return jm.invoke(object, key, value); + } } + final Object[] args = {key, value}; + final String mname = setters[0].getName(); + final MethodKey km = new MethodKey(mname, args); + jm = km.getMostSpecificMethod(setters); if (jm != null) { - return jm.invoke(object, args); + Object invoked = jm.invoke(object, args); + set = jm; + return invoked; } } - throw new IntrospectionException("property set error: " + object.getClass().toString() + "@" + key.toString()); + throw new IntrospectionException("property set error: " + + object.getClass().toString() + + "@" + key.toString()); } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java index d9a1e8356..e89ef95ed 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java @@ -21,7 +21,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; @@ -67,6 +66,10 @@ public CacheMiss() { * The class loader used to solve constructors if needed. */ private ClassLoader loader; + /** + * The permissions. + */ + private final Permissions permissions; /** * The read/write lock. */ @@ -90,8 +93,19 @@ public CacheMiss() { * @param cloader the class loader */ public Introspector(Log log, ClassLoader cloader) { + this(log, cloader, null); + } + + /** + * Create the introspector. + * @param log the logger to use + * @param cloader the class loader + * @param perms the permissions + */ + public Introspector(Log log, ClassLoader cloader, Permissions perms) { this.rlog = log; - loader = cloader; + this.loader = cloader; + this.permissions = perms != null? perms : Permissions.DEFAULT; } /** @@ -131,8 +145,8 @@ public Method getMethod(Class c, MethodKey key) { try { return getMap(c).getMethod(key); } catch (MethodKey.AmbiguousException xambiguous) { - // whoops. Ambiguous. Make a nice log message and return null... - if (rlog != null && rlog.isInfoEnabled()) { + // whoops. Ambiguous and not benign. Make a nice log message and return null... + if (rlog != null && xambiguous.isSevere() && rlog.isInfoEnabled()) { rlog.info("ambiguous method invocation: " + c.getName() + "." + key.debugString(), xambiguous); @@ -247,7 +261,7 @@ public Constructor getConstructor(final Class c, final MethodKey key) { } List> l = new ArrayList>(); for (Constructor ictor : clazz.getConstructors()) { - if (Modifier.isPublic(ictor.getModifiers()) && Permissions.allow(ictor)) { + if (permissions.allow(ictor)) { l.add(ictor); } } @@ -298,7 +312,7 @@ private ClassMap getMap(Class c) { // try again classMap = classMethodMaps.get(c); if (classMap == null) { - classMap = new ClassMap(c, rlog); + classMap = new ClassMap(c, permissions, rlog); classMethodMaps.put(c, classMap); } } finally { diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ListGetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ListGetExecutor.java index fd2c558ca..956b65c6e 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ListGetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ListGetExecutor.java @@ -72,7 +72,7 @@ public Object getTargetProperty() { @Override public Object invoke(final Object obj) { if (method == ARRAY_GET) { - return java.lang.reflect.Array.get(obj, property.intValue()); + return Array.get(obj, property.intValue()); } else { return ((List) obj).get(property.intValue()); } @@ -85,7 +85,7 @@ public Object tryInvoke(final Object obj, Object identifier) { && objectClass.equals(obj.getClass()) && index != null) { if (method == ARRAY_GET) { - return java.lang.reflect.Array.get(obj, index.intValue()); + return Array.get(obj, index.intValue()); } else { return ((List) obj).get(index.intValue()); } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ListSetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ListSetExecutor.java index 18b1b837d..e41e88777 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ListSetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ListSetExecutor.java @@ -81,7 +81,7 @@ public Object getTargetProperty() { @Override public Object invoke(final Object obj, Object value) { if (method == ARRAY_SET) { - java.lang.reflect.Array.set(obj, property.intValue(), value); + Array.set(obj, property.intValue(), value); } else { @SuppressWarnings("unchecked") // LSE should only be created for array or list types final List list = (List) obj; diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/MapGetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/MapGetExecutor.java index 4cd955531..1799fe5a4 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/MapGetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/MapGetExecutor.java @@ -62,7 +62,6 @@ public Object getTargetProperty() { return property; } - @Override public Object invoke(final Object obj) { @SuppressWarnings("unchecked") // ctor only allows Map instances - see discover() method @@ -72,9 +71,11 @@ public Object invoke(final Object obj) { @Override public Object tryInvoke(final Object obj, Object key) { - if (obj != null && method != null - && objectClass.equals(obj.getClass()) - && (key == null || property.getClass().equals(key.getClass()))) { + if (obj != null + && method != null + && objectClass.equals(obj.getClass()) + && ((property == null && key == null) + || (property != null && key != null && property.getClass().equals(key.getClass())))) { @SuppressWarnings("unchecked") // ctor only allows Map instances - see discover() method final Map map = (Map) obj; return map.get(key); diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/MapSetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/MapSetExecutor.java index d1d25648d..ce439dc0c 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/MapSetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/MapSetExecutor.java @@ -72,9 +72,11 @@ public Object invoke(final Object obj, Object value) throws IllegalAccessExcepti @Override public Object tryInvoke(final Object obj, Object key, Object value) { - if (obj != null && method != null + if (obj != null + && method != null && objectClass.equals(obj.getClass()) - && (key == null || property.getClass().equals(key.getClass()))) { + && ((property == null && key == null) + || (property != null && key != null && property.getClass().equals(key.getClass())))) { @SuppressWarnings("unchecked") // ctor only allows Map instances - see discover() method final Map map = ((Map) obj); map.put(key, value); diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/MethodKey.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/MethodKey.java index a08b40290..b0c3cae77 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/MethodKey.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/MethodKey.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.Map; /** @@ -46,38 +47,7 @@ */ public final class MethodKey { /** The initial size of the primitive conversion map. */ - private static final int PRIMITIVE_SIZE = 13; - /** The primitive type to class conversion map. */ - private static final Map, Class> PRIMITIVE_TYPES; - - static { - PRIMITIVE_TYPES = new HashMap, Class>(PRIMITIVE_SIZE); - PRIMITIVE_TYPES.put(Boolean.TYPE, Boolean.class); - PRIMITIVE_TYPES.put(Byte.TYPE, Byte.class); - PRIMITIVE_TYPES.put(Character.TYPE, Character.class); - PRIMITIVE_TYPES.put(Double.TYPE, Double.class); - PRIMITIVE_TYPES.put(Float.TYPE, Float.class); - PRIMITIVE_TYPES.put(Integer.TYPE, Integer.class); - PRIMITIVE_TYPES.put(Long.TYPE, Long.class); - PRIMITIVE_TYPES.put(Short.TYPE, Short.class); - } - - /** Converts a primitive type to its corresponding class. - *

        - * If the argument type is primitive then we want to convert our - * primitive type signature to the corresponding Object type so - * introspection for methods with primitive types will work - * correctly. - *

        - * @param parm a may-be primitive type class - * @return the equivalent object class - */ - static Class primitiveClass(Class parm) { - // it is marginally faster to get from the map than call isPrimitive... - //if (!parm.isPrimitive()) return parm; - Class prim = PRIMITIVE_TYPES.get(parm); - return prim == null ? parm : prim; - } + private static final int PRIMITIVE_SIZE = 11; /** The hash code. */ private final int hashCode; /** The method name. */ @@ -173,13 +143,11 @@ Class[] getParameters() { return params; } - /** {@inheritDoc} */ @Override public int hashCode() { return hashCode; } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (obj instanceof MethodKey) { @@ -189,7 +157,6 @@ public boolean equals(Object obj) { return false; } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder builder = new StringBuilder(method); @@ -261,7 +228,7 @@ public static boolean isVarArgs(final Method method) { * @throws MethodKey.AmbiguousException if there is more than one. */ public Method getMostSpecificMethod(Method[] methods) { - return METHODS.getMostSpecific(methods, params); + return METHODS.getMostSpecific(this, methods); } /** @@ -271,7 +238,7 @@ public Method getMostSpecificMethod(Method[] methods) { * @throws MethodKey.AmbiguousException if there is more than one. */ public Constructor getMostSpecificConstructor(Constructor[] methods) { - return CONSTRUCTORS.getMostSpecific(methods, params); + return CONSTRUCTORS.getMostSpecific(this, methods); } /** @@ -295,69 +262,7 @@ public Constructor getMostSpecificConstructor(Constructor[] methods) { * the formal type. */ public static boolean isInvocationConvertible(Class formal, Class actual, boolean possibleVarArg) { - /* if it's a null, it means the arg was null */ - if (actual == null && !formal.isPrimitive()) { - return true; - } - - /* Check for identity or widening reference conversion */ - if (actual != null && formal.isAssignableFrom(actual)) { - return true; - } - - /** Catch all... */ - if (formal == Object.class) { - return true; - } - - /* Check for boxing with widening primitive conversion. Note that - * actual parameters are never primitives. */ - if (formal.isPrimitive()) { - if (formal == Boolean.TYPE && actual == Boolean.class) { - return true; - } - if (formal == Character.TYPE && actual == Character.class) { - return true; - } - if (formal == Byte.TYPE && actual == Byte.class) { - return true; - } - if (formal == Short.TYPE - && (actual == Short.class || actual == Byte.class)) { - return true; - } - if (formal == Integer.TYPE - && (actual == Integer.class || actual == Short.class - || actual == Byte.class)) { - return true; - } - if (formal == Long.TYPE - && (actual == Long.class || actual == Integer.class - || actual == Short.class || actual == Byte.class)) { - return true; - } - if (formal == Float.TYPE - && (actual == Float.class || actual == Long.class - || actual == Integer.class || actual == Short.class - || actual == Byte.class)) { - return true; - } - if (formal == Double.TYPE - && (actual == Double.class || actual == Float.class - || actual == Long.class || actual == Integer.class - || actual == Short.class || actual == Byte.class)) { - return true; - } - } - - /* Check for vararg conversion. */ - if (possibleVarArg && formal.isArray()) { - if (actual != null && actual.isArray()) { - actual = actual.getComponentType(); - } - return isInvocationConvertible(formal.getComponentType(), actual, false); - } - return false; + return isInvocationConvertible(formal, actual, false, possibleVarArg); } /** @@ -377,52 +282,124 @@ public static boolean isInvocationConvertible(Class formal, Class actual, * subject to widening conversion to formal. */ public static boolean isStrictInvocationConvertible(Class formal, Class actual, boolean possibleVarArg) { - /* we shouldn't get a null into, but if so */ + return isInvocationConvertible(formal, actual, true, possibleVarArg); + } + + /** Converts a primitive type to its corresponding class. + *

        + * If the argument type is primitive then we want to convert our + * primitive type signature to the corresponding Object type so + * introspection for methods with primitive types will work + * correctly. + *

        + * @param parm a may-be primitive type class + * @return the equivalent object class + */ + static Class primitiveClass(Class parm) { + // it was marginally faster to get from the map than call isPrimitive... + //if (!parm.isPrimitive()) return parm; + Class[] prim = CONVERTIBLES.get(parm); + return prim == null ? parm : prim[0]; + } + + /** + * Helper to build class arrays. + * @param args the classes + * @return the array + */ + private static Class[] asArray(Class... args) { + return args; + } + + /** + * Maps from primitive types to invocation compatible classes. + *

        Considering the key as a parameter type, the value is the list of argument classes that are invocation + * compatible with the parameter. Example is Long is invocation convertible to long. + */ + private static final Map, Class[]> CONVERTIBLES; + static { + CONVERTIBLES = new HashMap, Class[]>(PRIMITIVE_SIZE); + CONVERTIBLES.put(Boolean.TYPE, + asArray(Boolean.class)); + CONVERTIBLES.put(Character.TYPE, + asArray(Character.class)); + CONVERTIBLES.put(Byte.TYPE, + asArray(Byte.class)); + CONVERTIBLES.put(Short.TYPE, + asArray(Short.class, Byte.class)); + CONVERTIBLES.put(Integer.TYPE, + asArray(Integer.class, Short.class, Byte.class)); + CONVERTIBLES.put(Long.TYPE, + asArray(Long.class, Integer.class, Short.class, Byte.class)); + CONVERTIBLES.put(Float.TYPE, + asArray(Float.class, Long.class, Integer.class, Short.class, Byte.class)); + CONVERTIBLES.put(Double.TYPE, + asArray(Double.class, Float.class, Long.class, Integer.class, Short.class, Byte.class)); + } + + /** + * Maps from primitive types to invocation compatible primitive types. + *

        Considering the key as a parameter type, the value is the list of argument types that are invocation + * compatible with the parameter. Example is 'int' is invocation convertible to 'long'. + */ + private static final Map, Class[]> STRICT_CONVERTIBLES; + static { + STRICT_CONVERTIBLES = new HashMap, Class[]>(PRIMITIVE_SIZE); + STRICT_CONVERTIBLES.put(Short.TYPE, + asArray(Byte.TYPE)); + STRICT_CONVERTIBLES.put(Integer.TYPE, + asArray(Short.TYPE, Byte.TYPE)); + STRICT_CONVERTIBLES.put(Long.TYPE, + asArray(Integer.TYPE, Short.TYPE, Byte.TYPE)); + STRICT_CONVERTIBLES.put(Float.TYPE, + asArray(Long.TYPE, Integer.TYPE, Short.TYPE, Byte.TYPE)); + STRICT_CONVERTIBLES.put(Double.TYPE, + asArray(Float.TYPE, Long.TYPE, Integer.TYPE, Short.TYPE, Byte.TYPE)); + } + + /** + * Determines parameter-argument invocation compatibility. + * + * @param formal the formal parameter type + * @param actual the argument type + * @param strict whether the check is strict or not + * @param possibleVarArg whether or not we're dealing with the last parameter in the method declaration + * @return true if compatible, false otherwise + */ + private static boolean isInvocationConvertible( + Class formal, Class actual, boolean strict, boolean possibleVarArg) { + /* if it's a null, it means the arg was null */ if (actual == null && !formal.isPrimitive()) { return true; } - - /* Check for identity or widening reference conversion */ - if (formal.isAssignableFrom(actual) && (actual != null && formal.isArray() == actual.isArray())) { + /* system asssignable, both sides must be array or not */ + if (actual != null && formal.isAssignableFrom(actual) && actual.isArray() == formal.isArray()) { return true; } - - /* Check for widening primitive conversion. */ + /** catch all... */ + if (!strict && formal == Object.class) { + return true; + } + /* Primitive conversion check. */ if (formal.isPrimitive()) { - if (formal == Short.TYPE && (actual == Byte.TYPE)) { - return true; - } - if (formal == Integer.TYPE - && (actual == Short.TYPE || actual == Byte.TYPE)) { - return true; - } - if (formal == Long.TYPE - && (actual == Integer.TYPE || actual == Short.TYPE - || actual == Byte.TYPE)) { - return true; - } - if (formal == Float.TYPE - && (actual == Long.TYPE || actual == Integer.TYPE - || actual == Short.TYPE || actual == Byte.TYPE)) { - return true; - } - if (formal == Double.TYPE - && (actual == Float.TYPE || actual == Long.TYPE - || actual == Integer.TYPE || actual == Short.TYPE - || actual == Byte.TYPE)) { - return true; + Class[] clist = strict ? STRICT_CONVERTIBLES.get(formal) : CONVERTIBLES.get(formal); + for(int c = 0; c < clist.length; ++c) { + if (actual == clist[c]) { + return true; + } } + return false; } - /* Check for vararg conversion. */ if (possibleVarArg && formal.isArray()) { if (actual != null && actual.isArray()) { actual = actual.getComponentType(); } - return isStrictInvocationConvertible(formal.getComponentType(), actual, false); + return isInvocationConvertible(formal.getComponentType(), actual, strict, false); } return false; } + /** * whether a method/ctor is more specific than a previously compared one. */ @@ -442,10 +419,28 @@ public static boolean isStrictInvocationConvertible(Class formal, Class ac * by the introspector. */ public static class AmbiguousException extends RuntimeException { + /** Version Id for serializable. */ + private static final long serialVersionUID = -201801091655L; + /** Whether this exception should be considered severe. */ + private final boolean severe; + /** - * Version Id for serializable. + * A severe or not ambiguous exception. + * @param flag logging flag */ - private static final long serialVersionUID = -2314636505414551664L; + AmbiguousException(boolean flag) { + this.severe = flag; + } + + /** + * Whether this exception is considered severe or benign. + *

        Note that this is meant in the context of an ambiguous exception; benign cases can only be triggered + * by null arguments often related to runtime problems (not simply on overload signatures). + * @return true if severe, false if benign. + */ + public boolean isSevere() { + return severe; + } } /** @@ -482,14 +477,14 @@ private abstract static class Parameters { * like this is needed. *

        * - * @param methods a list of methods. - * @param classes list of argument types. + * @param key a method key, esp its parameters + * @param methods a list of methods * @return the most specific method. * @throws MethodKey.AmbiguousException if there is more than one. */ - private T getMostSpecific(T[] methods, Class[] classes) { - LinkedList applicables = getApplicables(methods, classes); - + private T getMostSpecific(MethodKey key, T[] methods) { + final Class[] args = key.params; + LinkedList applicables = getApplicables(methods, args); if (applicables.isEmpty()) { return null; } @@ -505,12 +500,12 @@ private T getMostSpecific(T[] methods, Class[] classes) { */ LinkedList maximals = new LinkedList(); for (T app : applicables) { - Class[] appArgs = getParameterTypes(app); + final Class[] parms = getParameterTypes(app); boolean lessSpecific = false; Iterator maximal = maximals.iterator(); while(!lessSpecific && maximal.hasNext()) { T max = maximal.next(); - switch (moreSpecific(appArgs, getParameterTypes(max))) { + switch (moreSpecific(args, parms, getParameterTypes(max))) { case MORE_SPECIFIC: /* * This method is more specific than the previously @@ -538,23 +533,69 @@ private T getMostSpecific(T[] methods, Class[] classes) { maximals.addLast(app); } } + // if we have more than one maximally specific method, this call is ambiguous... if (maximals.size() > 1) { - // We have more than one maximally specific method - throw new AmbiguousException(); + throw ambiguousException(args, applicables); } return maximals.getFirst(); } // CSON: RedundantThrows + /** + * Creates an ambiguous exception. + *

        + * This method computes the severity of the ambiguity. The only non-severe case is when there is + * at least one null argument and at most one applicable method or constructor has a corresponding 'Object' + * parameter. + * We thus consider that ambiguity is benign in presence of null arguments but in the case where + * the corresponding parameter is of type Object in more than one applicable overloads. + *

        + * Rephrasing: + *

          + *
        • If all arguments are valid instances - no null argument -, ambiguity is severe.
        • + *
        • If there is at least one null argument, the ambiguity is severe if more than one method has a + * corresponding parameter of class 'Object'.
        • + *
        + * + * @param classes the argument args + * @param applicables the list of applicable methods or constructors + * @return an ambiguous exception + */ + private AmbiguousException ambiguousException (Class[] classes, List applicables) { + boolean severe = false; + int instanceArgCount = 0; // count the number of valid instances, aka not null + for(int c = 0; c < classes.length; ++c) { + Class argClazz = classes[c]; + if (Void.class.equals(argClazz)) { + // count the number of methods for which the current arg maps to an Object parameter + int objectParmCount = 0; + for (T app : applicables) { + Class[] parmClasses = getParameterTypes(app); + Class parmClass = parmClasses[c]; + if (Object.class.equals(parmClass)) { + if (objectParmCount++ == 2) { + severe = true; + break; + } + } + } + } else { + instanceArgCount += 1; + } + } + return new AmbiguousException(severe || instanceArgCount == classes.length); + } + /** * Determines which method signature (represented by a class array) is more * specific. This defines a partial ordering on the method signatures. * - * @param c1 first signature to compare - * @param c2 second signature to compare + * @param a the arguments signature + * @param c1 first method signature to compare + * @param c2 second method signature to compare * @return MORE_SPECIFIC if c1 is more specific than c2, LESS_SPECIFIC if * c1 is less specific than c2, INCOMPARABLE if they are incomparable. */ - private int moreSpecific(Class[] c1, Class[] c2) { + private int moreSpecific(final Class[] a, final Class[] c1, final Class[] c2) { boolean c1MoreSpecific = false; boolean c2MoreSpecific = false; @@ -575,6 +616,16 @@ private int moreSpecific(Class[] c1, Class[] c2) { for (int i = 0; i < length; ++i) { if (c1[i] != c2[i]) { boolean last = (i == ultimate); + if (a[i] == Void.class) { + if (c1[i] == Object.class && c2[i] != Object.class) { + c1MoreSpecific = true; + continue; + } + if (c1[i] != Object.class && c2[i] == Object.class) { + c2MoreSpecific = true; + continue; + } + } c1MoreSpecific = c1MoreSpecific || isStrictConvertible(c2[i], c1[i], last); c2MoreSpecific = c2MoreSpecific || isStrictConvertible(c1[i], c2[i], last); } @@ -604,7 +655,8 @@ private int moreSpecific(Class[] c1, Class[] c2) { } if (primDiff > 0) { return MORE_SPECIFIC; - } else if (primDiff < 0) { + } + if (primDiff < 0) { return LESS_SPECIFIC; } /* @@ -753,6 +805,7 @@ private boolean isStrictConvertible(Class formal, Class actual, return isStrictInvocationConvertible(formal, actual.equals(Void.class) ? null : actual, possibleVarArg); } } + /** * The parameter matching service for methods. */ @@ -766,7 +819,9 @@ protected Class[] getParameterTypes(Method app) { public boolean isVarArgs(Method app) { return MethodKey.isVarArgs(app); } + }; + /** * The parameter matching service for constructors. */ @@ -780,5 +835,6 @@ protected Class[] getParameterTypes(Constructor app) { public boolean isVarArgs(Constructor app) { return app.isVarArgs(); } + }; } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java index 19073a3ec..c79e7e0f1 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java @@ -20,25 +20,42 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import org.apache.commons.jexl3.annotations.NoJexl; /** - * Checks whether an element (ctor, field or method) is visible by JEXL introspection - * by checking if has been annotated with NoJexl. + * Checks whether an element (ctor, field or method) is visible by JEXL introspection. + * Default implementation does this by checking if element has been annotated with NoJexl. */ public class Permissions { /** Make non instantiable. */ private Permissions() { } + /** + * The default singleton. + */ + public static final Permissions DEFAULT = new Permissions(); /** - * Checks whether a class or one of its superclasses or implemented interfaces + * Checks whether a package explicitly disallows JEXL introspection. + * @param pack the package + * @return true if JEXL is allowed to introspect, false otherwise + */ + public boolean allow(Package pack) { + if (pack != null && pack.getAnnotation(NoJexl.class) != null) { + return false; + } + return true; + } + + /** + * Checks whether a class or one of its super-classes or implemented interfaces * explicitly disallows JEXL introspection. * @param clazz the class to check * @return true if JEXL is allowed to introspect, false otherwise */ - public static boolean allow(Class clazz) { - return allow(clazz, true); + public boolean allow(Class clazz) { + return clazz != null && allow(clazz.getPackage()) && allow(clazz, true); } /** @@ -46,10 +63,13 @@ public static boolean allow(Class clazz) { * @param ctor the constructor to check * @return true if JEXL is allowed to introspect, false otherwise */ - public static boolean allow(Constructor ctor) { + public boolean allow(Constructor ctor) { if (ctor == null) { return false; } + if (!Modifier.isPublic(ctor.getModifiers())) { + return false; + } Class clazz = ctor.getDeclaringClass(); if (!allow(clazz, false)) { return false; @@ -67,10 +87,13 @@ public static boolean allow(Constructor ctor) { * @param field the field to check * @return true if JEXL is allowed to introspect, false otherwise */ - public static boolean allow(Field field) { + public boolean allow(Field field) { if (field == null) { return false; } + if (!Modifier.isPublic(field.getModifiers())) { + return false; + } Class clazz = field.getDeclaringClass(); if (!allow(clazz, false)) { return false; @@ -85,15 +108,18 @@ public static boolean allow(Field field) { /** * Checks whether a method explicitly disallows JEXL introspection. - *

        Since methods can be overriden, this also checks that no superclass or interface + *

        Since methods can be overridden, this also checks that no superclass or interface * explictly disallows this methods.

        * @param method the method to check * @return true if JEXL is allowed to introspect, false otherwise */ - public static boolean allow(Method method) { + public boolean allow(Method method) { if (method == null) { return false; } + if (!Modifier.isPublic(method.getModifiers())) { + return false; + } // is method annotated with nojexl ? NoJexl nojexl = method.getAnnotation(NoJexl.class); if (nojexl != null) { @@ -134,9 +160,7 @@ private static boolean allow(Class clazz, boolean interf) { if (clazz == null) { return false; } - // is package annotated with nojexl ? - Package pack = clazz.getPackage(); - if (pack != null && pack.getAnnotation(NoJexl.class) != null) { + if (!Modifier.isPublic(clazz.getModifiers())) { return false; } // lets walk all interfaces diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/SandboxUberspect.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/SandboxUberspect.java index 3d5483eeb..583a9d400 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/SandboxUberspect.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/SandboxUberspect.java @@ -53,25 +53,16 @@ public SandboxUberspect(final JexlUberspect theUberspect, final JexlSandbox theS this.sandbox = theSandbox.copy(); } - /** - * {@inheritDoc} - */ @Override public void setClassLoader(ClassLoader loader) { uberspect.setClassLoader(loader); } - /** - * {@inheritDoc} - */ @Override public int getVersion() { return uberspect.getVersion(); } - /** - * {@inheritDoc} - */ @Override public JexlMethod getConstructor(final Object ctorHandle, final Object... args) { final String className; @@ -89,9 +80,6 @@ public JexlMethod getConstructor(final Object ctorHandle, final Object... args) return null; } - /** - * {@inheritDoc} - */ @Override public JexlMethod getMethod(final Object obj, final String method, final Object... args) { if (obj != null && method != null) { @@ -104,25 +92,16 @@ public JexlMethod getMethod(final Object obj, final String method, final Object. return null; } - /** - * {@inheritDoc} - */ @Override public List getResolvers(JexlOperator op, Object obj) { return uberspect.getResolvers(op, obj); } - /** - * {@inheritDoc} - */ @Override public JexlPropertyGet getPropertyGet(final Object obj, final Object identifier) { return getPropertyGet(null, obj, identifier); } - /** - * {@inheritDoc} - */ @Override public JexlPropertyGet getPropertyGet(final List resolvers, final Object obj, @@ -136,17 +115,11 @@ public JexlPropertyGet getPropertyGet(final List resolvers, return null; } - /** - * {@inheritDoc} - */ @Override public JexlPropertySet getPropertySet(final Object obj,final Object identifier,final Object arg) { return getPropertySet(null, obj, identifier, arg); } - /** - * {@inheritDoc} - */ @Override public JexlPropertySet getPropertySet(final List resolvers, final Object obj, @@ -161,17 +134,11 @@ public JexlPropertySet getPropertySet(final List resolvers, return null; } - /** - * {@inheritDoc} - */ @Override public Iterator getIterator(final Object obj) { return uberspect.getIterator(obj); } - /** - * {@inheritDoc} - */ @Override public JexlArithmetic.Uberspect getArithmetic(final JexlArithmetic arithmetic) { return uberspect.getArithmetic(arithmetic); diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java index cab15dce8..8ebdd082f 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java @@ -109,9 +109,6 @@ protected final Introspector base() { } // CSON: DoubleCheckedLocking - /** - * {@inheritDoc} - */ @Override public void setClassLoader(ClassLoader nloader) { synchronized (this) { @@ -128,9 +125,6 @@ public void setClassLoader(ClassLoader nloader) { } } - /** - * {@inheritDoc} - */ @Override public int getVersion() { return version.intValue(); @@ -219,33 +213,21 @@ public final Method[] getMethods(Class c, final String methodName) { return base().getMethods(c, methodName); } - /** - * {@inheritDoc} - */ @Override public JexlMethod getMethod(Object obj, String method, Object... args) { return MethodExecutor.discover(base(), obj, method, args); } - /** - * {@inheritDoc} - */ @Override public List getResolvers(JexlOperator op, Object obj) { return strategy.apply(op, obj); } - /** - * {@inheritDoc} - */ @Override public JexlPropertyGet getPropertyGet(Object obj, Object identifier) { return getPropertyGet(null, obj, identifier); } - /** - * {@inheritDoc} - */ @Override public JexlPropertyGet getPropertyGet( final List resolvers, final Object obj, final Object identifier @@ -287,6 +269,10 @@ public JexlPropertyGet getPropertyGet( case FIELD: // a field may be? (can not be a number) executor = FieldGetExecutor.discover(is, claz, property); + // static class fields (enums included) + if (obj instanceof Class) { + executor = FieldGetExecutor.discover(is, (Class) obj, property); + } break; case CONTAINER: // or an indexed property? @@ -304,17 +290,12 @@ public JexlPropertyGet getPropertyGet( } return null; } - /** - * {@inheritDoc} - */ + @Override public JexlPropertySet getPropertySet(final Object obj, final Object identifier, final Object arg) { return getPropertySet(null, obj, identifier, arg); } - /** - * {@inheritDoc} - */ @Override public JexlPropertySet getPropertySet( final List resolvers, final Object obj, final Object identifier, final Object arg @@ -368,9 +349,6 @@ public JexlPropertySet getPropertySet( return null; } - /** - * {@inheritDoc} - */ @Override @SuppressWarnings("unchecked") public Iterator getIterator(Object obj) { @@ -405,9 +383,6 @@ public Iterator getIterator(Object obj) { return null; } - /** - * {@inheritDoc} - */ @Override public JexlMethod getConstructor(Object ctorHandle, Object... args) { return ConstructorMethod.discover(base(), ctorHandle, args); @@ -420,7 +395,7 @@ protected class ArithmeticUberspect implements JexlArithmetic.Uberspect { /** The arithmetic instance being analyzed. */ private final JexlArithmetic arithmetic; /** The set of overloaded operators. */ - private final EnumSet overloads; + private final Set overloads; /** * Creates an instance. @@ -429,9 +404,7 @@ protected class ArithmeticUberspect implements JexlArithmetic.Uberspect { */ private ArithmeticUberspect(JexlArithmetic theArithmetic, Set theOverloads) { this.arithmetic = theArithmetic; - this.overloads = EnumSet.copyOf(theOverloads); - // register this arithmetic class in the operator map - operatorMap.put(arithmetic.getClass(), overloads); + this.overloads = theOverloads; } @Override @@ -447,41 +420,40 @@ public boolean overloads(JexlOperator operator) { } } - /** - * {@inheritDoc} - */ @Override public JexlArithmetic.Uberspect getArithmetic(JexlArithmetic arithmetic) { JexlArithmetic.Uberspect jau = null; if (arithmetic != null) { - Set ops = operatorMap.get(arithmetic.getClass()); + final Class aclass = arithmetic.getClass(); + Set ops = operatorMap.get(aclass); if (ops == null) { ops = EnumSet.noneOf(JexlOperator.class); - for (JexlOperator op : JexlOperator.values()) { - Method[] methods = getMethods(arithmetic.getClass(), op.getMethodName()); - if (methods != null) { - for (Method method : methods) { - Class[] parms = method.getParameterTypes(); - if (parms.length != op.getArity()) { - continue; - } - // eliminate method(Object) and method(Object, Object) - boolean root = true; - for (int p = 0; root && p < parms.length; ++p) { - if (!Object.class.equals(parms[p])) { - root = false; + // deal only with derived classes + if (!JexlArithmetic.class.equals(aclass)) { + for (JexlOperator op : JexlOperator.values()) { + Method[] methods = getMethods(arithmetic.getClass(), op.getMethodName()); + if (methods != null) { + mloop: + for (Method method : methods) { + Class[] parms = method.getParameterTypes(); + if (parms.length != op.getArity()) { + continue; + } + // keep only methods that are not overrides + try { + JexlArithmetic.class.getMethod(method.getName(), method.getParameterTypes()); + } catch (NoSuchMethodException xmethod) { + // method was not found in JexlArithmetic; this is an operator definition + ops.add(op); } - } - if (!root) { - ops.add(op); } } } } + // register this arithmetic class in the operator map + operatorMap.put(aclass, ops); } - if (!ops.isEmpty()) { - jau = new ArithmeticUberspect(arithmetic, ops); - } + jau = new ArithmeticUberspect(arithmetic, ops); } return jau; } diff --git a/src/main/java/org/apache/commons/jexl3/introspection/JexlSandbox.java b/src/main/java/org/apache/commons/jexl3/introspection/JexlSandbox.java index cebbb1832..1b4e29920 100644 --- a/src/main/java/org/apache/commons/jexl3/introspection/JexlSandbox.java +++ b/src/main/java/org/apache/commons/jexl3/introspection/JexlSandbox.java @@ -25,56 +25,83 @@ /** * A sandbox describes permissions on a class by explicitly allowing or forbidding access to methods and properties * through "whitelists" and "blacklists". - * + * *

        A whitelist explicitly allows methods/properties for a class;

        - * + * *
          - *
        • If a whitelist is empty and thus does not contain any names, + *
        • If a whitelist is empty and thus does not contain any names, * all properties/methods are allowed for its class.
        • *
        • If it is not empty, the only allowed properties/methods are the ones contained.
        • *
        - * + * *

        A blacklist explicitly forbids methods/properties for a class;

        - * + * *
          *
        • If a blacklist is empty and thus does not contain any names, * all properties/methods are forbidden for its class.
        • *
        • If it is not empty, the only forbidden properties/methods are the ones contained.
        • *
        - * + * *

        Permissions are composed of three lists, read, write, execute, each being "white" or "black":

        - * + * *
          *
        • read controls readable properties
        • - *
        • write controls writeable properties
        • + *
        • write controls writable properties
        • *
        • execute controls executable methods and constructor
        • *
        - * - *

        Note that a JexlUberspect allways uses a copy of the JexlSandbox used to built it to avoid synchronization and/or + * + *

        Note that a JexlUberspect always uses a copy of the JexlSandbox used to built it to avoid synchronization and/or * concurrent modifications at runtime.

        - * + * * @since 3.0 */ public final class JexlSandbox { - /** * The map from class names to permissions. */ private final Map sandbox; + /** + * Default behavior, black or white. + */ + private final boolean white; /** * Creates a new default sandbox. + *

        In the absence of explicit permissions on a class, the + * sandbox is a white-box, white-listing that class for all permissions (read, write and execute). */ public JexlSandbox() { - this(new HashMap()); + this(true, new HashMap()); + } + + /** + * Creates a new default sandbox. + *

        A white-box considers no permissions as "everything is allowed" when + * a black-box considers no permissions as "nothing is allowed". + * @param wb whether this sandbox is white (true) or black (false) + * if no permission is explicitly defined for a class. + * @since 3.1 + */ + public JexlSandbox(boolean wb) { + this(wb, new HashMap()); } /** * Creates a sandbox based on an existing permissions map. - * * @param map the permissions map */ protected JexlSandbox(Map map) { + this(true, map); + } + + /** + * Creates a sandbox based on an existing permissions map. + * @param wb whether this sandbox is white (true) or black (false) + * @param map the permissions map + * @since 3.1 + */ + protected JexlSandbox(boolean wb, Map map) { + white = wb; sandbox = map; } @@ -86,12 +113,12 @@ public JexlSandbox copy() { for (Map.Entry entry : sandbox.entrySet()) { map.put(entry.getKey(), entry.getValue().copy()); } - return new JexlSandbox(map); + return new JexlSandbox(white, map); } /** * Gets the read permission value for a given property of a class. - * + * * @param clazz the class * @param name the property name * @return null if not allowed, the name of the property to use otherwise @@ -102,7 +129,7 @@ public String read(Class clazz, String name) { /** * Gets the read permission value for a given property of a class. - * + * * @param clazz the class name * @param name the property name * @return null if not allowed, the name of the property to use otherwise @@ -110,7 +137,7 @@ public String read(Class clazz, String name) { public String read(String clazz, String name) { Permissions permissions = sandbox.get(clazz); if (permissions == null) { - return name; + return white? name : null; } else { return permissions.read().get(name); } @@ -118,7 +145,7 @@ public String read(String clazz, String name) { /** * Gets the write permission value for a given property of a class. - * + * * @param clazz the class * @param name the property name * @return null if not allowed, the name of the property to use otherwise @@ -129,7 +156,7 @@ public String write(Class clazz, String name) { /** * Gets the write permission value for a given property of a class. - * + * * @param clazz the class name * @param name the property name * @return null if not allowed, the name of the property to use otherwise @@ -137,7 +164,7 @@ public String write(Class clazz, String name) { public String write(String clazz, String name) { Permissions permissions = sandbox.get(clazz); if (permissions == null) { - return name; + return white ? name : null; } else { return permissions.write().get(name); } @@ -145,7 +172,7 @@ public String write(String clazz, String name) { /** * Gets the execute permission value for a given method of a class. - * + * * @param clazz the class * @param name the method name * @return null if not allowed, the name of the method to use otherwise @@ -156,7 +183,7 @@ public String execute(Class clazz, String name) { /** * Gets the execute permission value for a given method of a class. - * + * * @param clazz the class name * @param name the method name * @return null if not allowed, the name of the method to use otherwise @@ -164,7 +191,7 @@ public String execute(Class clazz, String name) { public String execute(String clazz, String name) { Permissions permissions = sandbox.get(clazz); if (permissions == null) { - return name; + return white ? name : null; } else { return permissions.execute().get(name); } @@ -177,7 +204,7 @@ public abstract static class Names { /** * Adds a name to this set. - * + * * @param name the name to add * @return true if the name was really added, false if not */ @@ -186,7 +213,7 @@ public abstract static class Names { /** * Adds an alias to a name to this set. *

        This only has an effect on white lists.

        - * + * * @param name the name to alias * @param alias the alias * @return true if the alias was added, false if it was already present @@ -197,7 +224,7 @@ public boolean alias(String name, String alias) { /** * Whether a given name is allowed or not. - * + * * @param name the method/property name to check * @return null if not allowed, the actual name to use otherwise */ @@ -304,7 +331,7 @@ public static final class Permissions { /** The controlled readable properties. */ private final Names read; - /** The controlled writeable properties. */ + /** The controlled writable properties. */ private final Names write; /** The controlled methods. */ @@ -312,7 +339,7 @@ public static final class Permissions { /** * Creates a new permissions instance. - * + * * @param readFlag whether the read property list is white or black * @param writeFlag whether the write property list is white or black * @param executeFlag whether the method list is white of black @@ -325,7 +352,7 @@ public static final class Permissions { /** * Creates a new permissions instance. - * + * * @param nread the read set * @param nwrite the write set * @param nexecute the method set @@ -345,7 +372,7 @@ Permissions copy() { /** * Adds a list of readable property names to these permissions. - * + * * @param pnames the property names * @return this instance of permissions */ @@ -357,8 +384,8 @@ public Permissions read(String... pnames) { } /** - * Adds a list of writeable property names to these permissions. - * + * Adds a list of writable property names to these permissions. + * * @param pnames the property names * @return this instance of permissions */ @@ -372,7 +399,7 @@ public Permissions write(String... pnames) { /** * Adds a list of executable methods names to these permissions. *

        The constructor is denoted as the empty-string, all other methods by their names.

        - * + * * @param mnames the method names * @return this instance of permissions */ @@ -385,7 +412,7 @@ public Permissions execute(String... mnames) { /** * Gets the set of readable property names in these permissions. - * + * * @return the set of property names */ public Names read() { @@ -393,8 +420,8 @@ public Names read() { } /** - * Gets the set of writeable property names in these permissions. - * + * Gets the set of writable property names in these permissions. + * * @return the set of property names */ public Names write() { @@ -403,7 +430,7 @@ public Names write() { /** * Gets the set of method names in these permissions. - * + * * @return the set of method names */ public Names execute() { @@ -417,10 +444,10 @@ public Names execute() { /** * Creates the set of permissions for a given class. - * + * * @param clazz the class for which these permissions apply * @param readFlag whether the readable property list is white - true - or black - false - - * @param writeFlag whether the writeable property list is white - true - or black - false - + * @param writeFlag whether the writable property list is white - true - or black - false - * @param executeFlag whether the executable method list is white white - true - or black - false - * @return the set of permissions */ @@ -432,7 +459,7 @@ public Permissions permissions(String clazz, boolean readFlag, boolean writeFlag /** * Creates a new set of permissions based on white lists for methods and properties for a given class. - * + * * @param clazz the whitened class name * @return the permissions instance */ @@ -442,7 +469,7 @@ public Permissions white(String clazz) { /** * Creates a new set of permissions based on black lists for methods and properties for a given class. - * + * * @param clazz the blackened class name * @return the permissions instance */ @@ -452,7 +479,7 @@ public Permissions black(String clazz) { /** * Gets the set of permissions associated to a class. - * + * * @param clazz the class name * @return the defined permissions or an all-white permission instance if none were defined */ diff --git a/src/main/java/org/apache/commons/jexl3/package.html b/src/main/java/org/apache/commons/jexl3/package.html index d6d026b90..92ea77441 100644 --- a/src/main/java/org/apache/commons/jexl3/package.html +++ b/src/main/java/org/apache/commons/jexl3/package.html @@ -80,11 +80,13 @@

        Important note

      • org.apache.commons.jexl3
      • org.apache.commons.jexl3.introspection
      • +

        The following packages follow a "use at your own maintenance cost" policy; these are only intended to be used for extending JEXL. Their classes and methods are not guaranteed to remain compatible in subsequent versions. If you think you need to use directly some of their features or methods, it might be a good idea to check with the community through the mailing list first. +

        • org.apache.commons.jexl3.parser
        • org.apache.commons.jexl3.scripting
        • diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTAnnotation.java b/src/main/java/org/apache/commons/jexl3/parser/ASTAnnotation.java new file mode 100644 index 000000000..be7954475 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTAnnotation.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3.parser; + +/** + * Annotation. + */ +public class ASTAnnotation extends JexlNode { + private String name = null; + + ASTAnnotation(int id) { + super(id); + } + + ASTAnnotation(Parser p, int id) { + super(p, id); + } + + @Override + public String toString() { + return name; + } + + void setName(String identifier) { + if (identifier.charAt(0) == '@') { + name = identifier.substring(1); + } else { + name = identifier; + } + } + + public String getName() { + return name; + } + + @Override + public Object jjtAccept(ParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTArrayLiteral.java b/src/main/java/org/apache/commons/jexl3/parser/ASTArrayLiteral.java index 5a90cb244..e0d550e1d 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTArrayLiteral.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTArrayLiteral.java @@ -44,7 +44,6 @@ protected boolean isConstant(boolean literal) { return constant; } - /** {@inheritDoc} */ @Override public void jjtClose() { constant = true; @@ -58,7 +57,6 @@ public void jjtClose() { } } - /** {@inheritDoc} */ @Override public Object jjtAccept(ParserVisitor visitor, Object data) { return visitor.visit(this, data); diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java b/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java index 2f8884e24..8dd894734 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java @@ -16,6 +16,7 @@ */ package org.apache.commons.jexl3.parser; +import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.internal.Scope; import java.util.Map; @@ -26,7 +27,9 @@ public class ASTJexlScript extends JexlNode { /** The script scope. */ private Scope scope = null; /** The pragmas. */ - Map pragmas = null; + private Map pragmas = null; + /** Features. */ + private JexlFeatures features = null; public ASTJexlScript(int id) { super(id); @@ -54,18 +57,34 @@ public ASTJexlScript script() { public Object jjtAccept(ParserVisitor visitor, Object data) { return visitor.visit(this, data); } + /** + * Sets this script pragmas. + * @param pragmas the pragmas + */ + public void setPragmas(Map thePragmas) { + this.pragmas = thePragmas; + } /** - * Coerce this script as an expression (ie only one child) if necessary. - * @return true if the script was coerced, false otherwise + * @return this script pragmas. */ - public boolean toExpression() { - if (jjtGetNumChildren() > 1) { - jjtSetChildren(new JexlNode[]{jjtGetChild(0)}); - return true; - } else { - return false; - } + public Map getPragmas() { + return pragmas; + } + + /** + * Sets this script features. + * @param theFeatures the features + */ + public void setFeatures(JexlFeatures theFeatures) { + this.features = theFeatures; + } + + /** + * @return this script scope + */ + public JexlFeatures getFeatures() { + return features; } /** @@ -83,13 +102,6 @@ public Scope getScope() { return scope; } - /** - * @return this script pragmas - */ - public Map getPragmas() { - return pragmas; - } - /** * Creates an array of arguments by copying values up to the number of parameters. * @param values the argument values diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java b/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java index e90bcf356..93a14b6fd 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java @@ -45,7 +45,6 @@ public String toString() { return this.literal; } - /** {@inheritDoc} */ @Override public Object jjtAccept(ParserVisitor visitor, Object data) { return visitor.visit(this, data); diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTMapLiteral.java b/src/main/java/org/apache/commons/jexl3/parser/ASTMapLiteral.java index cae63f801..5650dbf63 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTMapLiteral.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTMapLiteral.java @@ -41,7 +41,6 @@ protected boolean isConstant(boolean literal) { return constant; } - /** {@inheritDoc} */ @Override public void jjtClose() { constant = true; @@ -55,7 +54,6 @@ public void jjtClose() { } } - /** {@inheritDoc} */ @Override public Object jjtAccept(ParserVisitor visitor, Object data) { return visitor.visit(this, data); diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTNumberLiteral.java b/src/main/java/org/apache/commons/jexl3/parser/ASTNumberLiteral.java index 163e6193b..d5b614dd3 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTNumberLiteral.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTNumberLiteral.java @@ -44,7 +44,7 @@ protected boolean isConstant(boolean literal) { return true; } - public Class getLiteralClass() { + public Class getLiteralClass() { return nlp.getLiteralClass(); } diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTSetLiteral.java b/src/main/java/org/apache/commons/jexl3/parser/ASTSetLiteral.java index 8531593fa..4db2395f4 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTSetLiteral.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTSetLiteral.java @@ -41,7 +41,6 @@ protected boolean isConstant(boolean literal) { return constant; } - /** {@inheritDoc} */ @Override public void jjtClose() { constant = true; @@ -53,7 +52,6 @@ public void jjtClose() { } } - /** {@inheritDoc} */ @Override public Object jjtAccept(ParserVisitor visitor, Object data) { return visitor.visit(this, data); diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTStringLiteral.java b/src/main/java/org/apache/commons/jexl3/parser/ASTStringLiteral.java index 383e6e31e..4898dbe0f 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTStringLiteral.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTStringLiteral.java @@ -42,8 +42,6 @@ public String getLiteral() { return this.literal; } - - /** {@inheritDoc} */ @Override protected boolean isConstant(boolean literal) { return true; @@ -53,8 +51,6 @@ void setLiteral(String literal) { this.literal = literal; } - - /** {@inheritDoc} */ @Override public Object jjtAccept(ParserVisitor visitor, Object data) { return visitor.visit(this, data); diff --git a/src/main/java/org/apache/commons/jexl3/parser/FeatureController.java b/src/main/java/org/apache/commons/jexl3/parser/FeatureController.java new file mode 100644 index 000000000..92d0e7217 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/parser/FeatureController.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3.parser; + +import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlFeatures; +import org.apache.commons.jexl3.JexlInfo; +import org.apache.commons.jexl3.internal.ScriptVisitor; +/** + * Controls that a script only uses enabled features. + */ +public class FeatureController extends ScriptVisitor { + /** The set of features. */ + private JexlFeatures features = null; + + /** + * Creates a features controller . + */ + public FeatureController(JexlFeatures features) { + this.features = features; + } + + /** + * Sets the features to controlNode. + * @param fdesc the features + */ + public void setFeatures(JexlFeatures fdesc) { + this.features = fdesc; + } + + /** + * @return the controlled features + */ + public JexlFeatures getFeatures() { + return features; + } + + /** + * Perform the control on a node. + *

          Note that controlNode() does *not* visit node children in this class. + * @param node the node to controlNode + * @throws JexlException.Feature if required feature is disabled + */ + public void controlNode(JexlNode node) { + node.jjtAccept(this, null); + } + + @Override + protected Object visitNode(JexlNode node, Object data) { + // no need to visit them since we close them one by one + return data; + } + + /** + * Throws a feature exception. + * @param feature the feature code + * @param node the node that caused it + */ + public void throwFeatureException(int feature, JexlNode node) { + JexlInfo dbgInfo = node.jexlInfo(); + throw new JexlException.Feature(dbgInfo, feature, ""); + } + + /** + * Checks whether a node is a string or an integer. + * @param child the child node + * @return true if string / integer, false otherwise + */ + private boolean isArrayReferenceLiteral(JexlNode child) { + if (child instanceof ASTStringLiteral) { + return true; + } + if (child instanceof ASTNumberLiteral && ((ASTNumberLiteral) child).isInteger()) { + return true; + } + return false; + } + + @Override + protected Object visit(ASTArrayAccess node, Object data) { + if (!features.supportsArrayReferenceExpr()) { + for (int i = 0; i < node.jjtGetNumChildren(); ++i) { + JexlNode child = node.jjtGetChild(i); + if (!isArrayReferenceLiteral(child)) { + throwFeatureException(JexlFeatures.ARRAY_REF_EXPR, child); + } + } + } + return data; + } + + @Override + protected Object visit(ASTWhileStatement node, Object data) { + if (!features.supportsLoops()) { + throwFeatureException(JexlFeatures.LOOP, node); + } + return data; + } + + @Override + protected Object visit(ASTForeachStatement node, Object data) { + if (!features.supportsLoops()) { + throwFeatureException(JexlFeatures.LOOP, node); + } + return data; + } + + @Override + protected Object visit(ASTConstructorNode node, Object data) { + if (!features.supportsNewInstance()) { + throwFeatureException(JexlFeatures.NEW_INSTANCE, node); + } + return data; + } + + @Override + protected Object visit(ASTMethodNode node, Object data) { + if (!features.supportsMethodCall()) { + throwFeatureException(JexlFeatures.METHOD_CALL, node); + } + return data; + } + + @Override + protected Object visit(ASTAnnotation node, Object data) { + if (!features.supportsAnnotation()) { + throwFeatureException(JexlFeatures.ANNOTATION, node); + } + return data; + } + + @Override + protected Object visit(ASTArrayLiteral node, Object data) { + if (!features.supportsStructuredLiteral()) { + throwFeatureException(JexlFeatures.STRUCTURED_LITERAL, node); + } + return data; + } + + @Override + protected Object visit(ASTMapLiteral node, Object data) { + if (!features.supportsStructuredLiteral()) { + throwFeatureException(JexlFeatures.STRUCTURED_LITERAL, node); + } + return data; + } + + @Override + protected Object visit(ASTSetLiteral node, Object data) { + if (!features.supportsStructuredLiteral()) { + throwFeatureException(JexlFeatures.STRUCTURED_LITERAL, node); + } + return data; + } + + @Override + protected Object visit(ASTRangeNode node, Object data) { + if (!features.supportsStructuredLiteral()) { + throwFeatureException(JexlFeatures.STRUCTURED_LITERAL, node); + } + return data; + } + + private Object controlSideEffect(JexlNode node, Object data) { + JexlNode lv = node.jjtGetChild(0); + if (!features.supportsSideEffectGlobal() && lv.isGlobalVar()) { + throwFeatureException(JexlFeatures.SIDE_EFFECT_GLOBAL, lv); + } + if (!features.supportsSideEffect()) { + throwFeatureException(JexlFeatures.SIDE_EFFECT, lv); + } + return data; + } + + @Override + protected Object visit(ASTAssignment node, Object data) { + return controlSideEffect(node, data); + } + + @Override + protected Object visit(ASTSetAddNode node, Object data) { + return controlSideEffect(node, data); + } + + @Override + protected Object visit(ASTSetMultNode node, Object data) { + return controlSideEffect(node, data); + } + + @Override + protected Object visit(ASTSetDivNode node, Object data) { + return controlSideEffect(node, data); + } + + @Override + protected Object visit(ASTSetAndNode node, Object data) { + return controlSideEffect(node, data); + } + + @Override + protected Object visit(ASTSetOrNode node, Object data) { + return controlSideEffect(node, data); + } + + @Override + protected Object visit(ASTSetXorNode node, Object data) { + return controlSideEffect(node, data); + } + + @Override + protected Object visit(ASTSetSubNode node, Object data) { + return controlSideEffect(node, data); + } + +} diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java b/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java index e50774f70..4772d5bf8 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java @@ -62,22 +62,24 @@ public void jjtSetLastToken(Token t) { * @return the info */ public JexlInfo jexlInfo() { + JexlInfo info = null; JexlNode node = this; while (node != null) { if (node.jjtGetValue() instanceof JexlInfo) { - JexlInfo info = (JexlInfo) node.jjtGetValue(); - if (lc >= 0) { - int c = lc & 0xfff; - int l = lc >> 0xc; - return info.at(l, c); - } else { - // weird though; no jjSetFirstToken(...) ever called? - return info; - } + info = (JexlInfo) node.jjtGetValue(); + break; } node = node.jjtGetParent(); } - return null; + if (lc >= 0) { + int c = lc & 0xfff; + int l = lc >> 0xc; + // at least an info with line/column number + return info != null? info.at(l, c) : new JexlInfo(null, l, c); + } else { + // weird though; no jjSetFirstToken(...) ever called? + return info; + } } /** @@ -136,17 +138,47 @@ protected boolean isConstant(boolean literal) { * @return true if node is assignable, false otherwise */ public boolean isLeftValue() { - if (this instanceof ASTIdentifier || this instanceof ASTIdentifierAccess) { + JexlNode walk = this; + do { + if (walk instanceof ASTIdentifier + || walk instanceof ASTIdentifierAccess + || walk instanceof ASTArrayAccess) { + return true; + } + int nc = walk.jjtGetNumChildren() - 1; + if (nc >= 0) { + walk = walk.jjtGetChild(nc); + } else if (walk.jjtGetParent() instanceof ASTReference) { + return true; + } else { + return false; + } + } while (walk != null); + return false; + } + + public boolean isGlobalVar() { + if (this instanceof ASTVar) { + return false; + } + if (this instanceof ASTIdentifier) { + return ((ASTIdentifier) this).getSymbol() < 0; + } + if (this instanceof ASTIdentifierAccess) { return true; } int nc = this.jjtGetNumChildren() - 1; if (nc >= 0) { - JexlNode last = this.jjtGetChild(this.jjtGetNumChildren() - 1); - return last.isLeftValue(); + JexlNode first = this.jjtGetChild(0); + return first.isGlobalVar(); } - if (jjtGetParent() instanceof ASTReference || jjtGetParent() instanceof ASTArrayAccess) { + if (jjtGetParent() instanceof ASTReference) { return true; } return false; } + + public boolean isLocalVar() { + return this instanceof ASTIdentifier && ((ASTIdentifier) this).getSymbol() >= 0; + } } diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java index c6a605cc9..188626e5a 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java @@ -16,44 +16,112 @@ */ package org.apache.commons.jexl3.parser; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; +import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JexlInfo; import org.apache.commons.jexl3.internal.Scope; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.TreeMap; +import java.util.Set; import java.util.Stack; +import java.util.TreeMap; + /** * The base class for parsing, manages the parameter/local variable frame. */ public abstract class JexlParser extends StringParser { - /** Whether the parser will allow user-named registers (aka #0 syntax). */ - boolean ALLOW_REGISTERS = false; - /** The source being processed. */ - String source = null; + /** + * The associated controller. + */ + protected final FeatureController featureController = new FeatureController(JexlEngine.DEFAULT_FEATURES); + /** + * The source being processed. + */ + protected String source = null; /** * The map of named registers aka script parameters. *

          Each parameter is associated to a register and is materialized * as an offset in the registers array used during evaluation.

          */ - Scope frame = null; - Stack frames = new Stack(); + protected Scope frame = null; + /** + * When parsing inner functions/lambda, need to stack the scope (sic). + */ + protected Stack frames = new Stack(); /** * The list of pragma declarations. */ - Map pragmas = null; + protected Map pragmas = null; + + /** + * Utility function to create '.' separated string from a list of string. + * @param lstr the list of strings + * @return the dotted version + */ + protected static String stringify(List lstr) { + StringBuilder strb = new StringBuilder(); + boolean dot = false; + for(String str : lstr) { + if (!dot) { + dot = true; + } else { + strb.append('.'); + } + strb.append(str); + } + return strb.toString(); + } + /** + * Reading a given source line + * @parma src the source + * @param lineno the line number + * @return the line + */ + protected static String readSourceLine(String src, int lineno) { + String msg = ""; + if (src != null && lineno >= 0) { + try { + BufferedReader reader = new BufferedReader(new StringReader(src)); + for (int l = 0; l < lineno; ++l) { + msg = reader.readLine(); + } + } catch (IOException xio) { + // ignore, very unlikely but then again... + } + } + return msg; + } /** * Internal, for debug purpose only. */ public void allowRegisters(boolean registers) { - ALLOW_REGISTERS = registers; + featureController.setFeatures(new JexlFeatures(featureController.getFeatures()).register(registers)); + } + + /** + * Sets a new set of options. + * @param features + */ + protected void setFeatures(JexlFeatures features) { + this.featureController.setFeatures(features); + } + + /** + * @return the current set of features active during parsing + */ + protected JexlFeatures getFeatures() { + return featureController.getFeatures(); } /** @@ -61,7 +129,7 @@ public void allowRegisters(boolean registers) { *

          This is used to allow parameters to be declared before parsing.

          * @param theFrame the register map */ - public void setFrame(Scope theFrame) { + protected void setFrame(Scope theFrame) { frame = theFrame; } @@ -71,14 +139,14 @@ public void setFrame(Scope theFrame) { * regain access after parsing to known which / how-many registers are needed.

          * @return the named register map */ - public Scope getFrame() { + protected Scope getFrame() { return frame; } /** * Create a new local variable frame and push it as current scope. */ - public void pushFrame() { + protected void pushFrame() { if (frame != null) { frames.push(frame); } @@ -88,7 +156,7 @@ public void pushFrame() { /** * Pops back to previous local variable frame. */ - public void popFrame() { + protected void popFrame() { if (!frames.isEmpty()) { frame = frames.pop(); } else { @@ -102,7 +170,7 @@ public void popFrame() { * @param image the identifier image * @return the image */ - public String checkVariable(ASTIdentifier identifier, String image) { + protected String checkVariable(ASTIdentifier identifier, String image) { if (frame != null) { Integer register = frame.getSymbol(image); if (register != null) { @@ -112,18 +180,33 @@ public String checkVariable(ASTIdentifier identifier, String image) { return image; } + protected boolean allowVariable(String image) { + JexlFeatures features = getFeatures(); + if (!features.supportsLocalVar()) { + return false; + } + if (features.isReservedName(image)) { + return false; + } + return true; + } + /** * Declares a local variable. *

          This method creates an new entry in the symbol map.

          - * @param identifier the identifier used to declare - * @param image the variable name + * @param var the identifier used to declare + * @param token the variable name toekn */ - public void declareVariable(ASTVar identifier, String image) { + protected void declareVariable(ASTVar var, Token token) { + String identifier = token.image; + if (!allowVariable(identifier)) { + throwFeatureException(JexlFeatures.LOCAL_VAR, token); + } if (frame == null) { frame = new Scope(null, (String[]) null); } - Integer register = frame.declareVariable(image); - identifier.setSymbol(register.intValue(), image); + Integer register = frame.declareVariable(identifier); + var.setSymbol(register.intValue(), identifier); } /** @@ -131,7 +214,10 @@ public void declareVariable(ASTVar identifier, String image) { * @param key the pragma key * @param value the pragma value */ - public void declarePragma(String key, Object value) { + protected void declarePragma(String key, Object value) { + if (!getFeatures().supportsPragma()) { + throwFeatureException(JexlFeatures.PRAGMA, getToken(0)); + } if (pragmas == null) { pragmas = new TreeMap(); } @@ -141,9 +227,13 @@ public void declarePragma(String key, Object value) { /** * Declares a local parameter. *

          This method creates an new entry in the symbol map.

          - * @param identifier the parameter name + * @param token the parameter name toekn */ - public void declareParameter(String identifier) { + protected void declareParameter(Token token) { + String identifier = token.image; + if (!allowVariable(identifier)) { + throwFeatureException(JexlFeatures.LOCAL_VAR, token); + } if (frame == null) { frame = new Scope(null, (String[]) null); } @@ -155,62 +245,97 @@ public void declareParameter(String identifier) { * @param top whether the identifier is beginning an l/r value * @throws ParseException subclasses may throw this */ - public void Identifier(boolean top) throws ParseException { + protected void Identifier(boolean top) throws ParseException { // Overriden by generated code } - final public void Identifier() throws ParseException { + final protected void Identifier() throws ParseException { Identifier(false); } - public Token getToken(int index) { - return null; - } + /** + * Overridden in actual parser to access tokens stack. + * @param index 0 to get current token + * @return the token on the stack + */ + protected abstract Token getToken(int index); - void jjtreeOpenNodeScope(JexlNode node) { + protected void jjtreeOpenNodeScope(JexlNode node) { + if (node instanceof ASTAmbiguous) { + throwParsingException(JexlException.Ambiguous.class, node); + } } + /** + * The set of assignment operators as classes. + */ + @SuppressWarnings("unchecked") + private static final Set> ASSIGN_NODES = new HashSet>( + Arrays.asList( + ASTAssignment.class, + ASTSetAddNode.class, + ASTSetMultNode.class, + ASTSetDivNode.class, + ASTSetAndNode.class, + ASTSetOrNode.class, + ASTSetXorNode.class, + ASTSetSubNode.class + ) + ); + /** * Called by parser at end of node construction. - *

          Detects "Ambiguous statement" and 'non-leaft value assignment'.

          + *

          + * Detects "Ambiguous statement" and 'non-left value assignment'.

          * @param node the node * @throws ParseException */ - void jjtreeCloseNodeScope(JexlNode node) throws ParseException { + protected void jjtreeCloseNodeScope(JexlNode node) throws ParseException { if (node instanceof ASTJexlScript) { + if (node instanceof ASTJexlLambda && !getFeatures().supportsLambda()) { + throwFeatureException(JexlFeatures.LAMBDA, node.jexlInfo()); + } ASTJexlScript script = (ASTJexlScript) node; // reaccess in case local variables have been declared if (script.getScope() != frame) { script.setScope(frame); } popFrame(); - } else if (node instanceof ASTAmbiguous) { - throwParsingException(JexlException.Ambiguous.class, node); - } else if (node instanceof ASTAssignment) { + } else if (ASSIGN_NODES.contains(node.getClass())) { JexlNode lv = node.jjtGetChild(0); if (!lv.isLeftValue()) { throwParsingException(JexlException.Assignment.class, lv); } } + // heavy check + featureController.controlNode(node); + } + + + /** + * Throws a feature exception. + * @param feature the feature code + * @param info the exception surroundings + */ + protected void throwFeatureException(int feature, JexlInfo info) { + String msg = info != null? readSourceLine(source, info.getLine()) : null; + throw new JexlException.Feature(info, feature, msg); } /** - * Utility function to create '.' separated string from a list of string. - * @param lstr the list of strings - * @return the dotted version + * Throws a feature exception. + * @param feature the feature code + * @param token the token that triggered it */ - String stringify(List lstr) { - StringBuilder strb = new StringBuilder(); - boolean dot = false; - for(String str : lstr) { - if (!dot) { - dot = true; - } else { - strb.append('.'); + protected void throwFeatureException(int feature, Token token) { + if (token == null) { + token = this.getToken(0); + if (token == null) { + throw new JexlException.Parsing(null, JexlFeatures.stringify(feature)); } - strb.append(str); } - return strb.toString(); + JexlInfo info = new JexlInfo(token.image, token.beginLine, token.beginColumn); + throwFeatureException(feature, info); } /** @@ -226,33 +351,22 @@ protected void throwParsingException(JexlNode node) { * @param xclazz the class of exception * @param node the node that caused it */ - private void throwParsingException(Class xclazz, JexlNode node) { - final JexlInfo dbgInfo; + protected void throwParsingException(Class xclazz, JexlNode node) { Token tok = this.getToken(0); - if (tok != null) { - dbgInfo = new JexlInfo(tok.image, tok.beginLine, tok.beginColumn); - } else { - dbgInfo = node.jexlInfo(); + if (tok == null) { + throw new JexlException.Parsing(null, "unrecoverable state"); } - String msg = null; - try { - if (source != null) { - BufferedReader reader = new BufferedReader(new StringReader(source)); - for (int l = 0; l < dbgInfo.getLine(); ++l) { - msg = reader.readLine(); - } - } else { - msg = ""; + JexlInfo dbgInfo = new JexlInfo(tok.image, tok.beginLine, tok.beginColumn); + String msg = readSourceLine(source, tok.beginLine); + JexlException xjexl = null; + if (xclazz != null) { + try { + Constructor ctor = xclazz.getConstructor(JexlInfo.class, String.class); + xjexl = ctor.newInstance(dbgInfo, msg); + } catch (Exception xany) { + // ignore, very unlikely but then again.. } - } catch (IOException xio) { - // ignore - } - if (JexlException.Ambiguous.class.equals(xclazz)) { - throw new JexlException.Ambiguous(dbgInfo, msg); - } - if (JexlException.Assignment.class.equals(xclazz)) { - throw new JexlException.Assignment(dbgInfo, msg); } - throw new JexlException.Parsing(dbgInfo, msg); + throw xjexl != null ? xjexl : new JexlException.Parsing(dbgInfo, msg); } } diff --git a/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java b/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java index ad3e08526..265a3070d 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java @@ -26,7 +26,7 @@ public final class NumberParser { /** The type literal value. */ private Number literal = null; /** The expected class. */ - private Class clazz = null; + private Class clazz = null; /** JEXL locale-neutral big decimal format. */ static final DecimalFormat BIGDF = new DecimalFormat("0.0b", new DecimalFormatSymbols(Locale.ENGLISH)); @@ -36,27 +36,25 @@ public String toString() { return "NaN"; } if (BigDecimal.class.equals(clazz)) { - return BIGDF.format(literal); + return BIGDF.format(literal); } StringBuilder strb = new StringBuilder(literal.toString()); - if (clazz != null) { - if (Float.class.equals(clazz)) { - strb.append('f'); - } else if (Double.class.equals(clazz)) { - strb.append('d'); - } else if (BigDecimal.class.equals(clazz)) { - strb.append('b'); - } else if (BigInteger.class.equals(clazz)) { - strb.append('h'); - } else if (Long.class.equals(clazz)) { - strb.append('l'); - } + if (Float.class.equals(clazz)) { + strb.append('f'); + } else if (Double.class.equals(clazz)) { + strb.append('d'); + } else if (BigDecimal.class.equals(clazz)) { + strb.append('b'); + } else if (BigInteger.class.equals(clazz)) { + strb.append('h'); + } else if (Long.class.equals(clazz)) { + strb.append('l'); } return strb.toString(); } - Class getLiteralClass() { + Class getLiteralClass() { return clazz; } @@ -87,7 +85,7 @@ static Number parseDouble(String s) { */ void setNatural(String s) { Number result; - Class rclass; + Class rclass; // determine the base final int base; if (s.charAt(0) == '0') { @@ -138,7 +136,7 @@ void setNatural(String s) { */ void setReal(String s) { Number result; - Class rclass; + Class rclass; if ("#NaN".equals(s) || "NaN".equals(s)) { result = Double.NaN; rclass = Double.class; diff --git a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt index d320fc539..f1942f559 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -35,11 +35,10 @@ PARSER_BEGIN(Parser) package org.apache.commons.jexl3.parser; import java.util.Collections; -import java.util.List; import java.util.LinkedList; -import java.io.Reader; import org.apache.commons.jexl3.JexlInfo; +import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JexlException; import org.apache.commons.jexl3.internal.Scope; @@ -47,23 +46,25 @@ public final class Parser extends JexlParser { private int loopCount = 0; - public ASTJexlScript parse(JexlInfo info, String jexlSrc, Scope scope, boolean registers, boolean expr) { + public ASTJexlScript parse(JexlInfo info, JexlFeatures jexlFeatures, String jexlSrc, Scope scope) { + JexlFeatures previous = getFeatures(); try { + setFeatures(jexlFeatures); // If registers are allowed, the default parser state has to be REGISTERS. - if (registers || ALLOW_REGISTERS) { + if (jexlFeatures.supportsRegister()) { token_source.defaultLexState = REGISTERS; } // lets do the 'Unique Init' in here to be safe - it's a pain to remember source = jexlSrc; pragmas = null; - ReInit(new java.io.StringReader(jexlSrc)); frame = scope; - ASTJexlScript script = expr? JexlExpression(scope) : JexlScript(scope) ; - script.pragmas = pragmas != null + ReInit(new java.io.StringReader(jexlSrc)); + ASTJexlScript script = jexlFeatures.supportsScript()? JexlScript(scope) : JexlExpression(scope); + script.jjtSetValue(info); + script.setPragmas(pragmas != null ? Collections.unmodifiableMap(pragmas) - : Collections.emptyMap(); + : Collections.emptyMap()); pragmas = null; - script.jjtSetValue(info); return script; } catch (TokenMgrError xtme) { throw new JexlException.Tokenization(info, xtme).clean(); @@ -73,6 +74,7 @@ public final class Parser extends JexlParser source = null; frame = null; token_source.defaultLexState = DEFAULT; + setFeatures(previous); } } } @@ -151,6 +153,7 @@ TOKEN_MGR_DECLS : { <*> TOKEN : { /* CONDITIONALS */ < QMARK : "?" > | < ELVIS : "?:" > + | < NULLP : "??" > | < AND : "&&" | "and" > | < OR : "||" | "or" > } @@ -203,6 +206,11 @@ TOKEN_MGR_DECLS : { < NAN_LITERAL : "NaN" > } +<*> TOKEN : /* ANNOTATION */ +{ + < ANNOTATION: "@" ( [ "0"-"9", "a"-"z", "A"-"Z", "_", "$" ])+ > +} + TOKEN : /* IDENTIFIERS */ { < DOT_IDENTIFIER: ( [ "0"-"9", "a"-"z", "A"-"Z", "_", "$", "@" ])+ > { popDot(); } /* Revert state to default. */ @@ -278,11 +286,25 @@ ASTJexlScript JexlExpression(Scope frame) #JexlScript : { } } +void Annotation() #Annotation : +{ + Token t; +} +{ + t= (LOOKAHEAD() Arguments() )? { jjtThis.setName(t.image); } +} + +void AnnotatedStatement() #AnnotatedStatement() : {} +{ + (LOOKAHEAD() Annotation())+ (LOOKAHEAD() Var() | LOOKAHEAD(1) Block() | Expression()) +} + void Statement() #void : {} { - | LOOKAHEAD( Expression() ) Block() // to diasmbiguate the set literals - | LOOKAHEAD( Statement() ) Block() // to diasmbiguate the set literals + | LOOKAHEAD() AnnotatedStatement() + | LOOKAHEAD( Expression() ) Block() // to disambiguate the set literals + | LOOKAHEAD( Statement() ) Block() // to disambiguate the set literals | IfStatement() | ForeachStatement() | WhileStatement() @@ -302,13 +324,15 @@ void Block() #Block : {} void ExpressionStatement() #void : {} { - Expression() (LOOKAHEAD(1) Expression() #Ambiguous())* (LOOKAHEAD(2) )? + Expression() (LOOKAHEAD(1) Expression() #Ambiguous())* (LOOKAHEAD(1) )? } void IfStatement() : {} { - Expression() (LOOKAHEAD(1) Block() | Statement()) ( LOOKAHEAD(1) (LOOKAHEAD(1) Block() | Statement()) )? + Expression() (LOOKAHEAD(1) Block() | Statement()) + ( LOOKAHEAD(2) Expression() (LOOKAHEAD(1) Block() | Statement()) )* + ( LOOKAHEAD(1) (LOOKAHEAD(1) Block() | Statement()) )? } @@ -319,7 +343,7 @@ void WhileStatement() : {} void ReturnStatement() : {} { - Expression() + ExpressionStatement() } void Continue() #Continue : {} @@ -354,7 +378,7 @@ void DeclareVar() #Var : Token t; } { - t= { declareVariable(jjtThis, t.image); } + t= { declareVariable(jjtThis, t); } } void Pragma() #void : @@ -435,6 +459,8 @@ void ConditionalExpression() #void : {} Expression() Expression() #TernaryNode(3) | Expression() #TernaryNode(2) + | + Expression() #NullpNode(2) )? } @@ -731,7 +757,7 @@ void Parameter() #void : Token t; } { - t= { declareParameter(t.image); } + t= { declareParameter(t); } } void Parameters() #void : {} @@ -841,7 +867,3 @@ void ValueExpression() #void : {} ( PrimaryExpression() ( LOOKAHEAD(2) MemberExpression() )*) #Reference(>1) } - - - - diff --git a/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java b/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java index a84329c98..03047382c 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java @@ -27,7 +27,7 @@ public abstract class ParserVisitor { * @return does not return */ protected final Object visit(SimpleNode node, Object data) { - throw new UnsupportedOperationException("Not supported yet."); + throw new UnsupportedOperationException(node.getClass().getSimpleName() + " : not supported yet."); } /** @@ -64,6 +64,8 @@ protected final Object visit(ASTAmbiguous node, Object data) { protected abstract Object visit(ASTTernaryNode node, Object data); + protected abstract Object visit(ASTNullpNode node, Object data); + protected abstract Object visit(ASTOrNode node, Object data); protected abstract Object visit(ASTAndNode node, Object data); @@ -177,4 +179,8 @@ protected final Object visit(ASTAmbiguous node, Object data) { protected abstract Object visit(ASTSetXorNode node, Object data); protected abstract Object visit(ASTJxltLiteral node, Object data); + + protected abstract Object visit(ASTAnnotation node, Object data); + + protected abstract Object visit(ASTAnnotatedStatement node, Object data); } diff --git a/src/main/java/org/apache/commons/jexl3/scripting/JexlScriptEngineFactory.java b/src/main/java/org/apache/commons/jexl3/scripting/JexlScriptEngineFactory.java index e8f760b68..c64c5732b 100644 --- a/src/main/java/org/apache/commons/jexl3/scripting/JexlScriptEngineFactory.java +++ b/src/main/java/org/apache/commons/jexl3/scripting/JexlScriptEngineFactory.java @@ -37,7 +37,7 @@ * See * Java Scripting API * Javadoc. - * + * * @since 2.0 */ public class JexlScriptEngineFactory implements ScriptEngineFactory { @@ -49,7 +49,7 @@ public String getEngineName() { @Override public String getEngineVersion() { - return "3.0"; // ensure this is updated if function changes are made to this class + return "3.2"; // ensure this is updated if function changes are made to this class } @Override @@ -59,7 +59,7 @@ public String getLanguageName() { @Override public String getLanguageVersion() { - return "3.0"; // TODO this should be derived from the actual version + return "3.2"; // TODO this should be derived from the actual version } @Override diff --git a/src/site/site.xml b/src/site/site.xml index c9c77e944..79e3609a0 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -25,9 +25,9 @@ - - - + + + diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml index 69b1d4fcb..f530ffef4 100644 --- a/src/site/xdoc/changes.xml +++ b/src/site/xdoc/changes.xml @@ -25,7 +25,161 @@ Commons Developers - + + + Allow range subexpression as an array property assignment identifier + + + Intermittent ambiguous method invocation when processing assignOverload + + + Engine in strict mode fails to fail on unsolvable variables or properties + + + Webapp classloader memory leaks + + + Allow restricting available features in Script/Expressions + + + NPE when script containing string interpolation executed in multiple threads + + + Unable to invoke a call operator using antish style variable resoltion + + + Restrict getLiteralClass to a Number for NumberLiterals + + + Ability to restrict usage of certain names when declaring local variables + + + Support CharSequence in size(), empty() and contains() operators + + + Extend application of operators startsWith and endsWith from String to CharSequence types + + + Syntax for accessing List elements is not mentioned in docs + + + List literal is not mentioned in docs + + + JexlScriptEngineFactory.getEngineVersion() should return actual version + + + add ?? operator support + + + Incorrect invoking methods with ObjectContext + + + The ability to overload call() operator in customized JexlArithmetic implementation + + + Restrict usage of assignment statements in JexlExpression + + + + + The ability to declare indexed property getter/setter in customised JexlArithmetic implementation + + + Sporadic undefined property error caused by NPE at MapGetExecutor.tryInvoke() + + + Blacklist by default in sandbox + + + Interpreter.getAttribute() raises exception in non-strict mode when cached property resolver is used + + + Improve parsing concurrency in multithreaded environment + + + JexlEngine.createInfo() is redundantly called when debug and caching is enabled leading to sub-optimal performance + + + Redundant call of fillInStackTrace() in JexlEngine.createInfo() ? + + + rename JexlBuilder.loader(Charset arg) to JexlBuilder.charset(Charset arg) + + + Add callable method to JexlExpression interface + + + The way to cancel script execution with an error + + + Unsolvable function/method '<?>.<null>(...)' + + + Documentation typos/inconsistencies + + + Inconsistent error handling + + + testCallableCancel() test hangs sporadically + + + testCancelForever() is not terminated properly + + + Script is not interrupted by a method call throwing Exception + + + JexlArithmetic.options() diverts Interpreter to use default implementation of JexlArithmetic instead of custom one + + + Detect invalid assignment operator usage with non-assignable l-value during script parsing + + + Allow Interpreter to use live values from JexlEngine.Option interface implemented by JexlContext + + + JxltEngine Template does not expose pragmas + + + Add annotations + + + Script execution hangs while calling method with one argument without parameter + + + Support for AtomicBoolean in logical expressions + + + allow synchronization on iterableValue in foreach statement + + + InterruptedException is swallowed in function call in silent and non-strict mode + + + Invalid return type when expected result is null + + + Jexl3 unsolvable property exception when using enum + + + local function within context is not resolved if function resolver class without namespace is specified + + + Possible bug in Interpreter.isCancelled() + + + Possible bug in JexlArithmetic.isFloatingPointNumber() + + + Jexl Syntax doc does not mention 'continue' and 'break' operators + + + Performance regression in arithmetic operations compared to JEXL 2.1 + + + dot-ed identifiers parsing failure @@ -174,7 +328,7 @@ Syntactically enforce that expressions do not contain statements: POTENTIAL EXPRESSION BREAK! (ie an expression is not a script and can NOT use 'if','for'... and blocks) - + Added syntactic shortcut to create parametric scripts (script source creates an anonymous function) diff --git a/src/site/xdoc/download_jexl.xml b/src/site/xdoc/download_jexl.xml index 5cb37d899..e4899c1ac 100644 --- a/src/site/xdoc/download_jexl.xml +++ b/src/site/xdoc/download_jexl.xml @@ -32,14 +32,16 @@ limitations under the License. | - commons.componentid (required, alphabetic, lower case) | | - commons.release.version (required) | | - commons.release.name (required) | - | - commons.binary.suffix (optional) | + | - commons.binary.suffix (optional) | | (defaults to "-bin", set to "" for pre-maven2 releases) | | - commons.release.desc (optional) | + | - commons.release.subdir (optional) | | | | - commons.release.2/3.version (conditional) | | - commons.release.2/3.name (conditional) | | - commons.release.2/3.binary.suffix (optional) | | - commons.release.2/3.desc (optional) | + | - commons.release.2/3.subdir (optional) | | | | 3) Example Properties | | (commons.release.name inherited by parent: | @@ -109,32 +111,32 @@ limitations under the License.

          -
          +
          - - - + + + - - - + + +
          commons-jexl3-3.0-bin.tar.gzmd5pgpcommons-jexl-3.1-bin.tar.gzmd5pgp
          commons-jexl3-3.0-bin.zipmd5pgpcommons-jexl-3.1-bin.zipmd5pgp
          - - - + + + - - - + + +
          commons-jexl3-3.0-src.tar.gzmd5pgpcommons-jexl-3.1-src.tar.gzmd5pgp
          commons-jexl3-3.0-src.zipmd5pgpcommons-jexl-3.1-src.zipmd5pgp
          diff --git a/src/site/xdoc/reference/examples.xml b/src/site/xdoc/reference/examples.xml index 644d9b95c..16303a3f9 100644 --- a/src/site/xdoc/reference/examples.xml +++ b/src/site/xdoc/reference/examples.xml @@ -32,7 +32,7 @@

        - You can find two sample programs in JEXL's CVS repository: + You can find two sample programs in JEXL's source repository:

        • Using arrays
        • Accessing Properties and invoking methods
        • diff --git a/src/site/xdoc/reference/jsr223.xml b/src/site/xdoc/reference/jsr223.xml index 1a4c72d72..2dd31d745 100644 --- a/src/site/xdoc/reference/jsr223.xml +++ b/src/site/xdoc/reference/jsr223.xml @@ -54,7 +54,7 @@

          The binary release includes a command-line application which can be used to exercise the JSR-223 script engine. For example: - java -cp commons-jexl-3.0.jar;commons-logging-1.2.jar + java -cp commons-jexl-3.1.jar;commons-logging-1.2.jar org.apache.commons.jexl3.scripting.Main script.jexl If a single argument is provided, then that is assumed to be the name of a script file; otherwise, the application prompts for script input to be evaluated. diff --git a/src/site/xdoc/reference/syntax.xml b/src/site/xdoc/reference/syntax.xml index 5fee85e92..1a8b695d6 100644 --- a/src/site/xdoc/reference/syntax.xml +++ b/src/site/xdoc/reference/syntax.xml @@ -17,668 +17,896 @@ --> - - Apache Commons JEXL Syntax - + + Apache Commons JEXL Syntax + - -

          -

          - This reference is split up into the following sections: -

            -
          1. Language Elements
          2. -
          3. Literals
          4. -
          5. Functions
          6. -
          7. Operators
          8. -
          9. Conditional Statements
          10. -
          -

          -

          - For more technical information about the JEXL Grammar, you can find the - JavaCC grammar for JEXL - here: Parser.jjt -

          -
          -
          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          ItemDescription
          Comments - Specified using ## or //and extend to the end of line, e.g. - ## This is a comment - Also specified using //, e.g. - // This is a comment - Multiple lines comments are specified using /*...*/, e.g. - /* This is a - multi-line comment */ -
          Identifiers / variables - Must start with a-z, A-Z, _ or $. - Can then be followed by 0-9, a-z, A-Z, _ or $. - e.g. -
            -
          • Valid: var1,_a99,$1
          • -
          • Invalid: 9v,!a99,1$
          • -
          + +

          - Variable names are case-sensitive, e.g. var1 and Var1 are different variables. + This reference is split up into the following sections: +

            +
          1. + Language Elements +
          2. +
          3. + Literals +
          4. +
          5. + Functions +
          6. +
          7. + Operators +
          8. +
          9. + Access +
          10. +
          11. + Conditional Statements +
          12. +

          - NOTE: JEXL does not support variables with hyphens in them, e.g. - commons-logging // invalid variable name (hyphenated) is not a valid variable, but instead is treated as a - subtraction of the variable logging from the variable commons + For more technical information about the JEXL Grammar, you can find the + JavaCC grammar for JEXL + here: Parser.jjt

          -

          - JEXL also supports ant-style variables, the following is a valid variable name: - my.dotted.var -

          -

          - N.B. the following keywords are reserved, and cannot be used as a variable name or property when using the dot operator: - or and eq ne lt gt le ge div mod not null true false new var return - For example, the following is invalid: - my.new.dotted.var // invalid ('new' is keyword) - In such cases, quoted identifiers or the [ ] operator can be used, for example: - my.'new'.dotted.var - my['new'].dotted.var -

          -
          Scripts -

          - A script in JEXL is made up of zero or more statements. Scripts can be read from a String, File or URL. -

          -

          - They can be created with named parameters which allow a later evaluation to be performed with arguments. -

          -

          - A script returns the last expression evaluated by default. -

          -

          - Using the return keyword, a script will return the expression that follows (or null). -

          -
          Local variablesCan be defined using the var keyword; their identifying rules are the same as contextual variables. -
            -
          • Basic declaration: var x;
          • -
          • Declaration with assignment: var theAnswer = 42;
          • -
          • Invalid declaration: var x.y;
          • -
          - Their scope is the entire script scope and they take precedence in resolution over contextual variables. - When scripts are created with named parameters, those behave as local variables. - Local variables can not use ant-style naming, only one identifier. -
          Statements - A statement can be the empty statement, the semicolon (;) , block, assignment or an expression. - Statements are optionally terminated with a semicolon. -
          Block - A block is simply multiple statements inside curly braces ({, }). -
          Assignment - Assigns the value of a variable (my.var = 'a value') using a - JexlContext as initial resolver. Both beans and ant-ish - variables assignment are supported. -
          Method calls - Calls a method of an object, e.g. - "hello world".hashCode() will call the hashCode method - of the "hello world" String. -

          In case of multiple arguments and overloading, JEXL will make the best effort to find - the most appropriate non ambiguous method to call.

          -
          #pragma - Declares a pragma, a method to communicate information from a script to its execution environment, e.g. - #pragma execution.option 42 will declare a pragma named execution.option with - a value of 42. -

          Pragma keys can be identifiers or antish names, pragma values can be literals (boolean, integer, - real, string, null, NaN) and antish names

          -
          -
          -
          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          ItemDescription
          Integer Literals1 or more digits from 0 to 9, eg 42. -
          Float Literals - 1 or more digits from 0 to 9, followed - by a decimal point and then one or more digits from - 0 to 9, - optionally followed by f or F, - eg 42.0 or 42.0f. -
          Long Literals1 or more digits from 0 to 9 suffixed with l or L - , eg 42l. -
          Double Literals - 1 or more digits from 0 to 9, followed - by a decimal point and then one or more digits from - 0 to 9 suffixed with d or D - , eg 42.0d. -
          Big Integer Literals1 or more digits from 0 to 9 suffixed with h or H - (for Huge ala OGNL, "does not interfere with hexa-decimal digits"), eg 42h. -
          Big Decimal Literals - 1 or more digits from 0 to 9, followed - by a decimal point and then one or more digits from - 0 to 9 suffixed with b or B) - , eg 42.0b. -
          Natural literals - octal and hex support - Natural numbers (i.e. Integer, Long, BigInteger) can also be expressed as octal or hexadecimal using the same format as Java. - i.e. prefix the number with 0 for octal, and prefix with 0x or 0X for hexadecimal. - For example 010 or 0x10. -
          Real literals - exponent support - Real numbers (i.e. Float, Double, BigDecimal) can also be expressed using standard Java exponent notation. - i.e. suffix the number with e or E followed by the sign + or - - followed by one or more decimal digits. - For example 42.0E-1D or 42.0E+3B. -
          String literals - Can start and end with either ' or " delimiters, e.g. - "Hello world" and - 'Hello world' are equivalent. -

          The escape character is \ (backslash); it only escapes the string delimiter

          -
          Multiline format literals - Start and end with ` delimiter - back-quote -, e.g. `Hello world` -

          The escape character is \ (backslash); it only escapes the string delimiter.

          - These format literals can span multiple lines and allow Unified JEXL expressions (JSTL like expressions) - to be interpolated. If a variable user valued JEXLis present in the environment - whether - as a local or global variable -, the format `Hello ${user}` will evaluate as Hello JEXL. -
          Boolean literals - The literals true and false can be used, e.g. - val1 == true -
          Null literal - The null value is represented as in java using the literal null, e.g. - val1 == null -
          Array literal - A [ followed by one or more expressions separated by , and ending - with ], e.g. - [ 1, 2, "three" ] -

          This syntax creates an Object[].

          -

          - JEXL will attempt to strongly type the array; if all entries are of the same class or if all - entries are Number instance, the array literal will be an MyClass[] in the former - case, a Number[] in the latter case.

          -

          Furthermore, if all entries in the array literal are of the same class - and that class has an equivalent primitive type, the array returned will be a primitive array. e.g. - [1, 2, 3] will be interpreted as int[].

          -
          Set literal - A { followed by one or more expressions separated by , and ending - with }, e.g. - { "one" , 2, "more"} -

          This syntax creates a HashSet<Object>.

          -
          Map literal - A { followed by one or more sets of key : value pairs separated by , and ending - with }, e.g. - { "one" : 1, "two" : 2, "three" : 3, "more": "many more" } -

          This syntax creates a HashMap<Object,Object>.

          -
          -
          -
          - - - - - - - - - - - - - - - - - - - - - - -
          FunctionDescription
          empty - Evaluates whether an expression if 'empty'. - This is true when the argument is: -
            -
          1. null
          2. -
          3. An instance of class C and the derived JexlArithmetic overloads a method 'public boolean empty(C arg)' - that returns true when the argument is considered empty
          4. -
          5. An empty string
          6. -
          7. An array of length zero
          8. -
          9. A collection of size zero
          10. -
          11. An empty map
          12. -
          13. Defining a method 'public boolean isEmpty()' - that returns true when the instance is considered empty
          14. -
          - This is false in other cases (besides errors). - empty(arg) -
          size - Evaluates the 'size' of an expression. - This returns: -
            -
          1. 0 if the argument is null
          2. -
          3. The result of calling a method from a derived JexlArithmetic overload 'public int size(C arg)', - C being the class of the argument
          4. -
          5. Length of an array
          6. -
          7. Length of a string
          8. -
          9. Size of a Collection
          10. -
          11. Size of a Map
          12. -
          13. The result of calling a method 'public int size()' defined by the argument class
          14. -
          - This returns 0 in other cases (besides errors). - size("Hello") returns 5. -
          new - Creates a new instance using a fully-qualified class name or Class: - new("java.lang.Double", 10) returns 10.0. -

          Note that the first argument of new can be a variable or any - expression evaluating as a String or Class; the rest of the arguments are used - as arguments to the constructor for the class considered.

          -

          In case of multiple constructors, JEXL will make the best effort to find - the most appropriate non ambiguous constructor to call.

          -
          ns:function - A JexlEngine can register objects or classes used as function namespaces. - This can allow expressions like: - math:cosinus(23.0) -
          function - Defines a function within the script, usually associated with a local variable assignment. - var fun = function(x, y) { x + y } - Calling a function follows the usual convention: - fun(17, 25) -

          Note that functions can use local variables and parameters from their declaring script. - Those variables values are bound in the function environment at definition time.

          - var t = 20; var s = function(x, y) {x + y + t}; t = 54; s(15, 7) - The function closure hoists 't' when defined; the result of the evaluation will - lead to 15 +7 + 20 = 42. -
          -
          -
          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          OperatorDescription
          Boolean and - The usual && operator can be used as well as the word and, e.g. - cond1 and cond2 and - cond1 && cond2 are equivalent -
          Boolean or - The usual || operator can be used as well as the word or, e.g. - cond1 or cond2 and - cond1 || cond2 are equivalent -
          Boolean not - The usual ! operator can be used as well as the word not, e.g. - !cond1 and - not cond1 are equivalent -
          Bitwise and - The usual & operator is used, e.g. - 33 & 4, 0010 0001 & 0000 0100 = 0. -
          Bitwise or - The usual | operator is used, e.g. - 33 | 4, 0010 0001 | 0000 0100 = 0010 0101 = 37. -
          Bitwise xor - The usual ^ operator is used, e.g. - 33 ^ 4, 0010 0001 ^ 0000 0100 = 0010 0100 = 37. -
          Bitwise complement - The usual ~ operator is used, e.g. - ~33, ~0010 0001 = 1101 1110 = -34. -
          Ternary conditional ?: - The usual ternary conditional operator condition ? if_true : if_false operator can be - used as well as the abbreviation value ?: if_false which returns the value if - its evaluation is defined, non-null and non-false, e.g. - val1 ? val1 : val2 and - val1 ?: val2 are equivalent. -

          - NOTE: The condition will evaluate to false when it - refers to an undefined variable or null for all JexlEngine - flag combinations. This allows explicit syntactic leniency and treats the condition - 'if undefined or null or false' the same way in all cases. -

          -
          Equality - The usual == operator can be used as well as the abbreviation eq. - For example - val1 == val2 and - val1 eq val2 are equivalent. -
            -
          1. - null is only ever equal to null, that is if you compare null - to any non-null value, the result is false. -
          2. -
          3. Equality uses the java equals method
          4. -
          -
          Inequality - The usual != operator can be used as well as the abbreviation ne. - For example - val1 != val2 and - val1 ne val2 are equivalent. -
          Less Than - The usual < operator can be used as well as the abbreviation lt. - For example - val1 < val2 and - val1 lt val2 are equivalent. -
          Less Than Or Equal To - The usual <= operator can be used as well as the abbreviation le. - For example - val1 <= val2 and - val1 le val2 are equivalent. -
          Greater Than - The usual > operator can be used as well as the abbreviation gt. - For example - val1 > val2 and - val1 gt val2 are equivalent. -
          Greater Than Or Equal To - The usual >= operator can be used as well as the abbreviation ge. - For example - val1 >= val2 and - val1 ge val2 are equivalent. -
          In or Match=~ - The syntactically Perl inspired =~ operator can be used to check that a string matches - a regular expression (expressed either a Java String or a java.util.regex.Pattern). - For example - "abcdef" =~ "abc.* returns true. - It also checks whether any collection, set or map (on keys) contains a value or not; in that case, it behaves - as an "in" operator. Note that it also applies to arrays as well as "duck-typed" collection, ie classes exposing a "contains" - method. - "a" =~ ["a","b","c","d","e",f"] returns true. -
          Not-In or Not-Match!~ - The syntactically Perl inspired !~ operator can be used to check that a string does not - match a regular expression (expressed either a Java String or a java.util.regex.Pattern). - For example - "abcdef" !~ "abc.* returns false. - It also checks whether any collection, set or map (on keys) does not contain a value; in that case, it behaves - as "not in" operator. - "a" !~ ["a","b","c","d","e",f"] returns true. - Note that through duck-typing, user classes exposing a public 'contains' method will allow their instances - to behave has right-hand-size operands of this operator. -
          Starts With=^ - The syntactically CSS3 inspired =^ operator is a short-hand for the 'startsWith' method. - For example, - "abcdef" ^= "abc" returns true. - Note that through duck-typing, user classes exposing a public 'startsWith' method will allow their instances - to behave has left-hand-size operands of this operator. -
          Not Starts With!^ - This is the negation of the 'starts with' operator. - a ^! "abc" is equivalent to !(a ^= "abc") -
          Ends With=$The syntactically CSS3 inspired =$ operator is a short-hand for the 'endsWith' method. - For example, - "abcdef" $= "def" returns true. - Note that through duck-typing, user classes exposing an 'endsWith' method will allow their instances - to behave has left-hand-size operands of this operator. -
          Not Ends With!^ - This is the negation of the 'ends with' operator. - a $! "abc" is equivalent to !(a $= "abc") -
          Range.. - This operator creates a 'range' of values (in the form of a java iterable). - For example, - for(var x: 1 .. 3) {} will loop 3 times with the value of 'x' being 1, 2 and 3. -
          Addition - The usual + operator is used. - For example - val1 + val2 -
          Subtraction - The usual - operator is used. - For example - val1 - val2 -
          Multiplication - The usual * operator is used. - For example - val1 * val2 -
          Division - The usual / operator is used, or one can use the div operator. - For example - val1 / val2 - or - val1 div val2 -
          Modulus (or remainder) - The % operator is used. An alternative is the mod - operator. - For example - 5 mod 2 gives 1 and is equivalent to 5 % 2 -
          Negation - The unary - operator is used. - For example - -12 -
          Array access - Array elements may be accessed using either square brackets or a dotted numeral, e.g. - arr1[0] and arr1.0 are equivalent -
          HashMap access - Map elements are accessed using square brackets, e.g. - map[0]; map['name']; map[var]; - Note that map['7'] and map[7] refer to different elements. - Map elements with a numeric key may also be accessed using a dotted numeral, e.g. - map[0] and map.0 are equivalent. -
          -
          -
          - - - - - - - - - - - - - - -
          OperatorDescription
          if - Classic, if/else statement, e.g. - if ((x * 2) == 5) { - y = 1; -} else { - y = 2; -} -
          for - Loop through items of an Array, Collection, Map, Iterator or Enumeration, e.g. - for(item : list) { - x = x + item; -} - Where item and list are variables. - The JEXL 1.1 syntax using foreach(item in list) is now unsupported. -
          while - Loop until a condition is satisfied, e.g. - while (x lt 10) { - x = x + 2; -} -
          -
          + +
          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          ItemDescription
          Comments + Specified using ## or //and extend to the end of line, e.g. + ## This is a comment + Also specified using //, e.g. + // This is a comment + Multiple lines comments are specified using /*...*/, e.g. + /* This is a + multi-line comment */ +
          Identifiers / variables + Must start with a-z, A-Z, _ or $. + Can then be followed by 0-9, a-z, A-Z, _ or $. + e.g. +
            +
          • Valid: var1,_a99,$1
          • +
          • Invalid: 9v,!a99,1$
          • +
          +

          + Variable names are case-sensitive, e.g. var1 and Var1 are different variables. +

          +

          + NOTE: JEXL does not support variables with hyphens in them, e.g. + commons-logging // invalid variable name (hyphenated) is not a valid variable, but instead is treated as a + subtraction of the variable logging from the variable commons +

          +

          + JEXL also supports ant-style variables, the following is a valid variable name: + my.dotted.var +

          +

          + N.B. the following keywords are reserved, and cannot be used as a variable name or property when using the dot operator: + or and eq ne lt gt le ge div mod not null true false new var break continue return + For example, the following is invalid: + my.new.dotted.var // invalid ('new' is keyword) + In such cases, quoted identifiers or the [ ] operator can be used, for example: + my.'new'.dotted.var + my['new'].dotted.var +

          +
          Scripts +

          + A script in JEXL is made up of zero or more statements. Scripts can include one or more pragmas. +

          +

          + Scripts can be read from a String, File or URL. +

          +

          + They can be created with named parameters which allow a later evaluation to be performed with arguments. +

          +

          + By default a script returns the value of the last evaluated statement. +

          +

          + Using the return keyword, a script will return the expression that follows (or null). +

          +
          Local variablesCan be defined using the var keyword; their identifying rules are the same as contextual variables. +
            +
          • Basic declaration: var x;
          • +
          • Declaration with assignment: var theAnswer = 42;
          • +
          • Invalid declaration: var x.y;
          • +
          + Their scope is the entire script scope and they take precedence in resolution over contextual variables. + When scripts are created with named parameters, those behave as local variables. + Local variables can not use ant-style naming, only one identifier. +
          Statements + A statement can be the empty statement, the semicolon (;) , block, conditional, assignment or an expression. + Statements are optionally terminated with a semicolon. + A single statement or a statement block can be annotated. +
          Block + A block is simply multiple statements inside curly braces ({, }). +
          Assignment + Assigns the value of a variable (my.var = 'a value') using a + JexlContext as initial resolver. Both beans and ant-ish + variables assignment are supported. +
          Expression + An expression can be the literal, variable, access operator, function definition, function call, method call or + an evaluation operator. +
          Function definition + Defines a function within the script, usually associated with a local variable assignment. + var fun = function(x, y) { x + y } + The following syntax is also supported + var fun = (x, y) -> { x + y } + Calling a function follows the usual convention: + fun(17, 25) +

          Note that functions can use local variables and parameters from their declaring script. + Those variables values are bound in the function environment at definition time.

          + var t = 20; var s = function(x, y) {x + y + t}; t = 54; s(15, 7) + The function closure hoists 't' when defined; the result of the evaluation will + lead to 15 +7 + 20 = 42. +
          Method call + Calls a method of an object, e.g. + "hello world".hashCode() will call the hashCode method + of the "hello world" String. +

          In case of multiple arguments and overloading, JEXL will make the best effort to find + the most appropriate non ambiguous method to call.

          +
          Access Operator + Allows to evaluate a property of an object, a value of the collection or an array + by using either square brackets or a dotted numeral, e.g. + foo.bar will access the bar property + of the foo Object. + arr1[0] will access the first element of the + of the arr1 array. +

          Access operators can be overloaded in JexlArithmetic, so that + the operator behaviour will differ depending on the type of the operator arguments

          +
          Evaluation Operator + Performs computational, logical or comparative action between one, two or three arguments + whose values are expressions, e.g. + 40 + 2 will call the add operator + between two integer literals. +

          All operators, except when stated otherwise, can be overloaded in JexlArithmetic, so that the action taken + will differ depending on the type of the operator arguments

          +
          #pragma + Declares a pragma, a method to communicate information from a script to its execution environment, e.g. + #pragma execution.option 42 will declare a pragma named execution.option with + a value of 42. +

          Pragma keys can be identifiers or antish names, pragma values can be literals (boolean, integer, + real, string, null, NaN) and antish names

          +
          @annotation + Annotations in JEXL are 'meta-statements'; they allow to wrap the execution of the JEXL statement in a user provided + caller; typical example would be: @synchronized(x) x.someMethod(); +

          + Annotations may be declared with zero or more parameters; + @lenient x.someMethod(); + @synchronized(x) x.someMethod(); + @parallel(pool, 8) x.someMethod(); +

          +

          + They also can be chained as in: + @lenient @silent x.someMethod(); +

          +

          + Annotation processing is implemented by providing a JexlContext.AnnotationProcessor; its processAnnotation + method will call the annotated statement encapsulated in a Callable. Annotation arguments are evaluated + and passed as arguments to processAnnotation. +

          +
          +
          +
          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          ItemDescription
          Integer Literals1 or more digits from 0 to 9, eg 42. +
          Float Literals + 1 or more digits from 0 to 9, followed + by a decimal point and then one or more digits from + 0 to 9, + optionally followed by f or F, + eg 42.0 or 42.0f. +
          Long Literals1 or more digits from 0 to 9 suffixed with l or L + , eg 42l. +
          Double Literals + 1 or more digits from 0 to 9, followed + by a decimal point and then one or more digits from + 0 to 9 suffixed with d or D + , eg 42.0d. +
          Big Integer Literals1 or more digits from 0 to 9 suffixed with h or H + (for Huge ala OGNL, "does not interfere with hexa-decimal digits"), eg 42h. +
          Big Decimal Literals + 1 or more digits from 0 to 9, followed + by a decimal point and then one or more digits from + 0 to 9 suffixed with b or B) + , eg 42.0b. +
          Natural literals - octal and hex support + Natural numbers (i.e. Integer, Long, BigInteger) can also be expressed as octal or hexadecimal using the same format as Java. + i.e. prefix the number with 0 for octal, and prefix with 0x or 0X for hexadecimal. + For example 010 or 0x10. +
          Real literals - exponent support + Real numbers (i.e. Float, Double, BigDecimal) can also be expressed using standard Java exponent notation. + i.e. suffix the number with e or E followed by the sign + or - + followed by one or more decimal digits. + For example 42.0E-1D or 42.0E+3B. +
          String literals + Can start and end with either ' or " delimiters, e.g. + "Hello world" and + 'Hello world' are equivalent. +

          The escape character is \ (backslash); it only escapes the string delimiter

          +
          Multiline format literals + Start and end with ` delimiter - back-quote -, e.g. `Hello world` +

          The escape character is \ (backslash); it only escapes the string delimiter.

          + These format literals can span multiple lines and allow Unified JEXL expressions (JSTL like expressions) + to be interpolated. If a variable user valued JEXLis present in the environment - whether + as a local or global variable -, the format `Hello ${user}` will evaluate as Hello JEXL. +
          Boolean literals + The literals true and false can be used, e.g. + val1 == true +
          Null literal + The null value is represented as in java using the literal null, e.g. + val1 == null +
          Array literal + A [ followed by one or more expressions separated by , and ending + with ], e.g. + [ 1, 2, "three" ] +

          This syntax creates an Object[].

          +

          + JEXL will attempt to strongly type the array; if all entries are of the same class or if all + entries are Number instance, the array literal will be an MyClass[] in the former + case, a Number[] in the latter case.

          +

          Furthermore, if all entries in the array literal are of the same class + and that class has an equivalent primitive type, the array returned will be a primitive array. e.g. + [1, 2, 3] will be interpreted as int[].

          +
          List literal + A [ followed by one or more expressions separated by , and ending + with ,...], e.g. + [ 1, 2, "three",...] +

          This syntax creates an ArrayList<Object>.

          +
          Set literal + A { followed by one or more expressions separated by , and ending + with }, e.g. + { "one" , 2, "more"} +

          This syntax creates a HashSet<Object>.

          +
          Map literal + A { followed by one or more sets of key : value pairs separated by , and ending + with }, e.g. + { "one" : 1, "two" : 2, "three" : 3, "more": "many more" } +

          This syntax creates a HashMap<Object,Object>.

          +
          Range literal + A value followed by .. and ending with other value, e.g. + 1 .. 42 +

          This syntax creates a 'range' object in the form of a java iterable which can be used in for statement, e.g. + for (i : 1..42) a = a + b[i]

          +
          +
          +
          + + + + + + + + + + + + + + + + + + + + + + + + + +
          FunctionDescription
          empty + Evaluates whether an expression if 'empty'. + This is true when the argument is: +
            +
          1. + null +
          2. +
          3. An instance of class C and the derived JexlArithmetic overloads a method 'public boolean empty(C arg)' + that returns true when the argument is considered empty
          4. +
          5. An empty string
          6. +
          7. An array of length zero
          8. +
          9. A collection of size zero
          10. +
          11. An empty map
          12. +
          13. Defining a method 'public boolean isEmpty()' + that returns true when the instance is considered empty
          14. +
          + This is false in other cases (besides errors). + empty(arg) +
          size + Evaluates the 'size' of an expression. + This returns: +
            +
          1. 0 if the argument is null
          2. +
          3. The result of calling a method from a derived JexlArithmetic overload 'public int size(C arg)', + C being the class of the argument
          4. +
          5. Length of an array
          6. +
          7. Length of a string
          8. +
          9. Size of a Collection
          10. +
          11. Size of a Map
          12. +
          13. The result of calling a method 'public int size()' defined by the argument class
          14. +
          + This returns 0 in other cases (besides errors). + size("Hello") returns 5. +
          new + Creates a new instance using a fully-qualified class name or Class: + new("java.lang.Double", 10) returns 10.0. +

          Note that the first argument of new can be a variable or any + expression evaluating as a String or Class; the rest of the arguments are used + as arguments to the constructor for the class considered.

          +

          In case of multiple constructors, JEXL will make the best effort to find + the most appropriate non ambiguous constructor to call.

          +
          Top level function + Top level function is a function which can be invoked without specifying a namespace. +

          Top level function can be defined by the function definition method inside the script

          +

          A JexlContext can define methods which can be invoked as top level functions. + This can allow expressions like: + string(23.0)

          +

          Another way to define top level function is to register to JexlEngine objects or classes + with null namespace.

          + +
          ns:function + A JexlEngine can register objects or classes used as function namespaces. + This can allow expressions like: + math:cosinus(23.0) +
          +
          +
          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          OperatorDescription
          Boolean and +

          The usual && operator can be used as well as the word and, e.g. + cond1 and cond2 and + cond1 && cond2 are equivalent.

          +

          Note that this operator can not be overloaded

          +
          Boolean or +

          The usual || operator can be used as well as the word or, e.g. + cond1 or cond2 and + cond1 || cond2 are equivalent.

          +

          Note that this operator can not be overloaded

          +
          Boolean not +

          The usual ! operator can be used as well as the word not, e.g. + !cond1 and + not cond1 are equivalent.

          +

          Note that this operator can not be overloaded

          +
          Bitwise and + The usual & operator is used, e.g. + 33 & 4, 0010 0001 & 0000 0100 = 0. +
          Bitwise or + The usual | operator is used, e.g. + 33 | 4, 0010 0001 | 0000 0100 = 0010 0101 = 37. +
          Bitwise xor + The usual ^ operator is used, e.g. + 33 ^ 4, 0010 0001 ^ 0000 0100 = 0010 0100 = 37. +
          Bitwise complement + The usual ~ operator is used, e.g. + ~33, ~0010 0001 = 1101 1110 = -34. +
          Ternary conditional ?: + The usual ternary conditional operator condition ? if_true : if_false operator can be + used as well as the abbreviation value ?: if_false which returns the value if + its evaluation is defined, non-null and non-false, e.g. + val1 ? val1 : val2 and + val1 ?: val2 are equivalent. +

          + NOTE: The condition will evaluate to false when it + refers to an undefined variable or null for all JexlEngine + flag combinations. This allows explicit syntactic leniency and treats the condition + 'if undefined or null or false' the same way in all cases. +

          +

          Note that this operator can not be overloaded

          +
          Null coalescing operator ?? + The null coalescing operator returns the result of its first operand if it is defined and is not null. +

          When xandyare null or undefined, + x ?? 'unknown or null x' evaluates as 'unknown or null x' + y ?? "default" evaluates as "default". +

          +

          + When var x = 42 and var y = "forty-two",x??"other" + evaluates as 42 and y??"other" evaluates as "forty-two". +

          +

          + NOTE: this operator does not behave like the ternary conditional since it + does not coerce the first argument to a boolean to evaluate the condition. + When var x = false and var y = 0,x??true + evaluates as false and y??1 evaluates as 0. +

          +

          Note that this operator can not be overloaded

          +
          Equality + The usual == operator can be used as well as the abbreviation eq. + For example + val1 == val2 and + val1 eq val2 are equivalent. +
            +
          1. + null is only ever equal to null, that is if you compare null + to any non-null value, the result is false. +
          2. +
          3. Equality uses the java equals method
          4. +
          +
          Inequality + The usual != operator can be used as well as the abbreviation ne. + For example + val1 != val2 and + val1 ne val2 are equivalent. +
          Less Than + The usual < operator can be used as well as the abbreviation lt. + For example + val1 < val2 and + val1 lt val2 are equivalent. +
          Less Than Or Equal To + The usual <= operator can be used as well as the abbreviation le. + For example + val1 <= val2 and + val1 le val2 are equivalent. +
          Greater Than + The usual > operator can be used as well as the abbreviation gt. + For example + val1 > val2 and + val1 gt val2 are equivalent. +
          Greater Than Or Equal To + The usual >= operator can be used as well as the abbreviation ge. + For example + val1 >= val2 and + val1 ge val2 are equivalent. +
          In or Match=~ + The syntactically Perl inspired =~ operator can be used to check that a string matches + a regular expression (expressed either a Java String or a java.util.regex.Pattern). + For example + "abcdef" =~ "abc.* returns true. + It also checks whether any collection, set or map (on keys) contains a value or not; in that case, it behaves + as an "in" operator. + Note that arrays and user classes exposing a public 'contains' method will allow their instances + to behave as right-hand side operands of this operator. + "a" =~ ["a","b","c","d","e",f"] returns true. +
          Not-In or Not-Match!~ + The syntactically Perl inspired !~ operator can be used to check that a string does not + match a regular expression (expressed either a Java String or a java.util.regex.Pattern). + For example + "abcdef" !~ "abc.* returns false. + It also checks whether any collection, set or map (on keys) does not contain a value; in that case, it behaves + as "not in" operator. + Note that arrays and user classes exposing a public 'contains' method will allow their instances + to behave as right-hand side operands of this operator. + "a" !~ ["a","b","c","d","e",f"] returns true. +
          Starts With=^ + The =^ operator is a short-hand for the 'startsWith' method. + For example, "abcdef" =^ "abc" returns true. + Note that through duck-typing, user classes exposing a public 'startsWith' method will allow their instances + to behave as left-hand side operands of this operator. +
          Not Starts With!^ + This is the negation of the 'starts with' operator. + a !^ "abc" is equivalent to !(a =^ "abc") +
          Ends With=$The =$ operator is a short-hand for the 'endsWith' method. + For example, "abcdef" =$ "def" returns true. + Note that through duck-typing, user classes exposing an 'endsWith' method will allow their instances + to behave as left-hand side operands of this operator. +
          Not Ends With!$ + This is the negation of the 'ends with' operator. + a !$ "abc" is equivalent to !(a =$ "abc") +
          Addition + The usual + operator is used. + For example + val1 + val2 +
          Subtraction + The usual - operator is used. + For example + val1 - val2 +
          Multiplication + The usual * operator is used. + For example + val1 * val2 +
          Division + The usual / operator is used, or one can use the div operator. + For example + val1 / val2 + or + val1 div val2 +
          Modulus (or remainder) + The % operator is used. An alternative is the mod + operator. + For example + 5 mod 2 gives 1 and is equivalent to 5 % 2 +
          Side-effect operators + Some operators exist in side-effect forms. + Their default behavior is to execute the operator and assign the left-hand side with the result. + For instance a += 2 is equivalent to a = a + 2 + The list of operators is: +
            +
          • +=
          • +
          • -=
          • +
          • *=
          • +
          • /=
          • +
          • %=
          • +
          • &=
          • +
          • |=
          • +
          • ^=
          • +
          +
          Negation + The unary - operator is used. + For example + -12 +
          +
          +
          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          OperatorDescription
          Array access + Array elements may be accessed using either square brackets or a dotted numeral, e.g. + arr1[0] and arr1.0 are equivalent +
          List access + List elements may be accessed using either square brackets or a dotted numeral, e.g. + list[0] and list.0 are equivalent +
          Map access + Map elements are accessed using square brackets, e.g. + map[0]; map['name']; map[var]; + Note that map['7'] and map[7] refer to different elements. + Map elements with a numeric key may also be accessed using a dotted numeral, e.g. + map[0] and map.0 are equivalent. +

          Note that map.1 and map.01 refer to different elements, + while map.1 and map[01] are equivalent.

          +
          JavaBean property access + Properties of JavaBean objects that define appropriate getter methods can be accessed + using either square brackets or a dotted numeral, e.g. + foo['bar'] and foo.bar are equivalent. + The appropriate Foo.getBar() method will be called. +

          Note that both foo.Bar and foo.bar can be used

          +
          Indexed JavaBean property access + Indexed properties of JavaBean objects that define appropriate getter methods can be accessed + using either square brackets or a dotted numeral, e.g. + x.attribute['name'] and x.attribute.name are equivalent. + The appropriate Foo.getAttribute(String index) method will be called +
          Public field access + Public fields of java objects can be accessed using either square brackets or a dotted numeral, e.g. + foo['bar'] and foo.bar are equivalent. +
          Duck-typed collection property access + Properties of Java classes that define public Object get(String name) method can be accessed + using either square brackets or a dotted numeral, e.g. + foo['bar'] and foo.bar are equivalent. + The appropriate Foo.get(String index) method will be called with the argument of "bar" String +
          +
          +
          + + + + + + + + + + + + + + + + + + + + + + + + + +
          StatementDescription
          if + Classic, if/else statement, e.g. + if ((x * 2) == 5) { + y = 1; + } else { + y = 2; + } +
          for +

          Loop through items of an Array, Collection, Map, Iterator or Enumeration, e.g. + for (item : list) { + x = x + item; + } + Where item is a context variable.

          +

          The following syntax is also supported: + for (var item : list) { + x = x + item; + } + + Where item is a local variable.

          +

          Note that item variable is accessible after loop evaluation

          +

          The JEXL 1.1 syntax using foreach(item in list) is now unsupported.

          +
          while + Loop until a condition is satisfied, e.g. + while (x lt 10) { + x = x + 2; + } +
          continue + Within loops (while/for), allows to skip to the next iteration. +
          break + Allows to break from a loop (while/for) inconditionally. +
          +
          - + diff --git a/src/test/java/org/apache/commons/jexl3/AnnotationTest.java b/src/test/java/org/apache/commons/jexl3/AnnotationTest.java new file mode 100644 index 000000000..fc03571be --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/AnnotationTest.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.Callable; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test cases for annotations. + * @since 3.1 + */ +@SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) + +public class AnnotationTest extends JexlTestCase { + + public AnnotationTest() { + super("AnnotationTest"); + } + + @Test + public void test197a() throws Exception { + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().create(); + JexlScript e = jexl.createScript("@synchronized { return 42; }"); + Object r = e.execute(jc); + Assert.assertEquals(42, r); + } + + public static class AnnotationContext extends MapContext implements JexlContext.AnnotationProcessor { + private int count = 0; + private final Set names = new TreeSet(); + + @Override + public Object processAnnotation(String name, Object[] args, Callable statement) throws Exception { + count += 1; + names.add(name); + if ("one".equals(name)) { + names.add(args[0].toString()); + } else if ("two".equals(name)) { + names.add(args[0].toString()); + names.add(args[1].toString()); + } else if ("error".equals(name)) { + names.add(args[0].toString()); + throw new IllegalArgumentException(args[0].toString()); + } else if ("unknown".equals(name)) { + return null; + } + return statement.call(); + } + + public int getCount() { + return count; + } + + public Set getNames() { + return names; + } + } + + public class OptAnnotationContext extends JexlEvalContext implements JexlContext.AnnotationProcessor { + @Override + public Object processAnnotation(String name, Object[] args, Callable statement) throws Exception { + // transient side effect for strict + if ("strict".equals(name)) { + boolean s = (Boolean) args[0]; + boolean b = this.isStrict(); + setStrict(s); + Object r = statement.call(); + setStrict(b); + return r; + } + // transient side effect for silent + if ("silent".equals(name)) { + if (args == null || args.length == 0) { + boolean b = this.isSilent(); + try { + return statement.call(); + } catch(JexlException xjexl) { + return null; + } finally { + setSilent(b); + } + } else { + boolean s = (Boolean) args[0]; + boolean b = this.isSilent(); + setSilent(s); + Object r = statement.call(); + setSilent(b); + return r; + } + } + // durable side effect for scale + if ("scale".equals(name)) { + this.setMathScale((Integer) args[0]); + return statement.call(); + } + return statement.call(); + } + } + + @Test + public void testVarStmt() throws Exception { + OptAnnotationContext jc = new OptAnnotationContext(); + JexlEngine jexl = new JexlBuilder().strict(true).silent(false).create(); + jc.setOptions(jexl); + JexlScript e; + Object r; + e = jexl.createScript("(s, v)->{ @strict(s) @silent(v) var x = y ; 42; }"); + + // wont make an error + try { + r = e.execute(jc, false, true); + Assert.assertEquals(42, r); + } catch (JexlException.Variable xjexl) { + Assert.fail("should not have thrown"); + } + + r = null; + // will make an error and throw + try { + r = e.execute(jc, true, false); + Assert.fail("should have thrown"); + } catch (JexlException.Variable xjexl) { + Assert.assertNull(r); + } + + r = null; + // will make an error and will not throw but result is null + try { + r = e.execute(jc, true, true); + Assert.assertEquals(null, r); + } catch (JexlException.Variable xjexl) { + Assert.fail("should not have thrown"); + } + + r = null; + // will not make an error and will not throw + try { + r = e.execute(jc, false, false); + Assert.assertEquals(42, r); + } catch (JexlException.Variable xjexl) { + Assert.fail("should not have thrown"); + } + //Assert.assertEquals(42, r); + Assert.assertTrue(jc.isStrict()); + e = jexl.createScript("@scale(5) 42;"); + r = e.execute(jc); + Assert.assertEquals(42, r); + Assert.assertTrue(jc.isStrict()); + Assert.assertEquals(5, jc.getArithmeticMathScale()); + } + + @Test + public void testNoArg() throws Exception { + AnnotationContext jc = new AnnotationContext(); + JexlEngine jexl = new JexlBuilder().create(); + JexlScript e = jexl.createScript("@synchronized { return 42; }"); + Object r = e.execute(jc); + Assert.assertEquals(42, r); + Assert.assertEquals(1, jc.getCount()); + Assert.assertTrue(jc.getNames().contains("synchronized")); + } + + @Test + public void testNoArgExpression() throws Exception { + AnnotationContext jc = new AnnotationContext(); + JexlEngine jexl = new JexlBuilder().create(); + JexlScript e = jexl.createScript("@synchronized 42"); + Object r = e.execute(jc); + Assert.assertEquals(42, r); + Assert.assertEquals(1, jc.getCount()); + Assert.assertTrue(jc.getNames().contains("synchronized")); + } + + + @Test + public void testOneArg() throws Exception { + AnnotationContext jc = new AnnotationContext(); + JexlEngine jexl = new JexlBuilder().create(); + JexlScript e = jexl.createScript("@one(1) { return 42; }"); + Object r = e.execute(jc); + Assert.assertEquals(42, r); + Assert.assertEquals(1, jc.getCount()); + Assert.assertTrue(jc.getNames().contains("one")); + Assert.assertTrue(jc.getNames().contains("1")); + } + + @Test + public void testMultiple() throws Exception { + AnnotationContext jc = new AnnotationContext(); + JexlEngine jexl = new JexlBuilder().create(); + JexlScript e = jexl.createScript("@one(1) @synchronized { return 42; }"); + Object r = e.execute(jc); + Assert.assertEquals(42, r); + Assert.assertEquals(2, jc.getCount()); + Assert.assertTrue(jc.getNames().contains("synchronized")); + Assert.assertTrue(jc.getNames().contains("one")); + Assert.assertTrue(jc.getNames().contains("1")); + } + + @Test + public void testError() throws Exception { + testError(true); + testError(false); + } + + private void testError(boolean silent) throws Exception { + CaptureLog log = new CaptureLog(); + AnnotationContext jc = new AnnotationContext(); + JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).create(); + JexlScript e = jexl.createScript("@error('42') { return 42; }"); + try { + Object r = e.execute(jc); + if (!silent) { + Assert.fail("should have failed"); + } else { + Assert.assertEquals(1, log.count("warn")); + } + } catch (JexlException.Annotation xjexl) { + Assert.assertEquals("error", xjexl.getAnnotation()); + } + Assert.assertEquals(1, jc.getCount()); + Assert.assertTrue(jc.getNames().contains("error")); + Assert.assertTrue(jc.getNames().contains("42")); + if (!silent) { + Assert.assertEquals(0, log.count("warn")); + } + } + + @Test + public void testUnknown() throws Exception { + testUnknown(true); + testUnknown(false); + } + + private void testUnknown(boolean silent) throws Exception { + CaptureLog log = new CaptureLog(); + AnnotationContext jc = new AnnotationContext(); + JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).create(); + JexlScript e = jexl.createScript("@unknown('42') { return 42; }"); + try { + Object r = e.execute(jc); + if (!silent) { + Assert.fail("should have failed"); + } else { + Assert.assertEquals(1, log.count("warn")); + } + } catch (JexlException.Annotation xjexl) { + Assert.assertEquals("unknown", xjexl.getAnnotation()); + } + Assert.assertEquals(1, jc.getCount()); + Assert.assertTrue(jc.getNames().contains("unknown")); + Assert.assertFalse(jc.getNames().contains("42")); + if (!silent) { + Assert.assertEquals(0, log.count("warn")); + } + } +} diff --git a/src/test/java/org/apache/commons/jexl3/AntishCallTest.java b/src/test/java/org/apache/commons/jexl3/AntishCallTest.java new file mode 100644 index 000000000..5e9814933 --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/AntishCallTest.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.util.Map; +import java.util.TreeMap; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test cases for calling antish variables as method names (JEXL-240); + * Also tests that a class instance is a functor that invokes the constructor when called. + */ +@SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) +public class AntishCallTest extends JexlTestCase { + + public AntishCallTest() { + super("AntishCallTest"); + } + + /** + * Wraps a class. + */ + public class ClassReference { + final Class clazz; + ClassReference(Class c) { + this.clazz = c; + } + } + + /** + * Considers any call using a class reference as functor as a call to its constructor. + *

          Note that before 3.2, a class was not considered a functor. + * @param clazz the class we seek to instantiate + * @param args the constructor arguments + * @return an instance if that was possible + */ + public static Object callConstructor(JexlEngine engine, ClassReference ref, Object... args) { + return callConstructor(engine, ref.clazz, args); + } + public static Object callConstructor(JexlEngine engine, Class clazz, Object... args) { + if (clazz == null || clazz.isPrimitive() || clazz.isInterface() + || clazz.isMemberClass() || clazz.isAnnotation() || clazz.isArray()) { + throw new ArithmeticException("not a constructible object"); + } + JexlEngine jexl = engine; + if (jexl == null) { + jexl = JexlEngine.getThreadEngine(); + if (jexl == null) { + throw new ArithmeticException("no engine to solve constructor"); + } + } + return jexl.newInstance(clazz, args); + } + + /** + * An arithmetic that considers class objects as callable. + */ + public class CallSupportArithmetic extends JexlArithmetic { + public CallSupportArithmetic(boolean strict) { + super(strict); + } + + public Object call(ClassReference clazz, Object... args) { + return callConstructor(null, clazz, args); + } + + public Object call(Class clazz, Object... args) { + return callConstructor(null, clazz, args); + } + } + + /** + * A context that considers class references as callable. + */ + public static class CallSupportContext extends MapContext { + CallSupportContext(Map map) { + super(map); + } + private JexlEngine engine; + + @Override public Object get(String str) { + if (!super.has(str)) { + try { + return CallSupportContext.class.getClassLoader().loadClass(str); + } catch(Exception xany) { + return null; + } + } + return super.get(str); + } + + @Override public boolean has(String str) { + if (!super.has(str)){ + try { + return CallSupportContext.class.getClassLoader().loadClass(str) != null; + } catch(Exception xany) { + return false; + } + } + return true; + } + + CallSupportContext engine(JexlEngine j) { + engine = j; + return this; + } + + public Object call(ClassReference clazz, Object... args) { + return callConstructor(engine, clazz, args); + } + + public Object call(Class clazz, Object... args) { + return callConstructor(engine, clazz, args); + } + } + + @Test + public void testAntishContextVar() throws Exception { + JexlEngine jexl = new JexlBuilder().cache(512).strict(true).silent(false).create(); + Map lmap = new TreeMap(); + JexlContext jc = new CallSupportContext(lmap).engine(jexl); + runTestCall(jexl, jc); + lmap.put("java.math.BigInteger", new ClassReference(java.math.BigInteger.class)); + runTestCall(jexl, jc); + lmap.remove("java.math.BigInteger"); + runTestCall(jexl, jc); + } + + @Test + public void testAntishArithmetic() throws Exception { + CallSupportArithmetic ja = new CallSupportArithmetic(true); + JexlEngine jexl = new JexlBuilder().cache(512).strict(true).silent(false).arithmetic(ja).create(); + Map lmap = new TreeMap(); + JexlContext jc = new MapContext(lmap); + lmap.put("java.math.BigInteger", java.math.BigInteger.class); + runTestCall(jexl, jc); + lmap.put("java.math.BigInteger", new ClassReference(java.math.BigInteger.class)); + runTestCall(jexl, jc); + lmap.remove("java.math.BigInteger"); + try { + runTestCall(jexl, jc); + Assert.fail("should have failed"); + } catch(JexlException xjexl) { + // + } + } + + void runTestCall(JexlEngine jexl, JexlContext jc) throws Exception { + JexlScript check1 = jexl.createScript("var x = java.math.BigInteger; x('1234')"); + JexlScript check2 = jexl.createScript("java.math.BigInteger('4321')"); + + Object o1 = check1.execute(jc); + Assert.assertEquals("Result is not 1234", new java.math.BigInteger("1234"), o1); + + Object o2 = check2.execute(jc); + Assert.assertEquals("Result is not 4321", new java.math.BigInteger("4321"), o2); + } + +} \ No newline at end of file diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java index 961ac9f52..0a03a0b40 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java @@ -71,6 +71,12 @@ public void testRegexp() throws Exception { asserter.assertExpression("str !~ match", Boolean.FALSE); asserter.assertExpression("str !~ nomatch", Boolean.TRUE); asserter.assertExpression("str =~ nomatch", Boolean.FALSE); + asserter.setVariable("match", new StringBuilder("abc.*")); + asserter.setVariable("nomatch", new StringBuilder(".*123")); + asserter.assertExpression("str =~ match", Boolean.TRUE); + asserter.assertExpression("str !~ match", Boolean.FALSE); + asserter.assertExpression("str !~ nomatch", Boolean.TRUE); + asserter.assertExpression("str =~ nomatch", Boolean.FALSE); asserter.setVariable("match", java.util.regex.Pattern.compile("abc.*")); asserter.setVariable("nomatch", java.util.regex.Pattern.compile(".*123")); asserter.assertExpression("str =~ match", Boolean.TRUE); @@ -94,6 +100,16 @@ public void testStartsEndsWithString() throws Exception { asserter.assertExpression("x =$ 'foo'", Boolean.TRUE); } + @Test + public void testStartsEndsWithStringDot() throws Exception { + asserter.setVariable("x.y", "foobar"); + asserter.assertExpression("x.y =^ 'foo'", Boolean.TRUE); + asserter.assertExpression("x.y =$ 'foo'", Boolean.FALSE); + asserter.setVariable("x.y", "barfoo"); + asserter.assertExpression("x.y =^ 'foo'", Boolean.FALSE); + asserter.assertExpression("x.y =$ 'foo'", Boolean.TRUE); + } + @Test public void testNotStartsEndsWithString() throws Exception { asserter.setVariable("x", "foobar"); @@ -104,6 +120,36 @@ public void testNotStartsEndsWithString() throws Exception { asserter.assertExpression("x !$ 'foo'", Boolean.FALSE); } + @Test + public void testNotStartsEndsWithStringDot() throws Exception { + asserter.setVariable("x.y", "foobar"); + asserter.assertExpression("x.y !^ 'foo'", Boolean.FALSE); + asserter.assertExpression("x.y !$ 'foo'", Boolean.TRUE); + asserter.setVariable("x.y", "barfoo"); + asserter.assertExpression("x.y !^ 'foo'", Boolean.TRUE); + asserter.assertExpression("x.y !$ 'foo'", Boolean.FALSE); + } + + @Test + public void testStartsEndsWithStringBuilder() throws Exception { + asserter.setVariable("x", new StringBuilder("foobar")); + asserter.assertExpression("x =^ 'foo'", Boolean.TRUE); + asserter.assertExpression("x =$ 'foo'", Boolean.FALSE); + asserter.setVariable("x", new StringBuilder("barfoo")); + asserter.assertExpression("x =^ 'foo'", Boolean.FALSE); + asserter.assertExpression("x =$ 'foo'", Boolean.TRUE); + } + + @Test + public void testNotStartsEndsWithStringBuilder() throws Exception { + asserter.setVariable("x", new StringBuilder("foobar")); + asserter.assertExpression("x !^ 'foo'", Boolean.FALSE); + asserter.assertExpression("x !$ 'foo'", Boolean.TRUE); + asserter.setVariable("x", new StringBuilder("barfoo")); + asserter.assertExpression("x !^ 'foo'", Boolean.TRUE); + asserter.assertExpression("x !$ 'foo'", Boolean.FALSE); + } + public static class MatchingContainer { private final Set values; @@ -349,6 +395,10 @@ public Object arraySet(Date date, String identifier, Object value) throws Except public Date now() { return new Date(System.currentTimeMillis()); } + + public Date multiply(Date d0, Date d1) { + throw new ArithmeticException("unsupported"); + } } public static class DateContext extends MapContext { @@ -368,6 +418,34 @@ public String format(Number number, String fmt) { } } + @Test + public void testOperatorError() throws Exception { + testOperatorError(true); + testOperatorError(false); + } + + private void testOperatorError(boolean silent) throws Exception { + CaptureLog log = new CaptureLog(); + DateContext jc = new DateContext(); + Date d = new Date(); + JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).cache(32) + .arithmetic(new DateArithmetic(true)).create(); + JexlScript expr0 = jexl.createScript("date * date", "date"); + try { + Object value0 = expr0.execute(jc, d); + if (!silent) { + Assert.fail("should have failed"); + } else { + Assert.assertEquals(1, log.count("warn")); + } + } catch(JexlException.Operator xop) { + Assert.assertEquals("*", xop.getSymbol()); + } + if (!silent) { + Assert.assertEquals(0, log.count("warn")); + } + } + @Test public void testDateArithmetic() throws Exception { Date d = new Date(); diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java index 7b7802ba5..f521088cc 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -25,6 +25,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicBoolean; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.junit.Assert; @@ -304,6 +305,7 @@ public void testBigExponentLiterals() throws Exception { } // JEXL-24: doubles with exponent + @Test public void test2DoubleLiterals() throws Exception { JexlEvalContext ctxt = new JexlEvalContext(); ctxt.setStrictArithmetic(true); @@ -428,6 +430,7 @@ public void testMultClass() throws Exception { Assert.assertEquals(java.math.BigDecimal.class, r1.getClass()); } + @Test public void testDivClass() throws Exception { JexlEngine jexl = new JexlBuilder().create(); JexlContext jc = new MapContext(); @@ -439,6 +442,7 @@ public void testDivClass() throws Exception { Assert.assertEquals(java.math.BigDecimal.class, r1.getClass()); } + @Test public void testPlusClass() throws Exception { JexlEngine jexl = new JexlBuilder().create(); JexlContext jc = new MapContext(); @@ -450,6 +454,7 @@ public void testPlusClass() throws Exception { Assert.assertEquals(java.math.BigDecimal.class, r1.getClass()); } + @Test public void testMinusClass() throws Exception { JexlEngine jexl = new JexlBuilder().create(); JexlContext jc = new MapContext(); @@ -537,6 +542,22 @@ public void testAddWithStringsStrict() throws Exception { Assert.assertEquals("1.21.2", result); } + @Test + public void testOption() throws Exception { + Map vars = new HashMap(); + JexlEvalContext context = new JexlEvalContext(vars); + JexlScript script = JEXL.createScript("0 + '1.2' "); + Object result; + + context.setStrictArithmetic(true); + result = script.execute(context); + Assert.assertEquals("01.2", result); + + context.setStrictArithmetic(false); + result = script.execute(context); + Assert.assertEquals(1.2d, (Double) result, EPSILON); + } + @Test public void testIsFloatingPointPattern() throws Exception { JexlArithmetic ja = new JexlArithmetic(true); @@ -548,11 +569,24 @@ public void testIsFloatingPointPattern() throws Exception { Assert.assertFalse(ja.isFloatingPointNumber("+10.2a+34")); Assert.assertFalse(ja.isFloatingPointNumber("0")); Assert.assertFalse(ja.isFloatingPointNumber("1")); + Assert.assertFalse(ja.isFloatingPointNumber("12A")); + Assert.assertFalse(ja.isFloatingPointNumber("2F3")); + Assert.assertFalse(ja.isFloatingPointNumber("23")); + Assert.assertFalse(ja.isFloatingPointNumber("+3")); + Assert.assertFalse(ja.isFloatingPointNumber("+34")); + Assert.assertFalse(ja.isFloatingPointNumber("+3-4")); + Assert.assertFalse(ja.isFloatingPointNumber("+3.-4")); + Assert.assertFalse(ja.isFloatingPointNumber("3ee4")); Assert.assertTrue(ja.isFloatingPointNumber("0.")); Assert.assertTrue(ja.isFloatingPointNumber("1.")); Assert.assertTrue(ja.isFloatingPointNumber("1.2")); Assert.assertTrue(ja.isFloatingPointNumber("1.2e3")); + Assert.assertTrue(ja.isFloatingPointNumber("2e3")); + Assert.assertTrue(ja.isFloatingPointNumber("+2e-3")); + Assert.assertTrue(ja.isFloatingPointNumber("+23E-34")); + Assert.assertTrue(ja.isFloatingPointNumber("+23.E-34")); + Assert.assertTrue(ja.isFloatingPointNumber("-23.4E+45")); Assert.assertTrue(ja.isFloatingPointNumber("1.2e34")); Assert.assertTrue(ja.isFloatingPointNumber("10.2e34")); Assert.assertTrue(ja.isFloatingPointNumber("+10.2e34")); @@ -599,8 +633,12 @@ public void testEmpty() throws Exception { "var x = []; return empty(x);", true, "var x = [1, 2]; return empty(x);", false, "var x = ['a', 'b']; return empty(x);", false, + "var x = [...]; return empty(x);", true, + "var x = [1, 2,...]; return empty(x);", false, "var x = {:}; return empty(x);", true, - "var x = {1:'A', 2:'B'}; return empty(x);", false + "var x = {1:'A', 2:'B'}; return empty(x);", false, + "var x = {}; return empty(x);", true, + "var x = {'A','B'}; return empty(x);", false }; JexlEngine jexl = new JexlBuilder().create(); JexlContext jc = new EmptyTestContext(); @@ -1297,4 +1335,74 @@ public void testCoerceBigDecimal() throws Exception { Assert.assertEquals(BigDecimal.valueOf(0.), ja.toBigDecimal(false)); } + @Test + public void testAtomicBoolean() throws Exception { + // in a condition + JexlScript e = JEXL.createScript("if (x) 1 else 2;", "x"); + JexlContext jc = new MapContext(); + AtomicBoolean ab = new AtomicBoolean(false); + Object o; + o = e.execute(jc, ab); + Assert.assertEquals("Result is not 2", new Integer(2), o); + ab.set(true); + o = e.execute(jc, ab); + Assert.assertEquals("Result is not 1", new Integer(1), o); + // in a binary logical op + e = JEXL.createScript("x && y", "x", "y"); + ab.set(true); + o = e.execute(jc, ab, Boolean.FALSE); + Assert.assertFalse((Boolean) o); + ab.set(true); + o = e.execute(jc, ab, Boolean.TRUE); + Assert.assertTrue((Boolean) o); + ab.set(false); + o = e.execute(jc, ab, Boolean.FALSE); + Assert.assertFalse((Boolean) o); + ab.set(false); + o = e.execute(jc, ab, Boolean.FALSE); + Assert.assertFalse((Boolean) o); + // in arithmetic op + e = JEXL.createScript("x + y", "x", "y"); + ab.set(true); + o = e.execute(jc, ab, 10); + Assert.assertEquals(11, o); + o = e.execute(jc, 10, ab); + Assert.assertEquals(11, o); + o = e.execute(jc, ab, 10.d); + Assert.assertEquals(11.d, (Double) o, EPSILON); + o = e.execute(jc, 10.d, ab); + Assert.assertEquals(11.d, (Double) o, EPSILON); + + BigInteger bi10 = BigInteger.TEN; + ab.set(false); + o = e.execute(jc, ab, bi10); + Assert.assertEquals(bi10, o); + o = e.execute(jc, bi10, ab); + Assert.assertEquals(bi10, o); + + BigDecimal bd10 = BigDecimal.TEN; + ab.set(false); + o = e.execute(jc, ab, bd10); + Assert.assertEquals(bd10, o); + o = e.execute(jc, bd10, ab); + Assert.assertEquals(bd10, o); + + // in a (the) monadic op + e = JEXL.createScript("!x", "x"); + ab.set(true); + o = e.execute(jc, ab); + Assert.assertFalse((Boolean) o); + ab.set(false); + o = e.execute(jc, ab); + Assert.assertTrue((Boolean) o); + + // in a (the) monadic op + e = JEXL.createScript("-x", "x"); + ab.set(true); + o = e.execute(jc, ab); + Assert.assertFalse((Boolean) o); + ab.set(false); + o = e.execute(jc, ab); + Assert.assertTrue((Boolean) o); + } } diff --git a/src/test/java/org/apache/commons/jexl3/ArrayAccessTest.java b/src/test/java/org/apache/commons/jexl3/ArrayAccessTest.java index fa1fadb15..a42b330f1 100644 --- a/src/test/java/org/apache/commons/jexl3/ArrayAccessTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArrayAccessTest.java @@ -59,6 +59,7 @@ public void setUp() { /** * test simple array access */ + @Test public void testArrayAccess() throws Exception { /* @@ -190,6 +191,7 @@ public void testDoubleMaps() throws Exception { asserter.assertExpression("foo.0.1", "three"); } + @Test public void testArrayProperty() throws Exception { Foo foo = new Foo(); @@ -202,6 +204,7 @@ public void testArrayProperty() throws Exception { } // This is JEXL-26 + @Test public void testArrayAndDottedConflict() throws Exception { Object[] objects = new Object[] {"an", "array", new Long(0)}; asserter.setStrict(false); @@ -216,6 +219,7 @@ public void testArrayAndDottedConflict() throws Exception { asserter.assertExpression("base.objects.1.status", null); } + @Test public void testArrayIdentifierParsing() throws Exception { Map map = new HashMap(); map.put("00200", -42.42d); @@ -228,6 +232,7 @@ public void testArrayIdentifierParsing() throws Exception { asserter.assertExpression("objects.200", 42.42d); } + @Test public void testArrayMethods() throws Exception { Object[] objects = new Object[] {"an", "array", new Long(0)}; @@ -239,6 +244,7 @@ public void testArrayMethods() throws Exception { asserter.assertExpression("objects[1]", "dion"); } + @Test public void testArrayArray() throws Exception { Integer i42 = Integer.valueOf(42); Integer i43 = Integer.valueOf(43); @@ -304,4 +310,23 @@ public void testArrayArray() throws Exception { asserter.assertExpression("foo[zero][zero][two]", s42); } } + + public static class Sample { + private int[] array; + public void setFoo(int[] a) { + array = a; + } + public int[] getFoo() { + return array; + } + } + @Test + public void testArrayGetSet() throws Exception { + Sample bar = new Sample(); + bar.setFoo(new int[]{24}); + asserter.setVariable("bar", bar); + asserter.assertExpression("bar.foo[0]", 24); + asserter.assertExpression("bar.foo = []", new int[0]); + //asserter.assertExpression("bar.foo[0]", 42); + } } \ No newline at end of file diff --git a/src/test/java/org/apache/commons/jexl3/AssignTest.java b/src/test/java/org/apache/commons/jexl3/AssignTest.java index bb8584930..7b13ed34d 100644 --- a/src/test/java/org/apache/commons/jexl3/AssignTest.java +++ b/src/test/java/org/apache/commons/jexl3/AssignTest.java @@ -109,6 +109,7 @@ public void testBeanish() throws Exception { Assert.assertEquals("Result is not 10", new Integer(10), o); } + @Test public void testAmbiguous() throws Exception { JexlExpression assign = JEXL.createExpression("froboz.nosuchbean = 10"); JexlContext jc = new MapContext(); diff --git a/src/test/java/org/apache/commons/jexl3/CaptureLog.java b/src/test/java/org/apache/commons/jexl3/CaptureLog.java new file mode 100644 index 000000000..2ae0aa655 --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/CaptureLog.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016 The Apache Software Foundation. + * + * 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. + */ +package org.apache.commons.jexl3; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.logging.Log; + +/** + * A log implementation to help control tests results. + */ +public class CaptureLog implements Log { + private List captured = new ArrayList(); + + static Object caller() { + StackTraceElement[] stack = new Exception().fillInStackTrace().getStackTrace(); + return stack[2]; + } + + public boolean isEmpty() { + return captured.isEmpty(); + } + + public int count(String type) { + int count = 0; + for (Object[] l : captured) { + if (type.equals(l[0].toString())) { + count += 1; + } + } + return count; + } + + @Override + public void debug(Object o) { + captured.add(new Object[]{"debug", caller(), o}); + } + + @Override + public void debug(Object o, Throwable thrwbl) { + captured.add(new Object[]{"debug", caller(), o, thrwbl}); + } + + @Override + public void error(Object o) { + captured.add(new Object[]{"error", caller(), o}); + } + + @Override + public void error(Object o, Throwable thrwbl) { + captured.add(new Object[]{"error", caller(), o, thrwbl}); + } + + @Override + public void fatal(Object o) { + captured.add(new Object[]{"fatal", caller(), o}); + } + + @Override + public void fatal(Object o, Throwable thrwbl) { + captured.add(new Object[]{"fatal", caller(), o, thrwbl}); + } + + @Override + public void info(Object o) { + captured.add(new Object[]{"info", caller(), o}); + } + + @Override + public void info(Object o, Throwable thrwbl) { + captured.add(new Object[]{"info", caller(), o, thrwbl}); + } + + @Override + public boolean isDebugEnabled() { + return true; + } + + @Override + public boolean isErrorEnabled() { + return true; + } + + @Override + public boolean isFatalEnabled() { + return true; + } + + @Override + public boolean isInfoEnabled() { + return true; + } + + @Override + public boolean isTraceEnabled() { + return true; + } + + @Override + public boolean isWarnEnabled() { + return true; + } + + @Override + public void trace(Object o) { + captured.add(new Object[]{"trace", caller(), o}); + } + + @Override + public void trace(Object o, Throwable thrwbl) { + captured.add(new Object[]{"trace", caller(), o, thrwbl}); + } + + @Override + public void warn(Object o) { + captured.add(new Object[]{"warn", caller(), o}); + } + + @Override + public void warn(Object o, Throwable thrwbl) { + captured.add(new Object[]{"warn", caller(), o, thrwbl}); + } + +} diff --git a/src/test/java/org/apache/commons/jexl3/ContextNamespaceTest.java b/src/test/java/org/apache/commons/jexl3/ContextNamespaceTest.java index 9a354d3a9..48f41d610 100644 --- a/src/test/java/org/apache/commons/jexl3/ContextNamespaceTest.java +++ b/src/test/java/org/apache/commons/jexl3/ContextNamespaceTest.java @@ -71,4 +71,52 @@ public void testThreadedContext() throws Exception { Assert.assertEquals(186., result); } + public static class Vat { + private double vat; + + Vat(double vat) { + this.vat = vat; + } + + public double getVAT() { + return vat; + } + + public void setVAT(double vat) { + this.vat = vat; + } + + public double getvat() { + throw new UnsupportedOperationException("no way"); + } + + public void setvat(double vat) { + throw new UnsupportedOperationException("no way"); + } + } + + @Test + public void testObjectContext() throws Exception { + JexlEngine jexl = new JexlBuilder().strict(true).silent(false).create(); + Vat vat = new Vat(18.6); + ObjectContext ctxt = new ObjectContext(jexl, vat); + Assert.assertEquals(18.6d, (Double) ctxt.get("VAT"), 0.0001d); + ctxt.set("VAT", 20.0d); + Assert.assertEquals(20.0d, (Double) ctxt.get("VAT"), 0.0001d); + + try { + ctxt.get("vat"); + Assert.fail("should have failed"); + } catch(JexlException.Property xprop) { + // + } + + try { + ctxt.set("vat", 33.0d); + Assert.fail("should have failed"); + } catch(JexlException.Property xprop) { + // + } + } + } diff --git a/src/test/java/org/apache/commons/jexl3/ExceptionTest.java b/src/test/java/org/apache/commons/jexl3/ExceptionTest.java index 003aa1353..b565effad 100644 --- a/src/test/java/org/apache/commons/jexl3/ExceptionTest.java +++ b/src/test/java/org/apache/commons/jexl3/ExceptionTest.java @@ -31,15 +31,30 @@ public ExceptionTest() { } public static class ThrowNPE { - public String method() { + boolean doThrow = false; + public String npe() { throw new NullPointerException("ThrowNPE"); } + + public void setFail(boolean f) { + doThrow = f; + if (f) { + throw new NullPointerException("ThrowNPE/set"); + } + } + + public boolean getFail() { + if (doThrow) { + throw new NullPointerException("ThrowNPE/get"); + } + return doThrow; + } } @Test public void testWrappedEx() throws Exception { JexlEngine jexl = new Engine(); - JexlExpression e = jexl.createExpression("method()"); + JexlExpression e = jexl.createExpression("npe()"); JexlContext jc = new ObjectContext(jexl, new ThrowNPE()); try { e.evaluate(jc); @@ -50,6 +65,60 @@ public void testWrappedEx() throws Exception { } } + @Test + public void testWrappedExmore() throws Exception { + JexlEngine jexl = new Engine(); + ThrowNPE npe = new ThrowNPE(); + try { + Object r = jexl.getProperty(npe, "foo"); + Assert.fail("Should have thrown JexlException.Property"); + } catch (JexlException.Property xany) { + Throwable xth = xany.getCause(); + Assert.assertNull(xth); + } + try { + jexl.setProperty(npe, "foo", 42); + Assert.fail("Should have thrown JexlException.Property"); + } catch (JexlException.Property xany) { + Throwable xth = xany.getCause(); + Assert.assertNull(xth); + } + + boolean b = (Boolean) jexl.getProperty(npe, "fail"); + Assert.assertFalse(b); + try { + jexl.setProperty(npe, "fail", false); + jexl.setProperty(npe, "fail", true); + Assert.fail("Should have thrown JexlException.Property"); + } catch (JexlException.Property xany) { + Throwable xth = xany.getCause(); + Assert.assertEquals(NullPointerException.class, xth.getClass()); + } + try { + jexl.getProperty(npe, "fail"); + Assert.fail("Should have thrown JexlException.Property"); + } catch (JexlException.Property xany) { + Throwable xth = xany.getCause(); + Assert.assertEquals(NullPointerException.class, xth.getClass()); + } + + try { + jexl.invokeMethod(npe, "foo", 42); + Assert.fail("Should have thrown JexlException.Method"); + } catch (JexlException.Method xany) { + Throwable xth = xany.getCause(); + Assert.assertNull(xth); + } + try { + jexl.invokeMethod(npe, "npe"); + Assert.fail("Should have thrown NullPointerException"); + } catch (JexlException.Method xany) { + Throwable xth = xany.getCause(); + Assert.assertEquals(NullPointerException.class, xth.getClass()); + } + } + + // Unknown vars and properties versus null operands @Test public void testEx() throws Exception { @@ -160,4 +229,52 @@ public void testExMethod() throws Exception { Assert.assertTrue(msg.indexOf("c.e") > 0); } } + + + @Test + public void test206() throws Exception { + String src = "null.1 = 2; return 42"; + doTest206(src, false, false); + doTest206(src, false, true); + doTest206(src, true, false); + doTest206(src, true, true); + src = "x = null.1; return 42"; + doTest206(src, false, false); + doTest206(src, false, true); + doTest206(src, true, false); + doTest206(src, true, true); + src = "x = y.1; return 42"; + doTest206(src, false, false); + doTest206(src, false, true); + doTest206(src, true, false); + doTest206(src, true, true); + } + private void doTest206(String src, boolean strict, boolean silent) throws Exception { + CaptureLog l = new CaptureLog(); + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().logger(l).strict(strict).silent(silent).create(); + JexlScript e; + Object r = -1; + e = jexl.createScript(src); + try { + r = e.execute(jc); + if (strict && !silent) { + Assert.fail("should have thrown an exception"); + } + } catch(JexlException xjexl) { + if (!strict || silent) { + Assert.fail("should not have thrown an exception"); + } + } + if (strict) { + if (silent && l.count("warn") == 0) { + Assert.fail("should have generated a warning"); + } + } else { + if (l.count("debug") == 0) { + Assert.fail("should have generated a debug"); + } + Assert.assertEquals(42, r); + } + } } diff --git a/src/test/java/org/apache/commons/jexl3/FeaturesTest.java b/src/test/java/org/apache/commons/jexl3/FeaturesTest.java new file mode 100644 index 000000000..3a64362c4 --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/FeaturesTest.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.util.Arrays; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for blocks + * @since 1.1 + */ +@SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) +public class FeaturesTest extends JexlTestCase { + private final JexlEngine jexl = new JexlBuilder().create(); + /** + * Create the test + */ + public FeaturesTest() { + super("BlockTest"); + } + + /** + * Checks that the script is valid with all features on then verifies it + * throws a feature exception with the given set (in features param). + * @param features + * @param scripts + * @throws Exception + */ + private void checkFeature(JexlFeatures features, String[] scripts) throws Exception { + for (String script : scripts) { + JexlScript ctl = JEXL.createScript(script); + Assert.assertNotNull(ctl); + try { + JexlScript e = jexl.createScript(features, null, script, null); + Assert.fail("should fail parse: " + script); + } catch (JexlException.Feature xfeature) { + String msg = xfeature.getMessage(); + Assert.assertNotNull(msg); + } catch (JexlException.Parsing xparsing) { + String msg = xparsing.getMessage(); + Assert.assertNotNull(msg); + } + } + } + + private void assertOk(JexlFeatures features, String[] scripts) { + for(String str : scripts) { + try { + JexlScript e = jexl.createScript(str); + } catch (JexlException.Feature xfeature) { + Assert.fail(str + " :: should not fail parse: " + xfeature.getMessage()); + } + } + } + + @Test + public void testNoScript() throws Exception { + JexlFeatures f = new JexlFeatures().script(false); + String[] scripts = new String[]{ + "if (false) { block(); }", + "{ noway(); }", + "while(true);", + "for(var i : {0 .. 10}) { bar(i); }" + }; + checkFeature(f, scripts); + } + + @Test + public void testNoLoop() throws Exception { + JexlFeatures f = new JexlFeatures().loops(false); + String[] scripts = new String[]{ + "while(true);", + "for(var i : {0 .. 10}) { bar(i); }" + }; + checkFeature(f, scripts); + } + + @Test + public void testNoLambda() throws Exception { + JexlFeatures f = new JexlFeatures().lambda(false); + String[] scripts = new String[]{ + "var x = ()->{ return 0 };", + "()->{ return 0 };", + "(x, y)->{ return 0 };", + "function() { return 0 };", + "function(x, y) { return 0 };", + "if (false) { (function(x, y) { return x + y })(3, 4) }" + }; + checkFeature(f, scripts); + } + + @Test + public void testNoNew() throws Exception { + JexlFeatures f = new JexlFeatures().newInstance(false); + String[] scripts = new String[]{ + "return new(clazz);", + "new('java.math.BigDecimal', 12) + 1" + }; + checkFeature(f, scripts); + } + + @Test + public void testNoSideEffects() throws Exception { + JexlFeatures f = new JexlFeatures().sideEffect(false); + String[] scripts = new String[]{ + "x = 1", + "x.y = 1", + "x().y = 1", + "x += 1", + "x.y += 1", + "x().y += 1", + "x -= 1", + "x *= 1", + "x /= 1", + "x ^= 1", + "x &= 1", + "x |= 1", + }; + checkFeature(f, scripts); + } + + @Test + public void testNoSideEffectsGlobal() throws Exception { + JexlFeatures f = new JexlFeatures().sideEffectGlobal(false); + String[] scripts = new String[]{ + "x = 1", + "x.y = 1", + "x().y = 1", + "x += 1", + "x.y += 1", + "x().y += 1", + "x -= 1", + "x *= 1", + "x /= 1", + "x ^= 1", + "x &= 1", + "x |= 1", + "4 + (x.y = 1)", + "if (true) x.y.z = 4" + }; + // these should all fail with x undeclared as local, thus x as global + checkFeature(f, scripts); + // same ones with x as local should work + for(String str : scripts) { + try { + JexlScript e = jexl.createScript("var x = foo(); " + str); + } catch (JexlException.Feature xfeature) { + Assert.fail(str + " :: should not fail parse: " + xfeature.getMessage()); + } + } + } + + @Test + public void testNoLocals() throws Exception { + JexlFeatures f = new JexlFeatures().localVar(false); + String[] scripts = new String[]{ + "var x = 0;", + "(x)->{ x }" + }; + checkFeature(f, scripts); + } + + @Test + public void testReservedVars() throws Exception { + JexlFeatures f = new JexlFeatures().reservedNames(Arrays.asList("foo", "bar")); + String[] scripts = new String[]{ + "var foo = 0;", + "(bar)->{ bar }", + "var f = function(bar) { bar; }" + }; + checkFeature(f, scripts); + String[] scriptsOk = new String[]{ + "var foo0 = 0;", + "(bar1)->{ bar }", + "var f = function(bar2) { bar2; }" + }; + assertOk(f, scriptsOk); + } + + @Test + public void testArrayRefs() throws Exception { + JexlFeatures f = new JexlFeatures().arrayReferenceExpr(false); + + String[] scripts = new String[]{ + "x[y]", + "x['a'][b]", + "x()['a'][b]", + "x.y['a'][b]" + }; + checkFeature(f, scripts); + assertOk(f, scripts); + // same ones with constant array refs should work + String[] scriptsOk = new String[]{ + "x['y']", + "x['a'][1]", + "x()['a']['b']", + "x.y['a']['b']" + }; + assertOk(f, scriptsOk); + } + @Test + public void testMethodCalls() throws Exception { + JexlFeatures f = new JexlFeatures().methodCall(false); + String[] scripts = new String[]{ + "x.y(z)", + "x['a'].m(b)", + "x()['a'](b)", + "x.y['a'](b)" + }; + checkFeature(f, scripts); + // same ones with constant array refs should work + String[] scriptsOk = new String[]{ + "x('y')", + "x('a')[1]", + "x()['a']['b']", + }; + assertOk(f, scriptsOk); + } + + @Test + public void testStructuredLiterals() throws Exception { + JexlFeatures f = new JexlFeatures().structuredLiteral(false); + String[] scripts = new String[]{ + "{1, 2, 3}", + "[1, 2, 3]", + "{ 1 :'one', 2 : 'two', 3 : 'three' }", + "(1 .. 5)" + }; + checkFeature(f, scripts); + assertOk(f, scripts); + } + + @Test + public void testAnnotations() throws Exception { + JexlFeatures f = new JexlFeatures().annotation(false); + String[] scripts = new String[]{ + "@synchronized(2) { return 42; }", + "@two var x = 3;" + }; + checkFeature(f, scripts); + } + + + @Test + public void testPragma() throws Exception { + JexlFeatures f = new JexlFeatures().pragma(false); + String[] scripts = new String[]{ + "#pragma foo 42", + "@two var x = 3; #pragma foo 'bar'" + }; + checkFeature(f, scripts); + } + + @Test + public void testMixedFeatures() throws Exception { + // no new, no local, no lambda, no loops, no-side effects + JexlFeatures f = new JexlFeatures() + .newInstance(false) + .localVar(false) + .lambda(false) + .loops(false) + .sideEffectGlobal(false); + String[] scripts = new String[]{ + "return new(clazz);", + "()->{ return 0 };", + "var x = 0;", + "(x, y)->{ return 0 };", + "for(var i : {0 .. 10}) { bar(i); }", + "x += 1", + "x.y += 1" + }; + checkFeature(f, scripts); + } + +} diff --git a/src/test/java/org/apache/commons/jexl3/Foo.java b/src/test/java/org/apache/commons/jexl3/Foo.java index 6b69b03e8..8a7ea20c8 100644 --- a/src/test/java/org/apache/commons/jexl3/Foo.java +++ b/src/test/java/org/apache/commons/jexl3/Foo.java @@ -51,9 +51,12 @@ public Foo getInnerFoo() return new Foo(); } - public String get(String arg) - { - return "Repeat : " + arg; + public String getQuux() { + return "String : quux"; + } + + public String repeat(String str) { + return "Repeat : " + str; } public String convertBoolean(boolean b) diff --git a/src/test/java/org/apache/commons/jexl3/IfTest.java b/src/test/java/org/apache/commons/jexl3/IfTest.java index d76977d21..83f0d2fe0 100644 --- a/src/test/java/org/apache/commons/jexl3/IfTest.java +++ b/src/test/java/org/apache/commons/jexl3/IfTest.java @@ -115,6 +115,46 @@ public void testIfWithSimpleExpression() throws Exception { Assert.assertEquals("Result is not true", Boolean.TRUE, o); } + @Test + public void testIfElseIfExpression() throws Exception { + JexlScript e = JEXL.createScript("if (x == 1) { 10; } else if (x == 2) 20 else 30", "x"); + Object o = e.execute(null, 1); + Assert.assertEquals(10, o); + o = e.execute(null, 2); + Assert.assertEquals(20, o); + o = e.execute(null, 4); + Assert.assertEquals(30, o); + } + + @Test + public void testIfElseIfReturnExpression0() throws Exception { + JexlScript e = JEXL.createScript( + "if (x == 1) return 10; if (x == 2) return 20; else if (x == 3) return 30 else { return 40 }", + "x"); + Object o = e.execute(null, 1); + Assert.assertEquals(10, o); + o = e.execute(null, 2); + Assert.assertEquals(20, o); + o = e.execute(null, 3); + Assert.assertEquals(30, o); + o = e.execute(null, 4); + Assert.assertEquals(40, o); + } + @Test + public void testIfElseIfReturnExpression() throws Exception { + JexlScript e = JEXL.createScript( + "if (x == 1) return 10; if (x == 2) return 20 else if (x == 3) return 30; else return 40;", + "x"); + Object o = e.execute(null, 1); + Assert.assertEquals(10, o); + o = e.execute(null, 2); + Assert.assertEquals(20, o); + o = e.execute(null, 3); + Assert.assertEquals(30, o); + o = e.execute(null, 4); + Assert.assertEquals(40, o); + } + /** * Test the if statement evaluates arithmetic expressions correctly * @@ -222,13 +262,14 @@ public void testTernary() throws Exception { /** * Ternary operator condition undefined or null evaluates to false - * independantly of engine flags. + * independently of engine flags; same for null coalescing operator. * @throws Exception */ @Test public void testTernaryShorthand() throws Exception { JexlEvalContext jc = new JexlEvalContext(); JexlExpression e = JEXL.createExpression("x.y.z = foo?:'quux'"); + JexlExpression f = JEXL.createExpression("foo??'quux'"); Object o; // undefined foo @@ -239,6 +280,8 @@ public void testTernaryShorthand() throws Exception { Assert.assertEquals("Should be quux", "quux", o); o = jc.get("x.y.z"); Assert.assertEquals("Should be quux", "quux", o); + o = f.evaluate(jc); + Assert.assertEquals("Should be quux", "quux", o); } jc.set("foo", null); @@ -250,6 +293,8 @@ public void testTernaryShorthand() throws Exception { Assert.assertEquals("Should be quux", "quux", o); o = jc.get("x.y.z"); Assert.assertEquals("Should be quux", "quux", o); + o = f.evaluate(jc); + Assert.assertEquals("Should be quux", "quux", o); } jc.set("foo", Boolean.FALSE); @@ -261,6 +306,8 @@ public void testTernaryShorthand() throws Exception { Assert.assertEquals("Should be quux", "quux", o); o = jc.get("x.y.z"); Assert.assertEquals("Should be quux", "quux", o); + o = f.evaluate(jc); + Assert.assertEquals("Should be false", false, o); } jc.set("foo", Double.NaN); @@ -272,6 +319,8 @@ public void testTernaryShorthand() throws Exception { Assert.assertEquals("Should be quux", "quux", o); o = jc.get("x.y.z"); Assert.assertEquals("Should be quux", "quux", o); + o = f.evaluate(jc); + Assert.assertTrue("Should be NaN", Double.isNaN((Double) o)); } jc.set("foo", ""); @@ -283,6 +332,8 @@ public void testTernaryShorthand() throws Exception { Assert.assertEquals("Should be quux", "quux", o); o = jc.get("x.y.z"); Assert.assertEquals("Should be quux", "quux", o); + o = f.evaluate(jc); + Assert.assertEquals("Should be empty string", "", o); } jc.set("foo", "false"); @@ -294,6 +345,8 @@ public void testTernaryShorthand() throws Exception { Assert.assertEquals("Should be quux", "quux", o); o = jc.get("x.y.z"); Assert.assertEquals("Should be quux", "quux", o); + o = f.evaluate(jc); + Assert.assertEquals("Should be 'false'", "false", o); } jc.set("foo", 0d); @@ -305,6 +358,8 @@ public void testTernaryShorthand() throws Exception { Assert.assertEquals("Should be quux", "quux", o); o = jc.get("x.y.z"); Assert.assertEquals("Should be quux", "quux", o); + o = f.evaluate(jc); + Assert.assertEquals("Should be 0", 0.d, o); } jc.set("foo", 0); @@ -316,6 +371,8 @@ public void testTernaryShorthand() throws Exception { Assert.assertEquals("Should be quux", "quux", o); o = jc.get("x.y.z"); Assert.assertEquals("Should be quux", "quux", o); + o = f.evaluate(jc); + Assert.assertEquals("Should be 0", 0, o); } jc.set("foo", "bar"); @@ -332,4 +389,41 @@ public void testTernaryShorthand() throws Exception { debuggerCheck(JEXL); } + @Test + public void testNullCoaelescing() throws Exception { + Object o; + JexlEvalContext jc = new JexlEvalContext(); + JexlExpression xtrue = JEXL.createExpression("x??true"); + o = xtrue.evaluate(jc); + Assert.assertEquals("Should be true", true, o); + jc.set("x", false); + o = xtrue.evaluate(jc); + Assert.assertEquals("Should be false", false, o); + JexlExpression yone = JEXL.createExpression("y??1"); + o = yone.evaluate(jc); + Assert.assertEquals("Should be 1", 1, o); + jc.set("y", 0); + o = yone.evaluate(jc); + Assert.assertEquals("Should be 0", 0, o); + debuggerCheck(JEXL); + } + + @Test + public void testNullCoaelescingScript() throws Exception { + Object o; + JexlEvalContext jc = new JexlEvalContext(); + JexlScript xtrue = JEXL.createScript("x??true"); + o = xtrue.execute(jc); + Assert.assertEquals("Should be true", true, o); + jc.set("x", false); + o = xtrue.execute(jc); + Assert.assertEquals("Should be false", false, o); + JexlScript yone = JEXL.createScript("y??1"); + o = yone.execute(jc); + Assert.assertEquals("Should be 1", 1, o); + jc.set("y", 0); + o = yone.execute(jc); + Assert.assertEquals("Should be 0", 0, o); + debuggerCheck(JEXL); + } } diff --git a/src/test/java/org/apache/commons/jexl3/IssuesTest.java b/src/test/java/org/apache/commons/jexl3/IssuesTest.java index cf3c71ec4..477afa58b 100644 --- a/src/test/java/org/apache/commons/jexl3/IssuesTest.java +++ b/src/test/java/org/apache/commons/jexl3/IssuesTest.java @@ -17,24 +17,17 @@ package org.apache.commons.jexl3; import org.apache.commons.jexl3.internal.Engine; -import java.math.BigDecimal; -import java.math.MathContext; +import org.apache.commons.jexl3.internal.introspection.Uberspect; + import java.util.HashMap; import java.util.Map; -import org.apache.commons.jexl3.internal.introspection.Uberspect; -import java.io.File; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Set; import org.junit.Assert; import org.junit.Before; import org.junit.Test; //import org.apache.commons.beanutils.LazyDynaMap; /** - * Test cases for reported issue . + * Test cases for reported issue between JEXL-1 and JEXL-100. */ @SuppressWarnings({"boxing", "UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) public class IssuesTest extends JexlTestCase { @@ -64,7 +57,7 @@ public void test49() throws Exception { // JEXL-48: bad assignment detection public static class Another { public String name = "whatever"; - private Boolean foo = Boolean.TRUE; + private final Boolean foo = Boolean.TRUE; public Boolean foo() { return foo; @@ -76,7 +69,7 @@ public int goo() { } public static class Foo { - private Another inner; + private final Another inner; Foo() { inner = new Another(); @@ -118,15 +111,15 @@ public void test47() throws Exception { JexlExpression expr = jexl.createExpression("true//false\n"); Object value = expr.evaluate(ctxt); - Assert.assertTrue("should be true", ((Boolean) value).booleanValue()); + Assert.assertTrue("should be true", (Boolean) value); expr = jexl.createExpression("/*true*/false"); value = expr.evaluate(ctxt); - Assert.assertFalse("should be false", ((Boolean) value).booleanValue()); + Assert.assertFalse("should be false", (Boolean) value); expr = jexl.createExpression("/*\"true\"*/false"); value = expr.evaluate(ctxt); - Assert.assertFalse("should be false", ((Boolean) value).booleanValue()); + Assert.assertFalse("should be false", (Boolean) value); } // JEXL-42: NullPointerException evaluating an expression @@ -170,7 +163,7 @@ public void test40() throws Exception { JexlExpression expr = jexl.createExpression("derived.foo()"); Object value = expr.evaluate(ctxt); - Assert.assertTrue("should be true", ((Boolean) value).booleanValue()); + Assert.assertTrue("should be true", (Boolean) value); } // JEXL-52: can be implemented by deriving Interpreter.{g,s}etAttribute; later @@ -416,763 +409,4 @@ public void test98() throws Exception { } } - @Test - public void test100() throws Exception { - JexlEngine jexl = new JexlBuilder().cache(4).create(); - JexlContext ctxt = new MapContext(); - int[] foo = {42}; - ctxt.set("foo", foo); - Object value; - for (int l = 0; l < 2; ++l) { - value = jexl.createExpression("foo[0]").evaluate(ctxt); - Assert.assertEquals(42, value); - value = jexl.createExpression("foo[0] = 43").evaluate(ctxt); - Assert.assertEquals(43, value); - value = jexl.createExpression("foo.0").evaluate(ctxt); - Assert.assertEquals(43, value); - value = jexl.createExpression("foo.0 = 42").evaluate(ctxt); - Assert.assertEquals(42, value); - } - } - -// A's class definition - public static class A105 { - String nameA; - String propA; - - public A105(String nameA, String propA) { - this.nameA = nameA; - this.propA = propA; - } - - @Override - public String toString() { - return "A [nameA=" + nameA + ", propA=" + propA + "]"; - } - - public String getNameA() { - return nameA; - } - - public String getPropA() { - return propA; - } - - public String uppercase(String str) { - return str.toUpperCase(); - } - } - - @Test - public void test105() throws Exception { - JexlContext context = new MapContext(); - JexlExpression selectExp = new Engine().createExpression("[a.propA]"); - context.set("a", new A105("a1", "p1")); - Object[] r = (Object[]) selectExp.evaluate(context); - Assert.assertEquals("p1", r[0]); - -//selectExp = new Engine().createExpression("[a.propA]"); - context.set("a", new A105("a2", "p2")); - r = (Object[]) selectExp.evaluate(context); - Assert.assertEquals("p2", r[0]); - } - - @Test - public void test106() throws Exception { - JexlEvalContext context = new JexlEvalContext(); - context.setStrict(true, true); - context.set("a", new BigDecimal(1)); - context.set("b", new BigDecimal(3)); - JexlEngine jexl = new Engine(); - try { - Object value = jexl.createExpression("a / b").evaluate(context); - Assert.assertNotNull(value); - } catch (JexlException xjexl) { - Assert.fail("should not occur"); - } - context.setMathContext(MathContext.UNLIMITED); - context.setMathScale(2); - try { - jexl.createExpression("a / b").evaluate(context); - Assert.fail("should fail"); - } catch (JexlException xjexl) { - //ok to fail - } - } - - @Test - public void test107() throws Exception { - String[] exprs = { - "'Q4'.toLowerCase()", "q4", - "(Q4).toLowerCase()", "q4", - "(4).toString()", "4", - "(1 + 3).toString()", "4", - "({ 'q' : 'Q4'}).get('q').toLowerCase()", "q4", - "{ 'q' : 'Q4'}.get('q').toLowerCase()", "q4", - "({ 'q' : 'Q4'})['q'].toLowerCase()", "q4", - "(['Q4'])[0].toLowerCase()", "q4" - }; - - JexlContext context = new MapContext(); - context.set("Q4", "Q4"); - JexlEngine jexl = new Engine(); - for (int e = 0; e < exprs.length; e += 2) { - JexlExpression expr = jexl.createExpression(exprs[e]); - Object expected = exprs[e + 1]; - Object value = expr.evaluate(context); - Assert.assertEquals(expected, value); - expr = jexl.createExpression(expr.getParsedText()); - value = expr.evaluate(context); - Assert.assertEquals(expected, value); - } - } - - @Test - public void test108() throws Exception { - JexlScript expr; - Object value; - JexlEngine jexl = new Engine(); - expr = jexl.createScript("size([])"); - value = expr.execute(null); - Assert.assertEquals(0, value); - expr = jexl.createScript(expr.getParsedText()); - value = expr.execute(null); - Assert.assertEquals(0, value); - - expr = jexl.createScript("if (true) { [] } else { {:} }"); - value = expr.execute(null); - Assert.assertTrue(value.getClass().isArray()); - expr = jexl.createScript(expr.getParsedText()); - value = expr.execute(null); - Assert.assertTrue(value.getClass().isArray()); - - expr = jexl.createScript("size({:})"); - value = expr.execute(null); - Assert.assertEquals(0, value); - expr = jexl.createScript(expr.getParsedText()); - value = expr.execute(null); - Assert.assertEquals(0, value); - - expr = jexl.createScript("if (false) { [] } else { {:} }"); - value = expr.execute(null); - Assert.assertTrue(value instanceof Map); - expr = jexl.createScript(expr.getParsedText()); - value = expr.execute(null); - Assert.assertTrue(value instanceof Map); - } - - @Test - public void test109() throws Exception { - JexlEngine jexl = new Engine(); - Object value; - JexlContext context = new MapContext(); - context.set("foo.bar", 40); - value = jexl.createExpression("foo.bar + 2").evaluate(context); - Assert.assertEquals(42, value); - } - - @Test - public void test110() throws Exception { - JexlEngine jexl = new Engine(); - String[] names = {"foo"}; - Object value; - JexlContext context = new MapContext(); - value = jexl.createScript("foo + 2", names).execute(context, 40); - Assert.assertEquals(42, value); - context.set("frak.foo", -40); - value = jexl.createScript("frak.foo - 2", names).execute(context, 40); - Assert.assertEquals(-42, value); - } - - static public class RichContext extends ObjectContext { - RichContext(JexlEngine jexl, A105 a105) { - super(jexl, a105); - } - } - - @Test - public void testRichContext() throws Exception { - A105 a105 = new A105("foo", "bar"); - JexlEngine jexl = new Engine(); - Object value; - JexlContext context = new RichContext(jexl, a105); - value = jexl.createScript("uppercase(nameA + propA)").execute(context); - Assert.assertEquals("FOOBAR", value); - } - - @Test - public void test111() throws Exception { - JexlEngine jexl = new Engine(); - Object value; - JexlContext context = new MapContext(); - String strExpr = "((x>0)?\"FirstValue=\"+(y-x):\"SecondValue=\"+x)"; - JexlExpression expr = jexl.createExpression(strExpr); - - context.set("x", 1); - context.set("y", 10); - value = expr.evaluate(context); - Assert.assertEquals("FirstValue=9", value); - - context.set("x", 1.0d); - context.set("y", 10.0d); - value = expr.evaluate(context); - Assert.assertEquals("FirstValue=9.0", value); - - context.set("x", 1); - context.set("y", 10.0d); - value = expr.evaluate(context); - Assert.assertEquals("FirstValue=9.0", value); - - context.set("x", 1.0d); - context.set("y", 10); - value = expr.evaluate(context); - Assert.assertEquals("FirstValue=9.0", value); - - context.set("x", -10); - context.set("y", 1); - value = expr.evaluate(context); - Assert.assertEquals("SecondValue=-10", value); - - context.set("x", -10.0d); - context.set("y", 1.0d); - value = expr.evaluate(context); - Assert.assertEquals("SecondValue=-10.0", value); - - context.set("x", -10); - context.set("y", 1.0d); - value = expr.evaluate(context); - Assert.assertEquals("SecondValue=-10", value); - - context.set("x", -10.0d); - context.set("y", 1); - value = expr.evaluate(context); - Assert.assertEquals("SecondValue=-10.0", value); - } - - @Test - public void testScaleIssue() throws Exception { - JexlEngine jexlX = new Engine(); - String expStr1 = "result == salary/month * work.percent/100.00"; - JexlExpression exp1 = jexlX.createExpression(expStr1); - JexlEvalContext ctx = new JexlEvalContext(); - ctx.set("result", new BigDecimal("9958.33")); - ctx.set("salary", new BigDecimal("119500.00")); - ctx.set("month", new BigDecimal("12.00")); - ctx.set("work.percent", new BigDecimal("100.00")); - - // will fail because default scale is 5 - Assert.assertFalse((Boolean) exp1.evaluate(ctx)); - - // will succeed with scale = 2 - ctx.setMathScale(2); - Assert.assertTrue((Boolean) exp1.evaluate(ctx)); - } - - @Test - public void test112() throws Exception { - Object result; - JexlEngine jexl = new Engine(); - result = jexl.createScript(Integer.toString(Integer.MAX_VALUE)).execute(null); - Assert.assertEquals(Integer.MAX_VALUE, result); - result = jexl.createScript(Integer.toString(Integer.MIN_VALUE + 1)).execute(null); - Assert.assertEquals(Integer.MIN_VALUE + 1, result); - result = jexl.createScript(Integer.toString(Integer.MIN_VALUE)).execute(null); - Assert.assertEquals(Integer.MIN_VALUE, result); - } - - @Test - public void test117() throws Exception { - JexlEngine jexl = new Engine(); - JexlExpression e = jexl.createExpression("TIMESTAMP > 20100102000000"); - JexlContext ctx = new MapContext(); - ctx.set("TIMESTAMP", new Long("20100103000000")); - Object result = e.evaluate(ctx); - Assert.assertTrue((Boolean) result); - } - - public static class Foo125 { - public String method() { - return "OK"; - } - - public String total(String tt) { - return "total " + tt; - } - } - - public static class Foo125Context extends ObjectContext { - public Foo125Context(JexlEngine engine, Foo125 wrapped) { - super(engine, wrapped); - } - } - - @Test - public void test125() throws Exception { - JexlEngine jexl = new Engine(); - JexlExpression e = jexl.createExpression("method()"); - JexlContext jc = new Foo125Context(jexl, new Foo125()); - Assert.assertEquals("OK", e.evaluate(jc)); - } - - @Test - public void test130a() throws Exception { - String myName = "Test.Name"; - Object myValue = "Test.Value"; - - JexlEngine myJexlEngine = new Engine(); - MapContext myMapContext = new MapContext(); - myMapContext.set(myName, myValue); - - Object myObjectWithTernaryConditional = myJexlEngine.createScript(myName + "?:null").execute(myMapContext); - Assert.assertEquals(myValue, myObjectWithTernaryConditional); - } - - @Test - public void test130b() throws Exception { - String myName = "Test.Name"; - Object myValue = new Object() { - @Override - public String toString() { - return "Test.Value"; - } - }; - - JexlEngine myJexlEngine = new Engine(); - MapContext myMapContext = new MapContext(); - myMapContext.set(myName, myValue); - - Object myObjectWithTernaryConditional = myJexlEngine.createScript(myName + "?:null").execute(myMapContext); - Assert.assertEquals(myValue, myObjectWithTernaryConditional); - } - - @Test - public void test135() throws Exception { - JexlEngine jexl = new Engine(); - JexlContext jc = new MapContext(); - JexlScript script; - Object result; - Map foo = new HashMap(); - foo.put(3, 42); - jc.set("state", foo); - - script = jexl.createScript("var y = state[3]; y"); - result = script.execute(jc, foo); - Assert.assertEquals(42, result); - - jc.set("a", 3); - script = jexl.createScript("var y = state[a]; y"); - result = script.execute(jc, foo); - Assert.assertEquals(42, result); - - jc.set("a", 2); - script = jexl.createScript("var y = state[a + 1]; y"); - result = script.execute(jc, foo); - Assert.assertEquals(42, result); - - jc.set("a", 2); - jc.set("b", 1); - script = jexl.createScript("var y = state[a + b]; y"); - result = script.execute(jc, foo); - Assert.assertEquals(42, result); - - script = jexl.createScript("var y = state[3]; y", "state"); - result = script.execute(null, foo, 3); - Assert.assertEquals(42, result); - - script = jexl.createScript("var y = state[a]; y", "state", "a"); - result = script.execute(null, foo, 3); - Assert.assertEquals(42, result); - - script = jexl.createScript("var y = state[a + 1]; y", "state", "a"); - result = script.execute(null, foo, 2); - Assert.assertEquals(42, result); - - script = jexl.createScript("var y = state[a + b]; y", "state", "a", "b"); - result = script.execute(null, foo, 2, 1); - Assert.assertEquals(42, result); - } - - @Test - public void test136() throws Exception { - JexlEngine jexl = new Engine(); - JexlContext jc = new MapContext(); - JexlScript script; - JexlExpression expr; - Object result; - - script = jexl.createScript("var x = $TAB[idx]; return x;", "idx"); - jc.set("fn01", script); - - script = jexl.createScript("$TAB = { 1:11, 2:22, 3:33}; IDX=2;"); - script.execute(jc); - - expr = jexl.createExpression("fn01(IDX)"); - result = expr.evaluate(jc); - Assert.assertEquals("EXPR01 result", 22, result); - } - -// @Test public void test138() throws Exception { -// MapContext ctxt = new MapContext(); -// ctxt.set("tz", java.util.TimeZone.class); -// String source = "" -// + "var currentDate = new('java.util.Date');" -// + "var gmt = tz.getTimeZone('GMT');" -// + "var cet = tz.getTimeZone('CET');" -// + "var calendarGMT = new('java.util.GregorianCalendar' , gmt);" -// + "var calendarCET = new('java.util.GregorianCalendar', cet);" -// + "var diff = calendarCET.getTime() - calendarGMT.getTime();" -// + "return diff"; -// -// JexlEngine jexl = new Engine(); -// JexlScript script = jexl.createScript(source); -// Object result = script.execute(ctxt); -// Assert.Assert.assertNotNull(result); -// } - @Test - public void test143() throws Exception { - JexlEngine jexl = new Engine(); - JexlContext jc = new MapContext(); - JexlScript script; - Object result; - - script = jexl.createScript("var total = 10; total = (total - ((x < 3)? y : z)) / (total / 10); total", "x", "y", "z"); - result = script.execute(jc, 2, 2, 1); - Assert.assertEquals(8, result); - script = jexl.createScript("var total = 10; total = (total - ((x < 3)? y : 1)) / (total / 10); total", "x", "y", "z"); - result = script.execute(jc, 2, 2, 1); - Assert.assertEquals(8, result); - } - - @Test - public void test144() throws Exception { - JexlEngine jexl = new Engine(); - JexlContext jc = new MapContext(); - JexlScript script; - Object result; - script = jexl.createScript("var total = 10; total('tt')"); - try { - result = script.execute(jc); - Assert.fail("total() is not solvable"); - } catch (JexlException.Method ambiguous) { - Assert.assertEquals("total", ambiguous.getMethod()); - } - } - - /** - * Test cases for empty array assignment. - */ - public static class Quux144 { - String[] arr; - String[] arr2; - - public Quux144() { - } - - public String[] getArr() { - return arr; - } - - public String[] getArr2() { - return arr2; - } - - public void setArr(String[] arr) { - this.arr = arr; - } - - public void setArr2(String[] arr2) { - this.arr2 = arr2; - } - - // Overloaded setter with different argument type. - public void setArr2(Integer[] arr2) { - } - } - - @Test - public void test144a() throws Exception { - JexlEngine JEXL = new Engine(); - JexlContext jc = new MapContext(); - jc.set("quuxClass", Quux144.class); - JexlExpression create = JEXL.createExpression("quux = new(quuxClass)"); - JexlExpression assignArray = JEXL.createExpression("quux.arr = [ 'hello', 'world' ]"); - JexlExpression checkArray = JEXL.createExpression("quux.arr"); - - // test with a string - Quux144 quux = (Quux144) create.evaluate(jc); - Assert.assertNotNull("quux is null", quux); - - // test with a nonempty string array - Object o = assignArray.evaluate(jc); - Assert.assertEquals("Result is not a string array", String[].class, o.getClass()); - o = checkArray.evaluate(jc); - Assert.assertEquals("The array elements are equal", Arrays.asList("hello", "world"), Arrays.asList((String[]) o)); - - // test with a null array - assignArray = JEXL.createExpression("quux.arr = null"); - o = assignArray.evaluate(jc); - Assert.assertNull("Result is not null", o); - o = checkArray.evaluate(jc); - Assert.assertNull("Result is not null", o); - - // test with an empty array - assignArray = JEXL.createExpression("quux.arr = [ ]"); - o = assignArray.evaluate(jc); - Assert.assertNotNull("Result is null", o); - o = checkArray.evaluate(jc); - Assert.assertEquals("The array elements are not equal", Arrays.asList(new String[0]), Arrays.asList((String[]) o)); - Assert.assertEquals("The array size is not zero", 0, ((String[]) o).length); - - // test with an empty array on the overloaded setter for different types. - // so, the assignment should fail with logging 'The ambiguous property, arr2, should have failed.' - try { - assignArray = JEXL.createExpression("quux.arr2 = [ ]"); - o = assignArray.evaluate(jc); - Assert.fail("The arr2 property shouldn't be set due to its ambiguity (overloaded setters with different types)."); - } catch (JexlException.Property e) { - //System.out.println("Expected ambiguous property setting exception: " + e); - } - Assert.assertNull("The arr2 property value should remain as null, not an empty array.", quux.arr2); - } - - @Test - public void test147b() throws Exception { - String[] scripts = {"var x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x.one", // results to 1 - "x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x.one",// results to 1 - "x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x['one']",//results to 1 - "var x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x['one']"// result to null? - }; - - JexlEngine JEXL = new Engine(); - JexlContext jc = new MapContext(); - for (String s : scripts) { - Object o = JEXL.createScript(s).execute(jc); - Assert.assertEquals(1, o); - } - } - - @Test - public void test147c() throws Exception { - String[] scripts = { - "var x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x.one", - "x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x.one", - "x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x['one']", - "var x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x['one']" - }; - JexlEngine JEXL = new Engine(); - for (String s : scripts) { - JexlContext jc = new MapContext(); - Object o = JEXL.createScript(s).execute(jc); - Assert.assertEquals(1, o); - } - } - - @Test - public void test5115a() throws Exception { - String str = "{\n" - + " var x = \"A comment\";\n" - + " var y = \"A comment\";\n" - + "}"; - try { - JexlEngine JEXL = new Engine(); - JexlScript s = JEXL.createScript(str); - } catch (JexlException.Parsing xparse) { - throw xparse; - } - } - - @Test - public void test5115b() throws Exception { - String str = "{\n" - + " var x = \"A comment\";\n" - + "}"; - try { - JexlEngine JEXL = new Engine(); - JexlScript s = JEXL.createScript(str); - } catch (JexlException.Parsing xparse) { - throw xparse; - } - } - - static final String TESTA = "src/test/scripts/testA.jexl"; - - @Test - public void test5115c() throws Exception { - URL testUrl = new File(TESTA).toURI().toURL(); - try { - JexlEngine JEXL = new Engine(); - JexlScript s = JEXL.createScript(testUrl); - } catch (JexlException.Parsing xparse) { - throw xparse; - } - } - - public static class Utils { - public List asList(T[] array) { - return Arrays.asList(array); - } - - public List asList(int[] array) { - List l = new ArrayList(array.length); - for (int i : array) { - l.add(i); - } - return l; - } - } - - @Test - public void test148a() throws Exception { - JexlEngine jexl = new Engine(); - JexlContext jc = new MapContext(); - jc.set("u", new Utils()); - - String src = "u.asList(['foo', 'bar'])"; - JexlScript e = jexl.createScript(src); - Object o = e.execute(jc); - Assert.assertTrue(o instanceof List); - Assert.assertEquals(Arrays.asList("foo", "bar"), o); - - src = "u.asList([1, 2])"; - e = jexl.createScript(src); - o = e.execute(jc); - Assert.assertTrue(o instanceof List); - Assert.assertEquals(Arrays.asList(1, 2), o); - } - - @Test - public void test155() throws Exception { - JexlEngine jexlEngine = new Engine(); - JexlExpression jexlExpresssion = jexlEngine.createExpression("first.second.name"); - JexlContext jc = new MapContext(); - jc.set("first.second.name", "RIGHT"); - jc.set("name", "WRONG"); - Object value = jexlExpresssion.evaluate(jc); - Assert.assertEquals("RIGHT", value.toString()); - } - - public static class Question42 extends MapContext { - public String functionA(String arg) { - return "a".equals(arg) ? "A" : ""; - } - - public String functionB(String arg) { - return "b".equals(arg) ? "B" : ""; - } - - public String functionC(String arg) { - return "c".equals(arg) ? "C" : ""; - } - - public String functionD(String arg) { - return "d".equals(arg) ? "D" : ""; - } - } - - public static class Arithmetic42 extends JexlArithmetic { - public Arithmetic42() { - super(false); - } - - public Object and(String lhs, String rhs) { - if (rhs.isEmpty()) { - return ""; - } - if (lhs.isEmpty()) { - return ""; - } - return lhs + rhs; - } - - public Object or(String lhs, String rhs) { - if (rhs.isEmpty()) { - return lhs; - } - if (lhs.isEmpty()) { - return rhs; - } - return lhs + rhs; - } - } - - @Test - public void testQuestion42() throws Exception { - JexlEngine jexl = new JexlBuilder().arithmetic(new Arithmetic42()).create(); - JexlContext jc = new Question42(); - - String str0 = "(functionA('z') | functionB('b')) & (functionC('c') | functionD('d') ) "; - JexlExpression expr0 = jexl.createExpression(str0); - Object value0 = expr0.evaluate(jc); - Assert.assertEquals("BCD", value0); - - String str1 = "(functionA('z') & functionB('b')) | (functionC('c') & functionD('d') ) "; - JexlExpression expr1 = jexl.createExpression(str1); - Object value1 = expr1.evaluate(jc); - Assert.assertEquals("CD", value1); - } - - @Test - public void test179() throws Exception { - JexlContext jc = new MapContext(); - JexlEngine jexl = new JexlBuilder().create(); - String src = "x = new ('java.util.HashSet'); x.add(1); x"; - JexlScript e = jexl.createScript(src); - Object o = e.execute(jc); - Assert.assertTrue(o instanceof Set); - Assert.assertTrue(((Set) o).contains(1)); - } -// -// -// @Test -// public void testUnderscoreInName() { -// JexlEngine jexl = new Engine(); -// String jexlExp = "(x.length_mm * x.width)"; -// JexlExpression e = jexl.createExpression( jexlExp ); -// JexlContext jc = new MapContext(); -// -// LazyDynaMap object = new LazyDynaMap(); -// object.set("length_mm", "10.0"); -// object.set("width", "5.0"); -// -// jc.set("x", object ); -// -// Assert.assertEquals(null, ((Double)e.evaluate(jc)).doubleValue(), 50d, 0d); -// } -// -// @Test -// public void testFullStopInName() { -// JexlEngine jexl = new Engine(); -// String jexlExp = "(x.length.mm * x.width)"; -// JexlExpression e = jexl.createExpression( jexlExp ); -// JexlContext jc = new MapContext(); -// -// LazyDynaMap object = new LazyDynaMap(); -// object.set("length.mm", "10.0"); -// object.set("width", "5.0"); -// -// Assert.assertEquals(null, object.get("length.mm"), "10.0"); -// -// jc.set("x", object ); -// -// Assert.assertEquals(null, ((Double)e.evaluate(jc)).doubleValue(), 50d, 0d); -// } -// -// @Test -// public void testFullStopInNameMakingSubObject() { -// JexlEngine jexl = new Engine(); -// String jexlExp = "(x.length.mm * x.width)"; -// JexlExpression e = jexl.createExpression( jexlExp ); -// JexlContext jc = new MapContext(); -// -// LazyDynaMap object = new LazyDynaMap(); -// LazyDynaMap subObject = new LazyDynaMap(); -// object.set("length", subObject); -// subObject.set("mm", "10.0"); -// object.set("width", "5.0"); -// -// jc.set("x", object ); -// -// Assert.assertEquals(null, ((Double)e.evaluate(jc)).doubleValue(), 50d, 0d); -// } - } diff --git a/src/test/java/org/apache/commons/jexl3/IssuesTest100.java b/src/test/java/org/apache/commons/jexl3/IssuesTest100.java new file mode 100644 index 000000000..35454f14b --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/IssuesTest100.java @@ -0,0 +1,814 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import org.apache.commons.jexl3.internal.Engine; + +import java.io.File; +import java.math.BigDecimal; +import java.math.MathContext; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Test cases for reported issue between JEXL-100 and JEXL-199. + */ +@SuppressWarnings({"boxing", "UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) +public class IssuesTest100 extends JexlTestCase { + public IssuesTest100() { + super("IssuesTest100", null); + } + + @Before + @Override + public void setUp() throws Exception { + // ensure jul logging is only error to avoid warning in silent mode + java.util.logging.Logger.getLogger(JexlEngine.class.getName()).setLevel(java.util.logging.Level.SEVERE); + } + + + @Test + public void test100() throws Exception { + JexlEngine jexl = new JexlBuilder().cache(4).create(); + JexlContext ctxt = new MapContext(); + int[] foo = {42}; + ctxt.set("foo", foo); + Object value; + for (int l = 0; l < 2; ++l) { + value = jexl.createExpression("foo[0]").evaluate(ctxt); + Assert.assertEquals(42, value); + value = jexl.createExpression("foo[0] = 43").evaluate(ctxt); + Assert.assertEquals(43, value); + value = jexl.createExpression("foo.0").evaluate(ctxt); + Assert.assertEquals(43, value); + value = jexl.createExpression("foo.0 = 42").evaluate(ctxt); + Assert.assertEquals(42, value); + } + } + +// A's class definition + public static class A105 { + String nameA; + String propA; + + public A105(String nameA, String propA) { + this.nameA = nameA; + this.propA = propA; + } + + @Override + public String toString() { + return "A [nameA=" + nameA + ", propA=" + propA + "]"; + } + + public String getNameA() { + return nameA; + } + + public String getPropA() { + return propA; + } + + public String uppercase(String str) { + return str.toUpperCase(); + } + } + + @Test + public void test105() throws Exception { + JexlContext context = new MapContext(); + JexlExpression selectExp = new Engine().createExpression("[a.propA]"); + context.set("a", new A105("a1", "p1")); + Object[] r = (Object[]) selectExp.evaluate(context); + Assert.assertEquals("p1", r[0]); + +//selectExp = new Engine().createExpression("[a.propA]"); + context.set("a", new A105("a2", "p2")); + r = (Object[]) selectExp.evaluate(context); + Assert.assertEquals("p2", r[0]); + } + + @Test + public void test106() throws Exception { + JexlEvalContext context = new JexlEvalContext(); + context.setStrict(true, true); + context.set("a", new BigDecimal(1)); + context.set("b", new BigDecimal(3)); + JexlEngine jexl = new Engine(); + try { + Object value = jexl.createExpression("a / b").evaluate(context); + Assert.assertNotNull(value); + } catch (JexlException xjexl) { + Assert.fail("should not occur"); + } + context.setMathContext(MathContext.UNLIMITED); + context.setMathScale(2); + try { + jexl.createExpression("a / b").evaluate(context); + Assert.fail("should fail"); + } catch (JexlException xjexl) { + //ok to fail + } + } + + @Test + public void test107() throws Exception { + String[] exprs = { + "'Q4'.toLowerCase()", "q4", + "(Q4).toLowerCase()", "q4", + "(4).toString()", "4", + "(1 + 3).toString()", "4", + "({ 'q' : 'Q4'}).get('q').toLowerCase()", "q4", + "{ 'q' : 'Q4'}.get('q').toLowerCase()", "q4", + "({ 'q' : 'Q4'})['q'].toLowerCase()", "q4", + "(['Q4'])[0].toLowerCase()", "q4" + }; + + JexlContext context = new MapContext(); + context.set("Q4", "Q4"); + JexlEngine jexl = new Engine(); + for (int e = 0; e < exprs.length; e += 2) { + JexlExpression expr = jexl.createExpression(exprs[e]); + Object expected = exprs[e + 1]; + Object value = expr.evaluate(context); + Assert.assertEquals(expected, value); + expr = jexl.createExpression(expr.getParsedText()); + value = expr.evaluate(context); + Assert.assertEquals(expected, value); + } + } + + @Test + public void test108() throws Exception { + JexlScript expr; + Object value; + JexlEngine jexl = new Engine(); + expr = jexl.createScript("size([])"); + value = expr.execute(null); + Assert.assertEquals(0, value); + expr = jexl.createScript(expr.getParsedText()); + value = expr.execute(null); + Assert.assertEquals(0, value); + + expr = jexl.createScript("if (true) { [] } else { {:} }"); + value = expr.execute(null); + Assert.assertTrue(value.getClass().isArray()); + expr = jexl.createScript(expr.getParsedText()); + value = expr.execute(null); + Assert.assertTrue(value.getClass().isArray()); + + expr = jexl.createScript("size({:})"); + value = expr.execute(null); + Assert.assertEquals(0, value); + expr = jexl.createScript(expr.getParsedText()); + value = expr.execute(null); + Assert.assertEquals(0, value); + + expr = jexl.createScript("if (false) { [] } else { {:} }"); + value = expr.execute(null); + Assert.assertTrue(value instanceof Map); + expr = jexl.createScript(expr.getParsedText()); + value = expr.execute(null); + Assert.assertTrue(value instanceof Map); + } + + @Test + public void test109() throws Exception { + JexlEngine jexl = new Engine(); + Object value; + JexlContext context = new MapContext(); + context.set("foo.bar", 40); + value = jexl.createExpression("foo.bar + 2").evaluate(context); + Assert.assertEquals(42, value); + } + + @Test + public void test110() throws Exception { + JexlEngine jexl = new Engine(); + String[] names = {"foo"}; + Object value; + JexlContext context = new MapContext(); + value = jexl.createScript("foo + 2", names).execute(context, 40); + Assert.assertEquals(42, value); + context.set("frak.foo", -40); + value = jexl.createScript("frak.foo - 2", names).execute(context, 40); + Assert.assertEquals(-42, value); + } + + static public class RichContext extends ObjectContext { + RichContext(JexlEngine jexl, A105 a105) { + super(jexl, a105); + } + } + + @Test + public void testRichContext() throws Exception { + A105 a105 = new A105("foo", "bar"); + JexlEngine jexl = new Engine(); + Object value; + JexlContext context = new RichContext(jexl, a105); + value = jexl.createScript("uppercase(nameA + propA)").execute(context); + Assert.assertEquals("FOOBAR", value); + } + + @Test + public void test111() throws Exception { + JexlEngine jexl = new Engine(); + Object value; + JexlContext context = new MapContext(); + String strExpr = "((x>0)?\"FirstValue=\"+(y-x):\"SecondValue=\"+x)"; + JexlExpression expr = jexl.createExpression(strExpr); + + context.set("x", 1); + context.set("y", 10); + value = expr.evaluate(context); + Assert.assertEquals("FirstValue=9", value); + + context.set("x", 1.0d); + context.set("y", 10.0d); + value = expr.evaluate(context); + Assert.assertEquals("FirstValue=9.0", value); + + context.set("x", 1); + context.set("y", 10.0d); + value = expr.evaluate(context); + Assert.assertEquals("FirstValue=9.0", value); + + context.set("x", 1.0d); + context.set("y", 10); + value = expr.evaluate(context); + Assert.assertEquals("FirstValue=9.0", value); + + context.set("x", -10); + context.set("y", 1); + value = expr.evaluate(context); + Assert.assertEquals("SecondValue=-10", value); + + context.set("x", -10.0d); + context.set("y", 1.0d); + value = expr.evaluate(context); + Assert.assertEquals("SecondValue=-10.0", value); + + context.set("x", -10); + context.set("y", 1.0d); + value = expr.evaluate(context); + Assert.assertEquals("SecondValue=-10", value); + + context.set("x", -10.0d); + context.set("y", 1); + value = expr.evaluate(context); + Assert.assertEquals("SecondValue=-10.0", value); + } + + @Test + public void testScaleIssue() throws Exception { + JexlEngine jexlX = new Engine(); + String expStr1 = "result == salary/month * work.percent/100.00"; + JexlExpression exp1 = jexlX.createExpression(expStr1); + JexlEvalContext ctx = new JexlEvalContext(); + ctx.set("result", new BigDecimal("9958.33")); + ctx.set("salary", new BigDecimal("119500.00")); + ctx.set("month", new BigDecimal("12.00")); + ctx.set("work.percent", new BigDecimal("100.00")); + + // will fail because default scale is 5 + Assert.assertFalse((Boolean) exp1.evaluate(ctx)); + + // will succeed with scale = 2 + ctx.setMathScale(2); + Assert.assertTrue((Boolean) exp1.evaluate(ctx)); + } + + @Test + public void test112() throws Exception { + Object result; + JexlEngine jexl = new Engine(); + result = jexl.createScript(Integer.toString(Integer.MAX_VALUE)).execute(null); + Assert.assertEquals(Integer.MAX_VALUE, result); + result = jexl.createScript(Integer.toString(Integer.MIN_VALUE + 1)).execute(null); + Assert.assertEquals(Integer.MIN_VALUE + 1, result); + result = jexl.createScript(Integer.toString(Integer.MIN_VALUE)).execute(null); + Assert.assertEquals(Integer.MIN_VALUE, result); + } + + @Test + public void test117() throws Exception { + JexlEngine jexl = new Engine(); + JexlExpression e = jexl.createExpression("TIMESTAMP > 20100102000000"); + JexlContext ctx = new MapContext(); + ctx.set("TIMESTAMP", new Long("20100103000000")); + Object result = e.evaluate(ctx); + Assert.assertTrue((Boolean) result); + } + + public static class Foo125 { + public String method() { + return "OK"; + } + + public String total(String tt) { + return "total " + tt; + } + } + + public static class Foo125Context extends ObjectContext { + public Foo125Context(JexlEngine engine, Foo125 wrapped) { + super(engine, wrapped); + } + } + + @Test + public void test125() throws Exception { + JexlEngine jexl = new Engine(); + JexlExpression e = jexl.createExpression("method()"); + JexlContext jc = new Foo125Context(jexl, new Foo125()); + Assert.assertEquals("OK", e.evaluate(jc)); + } + + @Test + public void test130a() throws Exception { + String myName = "Test.Name"; + Object myValue = "Test.Value"; + + JexlEngine myJexlEngine = new Engine(); + MapContext myMapContext = new MapContext(); + myMapContext.set(myName, myValue); + + Object myObjectWithTernaryConditional = myJexlEngine.createScript(myName + "?:null").execute(myMapContext); + Assert.assertEquals(myValue, myObjectWithTernaryConditional); + } + + @Test + public void test130b() throws Exception { + String myName = "Test.Name"; + Object myValue = new Object() { + @Override + public String toString() { + return "Test.Value"; + } + }; + + JexlEngine myJexlEngine = new Engine(); + MapContext myMapContext = new MapContext(); + myMapContext.set(myName, myValue); + + Object myObjectWithTernaryConditional = myJexlEngine.createScript(myName + "?:null").execute(myMapContext); + Assert.assertEquals(myValue, myObjectWithTernaryConditional); + } + + @Test + public void test135() throws Exception { + JexlEngine jexl = new Engine(); + JexlContext jc = new MapContext(); + JexlScript script; + Object result; + Map foo = new HashMap(); + foo.put(3, 42); + jc.set("state", foo); + + script = jexl.createScript("var y = state[3]; y"); + result = script.execute(jc, foo); + Assert.assertEquals(42, result); + + jc.set("a", 3); + script = jexl.createScript("var y = state[a]; y"); + result = script.execute(jc, foo); + Assert.assertEquals(42, result); + + jc.set("a", 2); + script = jexl.createScript("var y = state[a + 1]; y"); + result = script.execute(jc, foo); + Assert.assertEquals(42, result); + + jc.set("a", 2); + jc.set("b", 1); + script = jexl.createScript("var y = state[a + b]; y"); + result = script.execute(jc, foo); + Assert.assertEquals(42, result); + + script = jexl.createScript("var y = state[3]; y", "state"); + result = script.execute(null, foo, 3); + Assert.assertEquals(42, result); + + script = jexl.createScript("var y = state[a]; y", "state", "a"); + result = script.execute(null, foo, 3); + Assert.assertEquals(42, result); + + script = jexl.createScript("var y = state[a + 1]; y", "state", "a"); + result = script.execute(null, foo, 2); + Assert.assertEquals(42, result); + + script = jexl.createScript("var y = state[a + b]; y", "state", "a", "b"); + result = script.execute(null, foo, 2, 1); + Assert.assertEquals(42, result); + } + + @Test + public void test136() throws Exception { + JexlEngine jexl = new Engine(); + JexlContext jc = new MapContext(); + JexlScript script; + JexlExpression expr; + Object result; + + script = jexl.createScript("var x = $TAB[idx]; return x;", "idx"); + jc.set("fn01", script); + + script = jexl.createScript("$TAB = { 1:11, 2:22, 3:33}; IDX=2;"); + script.execute(jc); + + expr = jexl.createExpression("fn01(IDX)"); + result = expr.evaluate(jc); + Assert.assertEquals("EXPR01 result", 22, result); + } + +// @Test +// public void test138() throws Exception { +// MapContext ctxt = new MapContext(); +// ctxt.set("tz", java.util.TimeZone.class); +// String source = "" +// + "var currentDate = new('java.util.Date');" +// + "var gmt = tz.getTimeZone('GMT');" +// + "var cet = tz.getTimeZone('CET');" +// + "var calendarGMT = new('java.util.GregorianCalendar' , gmt);" +// + "var calendarCET = new('java.util.GregorianCalendar', cet);" +// + "var diff = calendarCET.getTime() - calendarGMT.getTime();" +// + "return diff"; +// +// JexlEngine jexl = new Engine(); +// JexlScript script = jexl.createScript(source); +// Object result = script.execute(ctxt); +// Assert.Assert.assertNotNull(result); +// } + @Test + public void test143() throws Exception { + JexlEngine jexl = new Engine(); + JexlContext jc = new MapContext(); + JexlScript script; + Object result; + + script = jexl.createScript("var total = 10; total = (total - ((x < 3)? y : z)) / (total / 10); total", "x", "y", "z"); + result = script.execute(jc, 2, 2, 1); + Assert.assertEquals(8, result); + script = jexl.createScript("var total = 10; total = (total - ((x < 3)? y : 1)) / (total / 10); total", "x", "y", "z"); + result = script.execute(jc, 2, 2, 1); + Assert.assertEquals(8, result); + } + + @Test + public void test144() throws Exception { + JexlEngine jexl = new Engine(); + JexlContext jc = new MapContext(); + JexlScript script; + Object result; + script = jexl.createScript("var total = 10; total('tt')"); + try { + result = script.execute(jc); + Assert.fail("total() is not solvable"); + } catch (JexlException.Method ambiguous) { + Assert.assertEquals("total", ambiguous.getMethod()); + } + } + + /** + * Test cases for empty array assignment. + */ + public static class Quux144 { + String[] arr; + String[] arr2; + + public Quux144() { + } + + public String[] getArr() { + return arr; + } + + public String[] getArr2() { + return arr2; + } + + public void setArr(String[] arr) { + this.arr = arr; + } + + public void setArr2(String[] arr2) { + this.arr2 = arr2; + } + + // Overloaded setter with different argument type. + public void setArr2(Integer[] arr2) { + } + } + + @Test + public void test144a() throws Exception { + JexlEngine jexl = new Engine(); + JexlContext jc = new MapContext(); + jc.set("quuxClass", Quux144.class); + JexlExpression create = jexl.createExpression("quux = new(quuxClass)"); + JexlExpression assignArray = jexl.createExpression("quux.arr = [ 'hello', 'world' ]"); + JexlExpression checkArray = jexl.createExpression("quux.arr"); + + // test with a string + Quux144 quux = (Quux144) create.evaluate(jc); + Assert.assertNotNull("quux is null", quux); + + // test with a nonempty string array + Object o = assignArray.evaluate(jc); + Assert.assertEquals("Result is not a string array", String[].class, o.getClass()); + o = checkArray.evaluate(jc); + Assert.assertEquals("The array elements are equal", Arrays.asList("hello", "world"), Arrays.asList((String[]) o)); + + // test with a null array + assignArray = jexl.createExpression("quux.arr = null"); + o = assignArray.evaluate(jc); + Assert.assertNull("Result is not null", o); + o = checkArray.evaluate(jc); + Assert.assertNull("Result is not null", o); + + // test with an empty array + assignArray = jexl.createExpression("quux.arr = [ ]"); + o = assignArray.evaluate(jc); + Assert.assertNotNull("Result is null", o); + o = checkArray.evaluate(jc); + Assert.assertEquals("The array elements are not equal", Arrays.asList(new String[0]), Arrays.asList((String[]) o)); + Assert.assertEquals("The array size is not zero", 0, ((String[]) o).length); + + // test with an empty array on the overloaded setter for different types. + // so, the assignment should fail with logging 'The ambiguous property, arr2, should have failed.' + try { + assignArray = jexl.createExpression("quux.arr2 = [ ]"); + o = assignArray.evaluate(jc); + Assert.fail("The arr2 property shouldn't be set due to its ambiguity (overloaded setters with different types)."); + } catch (JexlException.Property e) { + //System.out.println("Expected ambiguous property setting exception: " + e); + } + Assert.assertNull("The arr2 property value should remain as null, not an empty array.", quux.arr2); + } + + @Test + public void test147b() throws Exception { + String[] scripts = {"var x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x.one", // results to 1 + "x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x.one",// results to 1 + "x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x['one']",//results to 1 + "var x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x['one']"// result to null? + }; + + JexlEngine jexl = new Engine(); + JexlContext jc = new MapContext(); + for (String s : scripts) { + Object o = jexl.createScript(s).execute(jc); + Assert.assertEquals(1, o); + } + } + + @Test + public void test147c() throws Exception { + String[] scripts = { + "var x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x.one", + "x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x.one", + "x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x['one']", + "var x = new ('java.util.HashMap'); x.one = 1; x.two = 2; x['one']" + }; + JexlEngine jexl = new Engine(); + for (String s : scripts) { + JexlContext jc = new MapContext(); + Object o = jexl.createScript(s).execute(jc); + Assert.assertEquals(1, o); + } + } + + @Test + public void test5115a() throws Exception { + String str = "{\n" + + " var x = \"A comment\";\n" + + " var y = \"A comment\";\n" + + "}"; + try { + JexlEngine jexl = new Engine(); + JexlScript s = jexl.createScript(str); + } catch (JexlException.Parsing xparse) { + throw xparse; + } + } + + @Test + public void test5115b() throws Exception { + String str = "{\n" + + " var x = \"A comment\";\n" + + "}"; + try { + JexlEngine jexl = new Engine(); + JexlScript s = jexl.createScript(str); + } catch (JexlException.Parsing xparse) { + throw xparse; + } + } + + static final String TESTA = "src/test/scripts/testA.jexl"; + + @Test + public void test5115c() throws Exception { + URL testUrl = new File(TESTA).toURI().toURL(); + try { + JexlEngine jexl = new Engine(); + JexlScript s = jexl.createScript(testUrl); + } catch (JexlException.Parsing xparse) { + throw xparse; + } + } + + public static class Utils { + public List asList(T[] array) { + return Arrays.asList(array); + } + + public List asList(int[] array) { + List l = new ArrayList(array.length); + for (int i : array) { + l.add(i); + } + return l; + } + } + + @Test + public void test148a() throws Exception { + JexlEngine jexl = new Engine(); + JexlContext jc = new MapContext(); + jc.set("u", new Utils()); + + String src = "u.asList(['foo', 'bar'])"; + JexlScript e = jexl.createScript(src); + Object o = e.execute(jc); + Assert.assertTrue(o instanceof List); + Assert.assertEquals(Arrays.asList("foo", "bar"), o); + + src = "u.asList([1, 2])"; + e = jexl.createScript(src); + o = e.execute(jc); + Assert.assertTrue(o instanceof List); + Assert.assertEquals(Arrays.asList(1, 2), o); + } + + @Test + public void test155() throws Exception { + JexlEngine jexlEngine = new Engine(); + JexlExpression jexlExpresssion = jexlEngine.createExpression("first.second.name"); + JexlContext jc = new MapContext(); + jc.set("first.second.name", "RIGHT"); + jc.set("name", "WRONG"); + Object value = jexlExpresssion.evaluate(jc); + Assert.assertEquals("RIGHT", value.toString()); + } + + public static class Question42 extends MapContext { + public String functionA(String arg) { + return "a".equals(arg) ? "A" : ""; + } + + public String functionB(String arg) { + return "b".equals(arg) ? "B" : ""; + } + + public String functionC(String arg) { + return "c".equals(arg) ? "C" : ""; + } + + public String functionD(String arg) { + return "d".equals(arg) ? "D" : ""; + } + } + + public static class Arithmetic42 extends JexlArithmetic { + public Arithmetic42() { + super(false); + } + + public Object and(String lhs, String rhs) { + if (rhs.isEmpty()) { + return ""; + } + if (lhs.isEmpty()) { + return ""; + } + return lhs + rhs; + } + + public Object or(String lhs, String rhs) { + if (rhs.isEmpty()) { + return lhs; + } + if (lhs.isEmpty()) { + return rhs; + } + return lhs + rhs; + } + } + + @Test + public void testQuestion42() throws Exception { + JexlEngine jexl = new JexlBuilder().arithmetic(new Arithmetic42()).create(); + JexlContext jc = new Question42(); + + String str0 = "(functionA('z') | functionB('b')) & (functionC('c') | functionD('d') ) "; + JexlExpression expr0 = jexl.createExpression(str0); + Object value0 = expr0.evaluate(jc); + Assert.assertEquals("BCD", value0); + + String str1 = "(functionA('z') & functionB('b')) | (functionC('c') & functionD('d') ) "; + JexlExpression expr1 = jexl.createExpression(str1); + Object value1 = expr1.evaluate(jc); + Assert.assertEquals("CD", value1); + } + + @Test + public void test179() throws Exception { + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().create(); + String src = "x = new ('java.util.HashSet'); x.add(1); x"; + JexlScript e = jexl.createScript(src); + Object o = e.execute(jc); + Assert.assertTrue(o instanceof Set); + Assert.assertTrue(((Set) o).contains(1)); + } + + public static class C192 { + public C192() { + } + + public static Integer callme(Integer n) { + if (n == null) { + return null; + } else { + return n >= 0 ? 42 : -42; + } + } + + public static Object kickme() { + return C192.class; + } + } + + @Test + public void test192() throws Exception { + JexlContext jc = new MapContext(); + jc.set("x.y.z", C192.class); + JexlEngine jexl = new JexlBuilder().create(); + JexlExpression js0 = jexl.createExpression("x.y.z.callme(t)"); + jc.set("t", null); + Assert.assertNull(js0.evaluate(jc)); + jc.set("t", 10); + Assert.assertEquals(42, js0.evaluate(jc)); + jc.set("t", -10); + Assert.assertEquals(-42, js0.evaluate(jc)); + jc.set("t", null); + Assert.assertNull(js0.evaluate(jc)); + js0 = jexl.createExpression("x.y.z.kickme().callme(t)"); + jc.set("t", null); + Assert.assertNull(js0.evaluate(jc)); + jc.set("t", 10); + Assert.assertEquals(42, js0.evaluate(jc)); + jc.set("t", -10); + Assert.assertEquals(-42, js0.evaluate(jc)); + jc.set("t", null); + Assert.assertNull(js0.evaluate(jc)); + } + + @Test + public void test199() throws Exception { + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().arithmetic(new JexlArithmetic(false)).create(); + + JexlScript e = jexl.createScript("(x, y)->{ x + y }"); + Object r = e.execute(jc, true, "EURT"); + Assert.assertEquals("trueEURT", r); + r = e.execute(jc, "ELSAF", false); + Assert.assertEquals("ELSAFfalse", r); + } + +} diff --git a/src/test/java/org/apache/commons/jexl3/IssuesTest200.java b/src/test/java/org/apache/commons/jexl3/IssuesTest200.java new file mode 100644 index 000000000..5f4b7e48e --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/IssuesTest200.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import java.util.TreeSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Test cases for reported issue between JEXL-200 and JEXL-299. + */ +@SuppressWarnings({"boxing", "UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) +public class IssuesTest200 extends JexlTestCase { + public IssuesTest200() { + super("IssuesTest200", null); + } + + @Before + @Override + public void setUp() throws Exception { + // ensure jul logging is only error to avoid warning in silent mode + java.util.logging.Logger.getLogger(JexlEngine.class.getName()).setLevel(java.util.logging.Level.SEVERE); + } + + public static class Eval { + private JexlEngine jexl; + + public JexlScript fn(String src) { + return jexl.createScript(src); + } + + void setJexl(JexlEngine je) { + jexl = je; + } + } + + @Test + public void test200() throws Exception { + JexlContext jc = new MapContext(); + Map funcs = new HashMap(); + Eval eval = new Eval(); + funcs.put(null, eval); + JexlEngine jexl = new JexlBuilder().namespaces(funcs).create(); + eval.setJexl(jexl); + String src = "var f = fn(\'(x)->{x + 42}\'); f(y)"; + JexlScript s200 = jexl.createScript(src, "y"); + Assert.assertEquals(142, s200.execute(jc, 100)); + Assert.assertEquals(52, s200.execute(jc, 10)); + } + + @Test + public void test200b() throws Exception { + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().create(); + JexlScript e = jexl.createScript("var x = 0; var f = (y)->{ x = y; }; f(42); x"); + Object r = e.execute(jc); + Assert.assertEquals(0, r); + } + + @Test + public void test209a() throws Exception { + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().create(); + JexlScript e = jexl.createScript("var x = new('java.util.HashMap'); x.a = ()->{return 1}; x['a']()"); + Object r = e.execute(jc); + Assert.assertEquals(1, r); + } + + @Test + public void test209b() throws Exception { + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().create(); + JexlScript e = jexl.createScript("var x = new('java.util.HashMap'); x['a'] = ()->{return 1}; x.a()"); + Object r = e.execute(jc); + Assert.assertEquals(1, r); + } + + public class T210 { + public void npe() { + throw new NullPointerException("NPE210"); + } + } + + @Test + public void test210() throws Exception { + JexlContext jc = new MapContext(); + jc.set("v210", new T210()); + JexlEngine jexl = new JexlBuilder().strict(false).silent(false).create(); + JexlScript e = jexl.createScript("v210.npe()"); + try { + e.execute(jc); + Assert.fail("should have thrown an exception"); + } catch(JexlException xjexl) { + Throwable th = xjexl.getCause(); + Assert.assertEquals("NPE210", th.getMessage()); + } + } + + @Test + public void test217() throws Exception { + JexlEvalContext jc = new JexlEvalContext(); + jc.set("foo", new int[]{0, 1, 2, 42}); + JexlEngine jexl; + JexlScript e; + Object r; + jexl = new JexlBuilder().strict(false).silent(false).create(); + e = jexl.createScript("foo[3]"); + r = e.execute(jc); + Assert.assertEquals(42, r); + + // cache and fail? + jc.set("foo", new int[]{0, 1}); + jc.setStrict(true); + try { + r = e.execute(jc); + Assert.fail("should have thrown an exception"); + } catch(JexlException xjexl) { + Throwable th = xjexl.getCause(); + Assert.assertTrue(ArrayIndexOutOfBoundsException.class.equals(th.getClass())); + } + // + jc.setStrict(false); + r = e.execute(jc); + Assert.assertNull("oob adverted", r); + } + + + @Test + public void test221() throws Exception { + JexlEvalContext jc = new JexlEvalContext(); + Map map = new HashMap(); + map.put("one", 1); + jc.set("map", map); + JexlEngine jexl = new JexlBuilder().cache(256).create(); + JexlScript e = jexl.createScript("(x)->{ map[x] }"); + Object r; + r = e.execute(jc, (Object) null); + Assert.assertEquals(null, r); + r = e.execute(jc, (Object) null); + Assert.assertEquals(null, r); + r = e.execute(jc, "one"); + Assert.assertEquals(1, r); + } + + + public static class JexlArithmetic224 extends JexlArithmetic { + public JexlArithmetic224(boolean astrict) { + super(astrict); + } + + protected Object nth(Collection c, int i) { + if (c instanceof List) { + // tell engine to use default + return JexlEngine.TRY_FAILED; + } + for (Object o : c) { + if (i-- == 0) { + return o; + } + } + return null; + } + + public Object propertyGet(Collection c, Number n) { + return nth(c, n.intValue()); + } + + public Object arrayGet(Collection c, Number n) { + return nth(c, n.intValue()); + } + + public Object call(Collection c, Number n) { + if (c instanceof List) { + return ((List) c).get(n.intValue()); + } + return nth(c, n.intValue()); + } + } + + @Test + public void test224() throws Exception { + List a0 = Arrays.asList("one", "two"); + Set a1 = new TreeSet(a0); + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().arithmetic(new JexlArithmetic224(true)).create(); + Object r; + JexlScript e = jexl.createScript("(map, x)->{ map[x] }"); + r = e.execute(jc, a0, 1); + Assert.assertEquals("two", r); + r = e.execute(jc, a1, 1); + Assert.assertEquals("two", r); + e = jexl.createScript("(map)->{ map.1 }"); + r = e.execute(jc, a0); + Assert.assertEquals("two", r); + r = e.execute(jc, a1); + Assert.assertEquals("two", r); + e = jexl.createScript("(map, x)->{ map(x) }"); + r = e.execute(jc, a0, 1); + Assert.assertEquals("two", r); + r = e.execute(jc, a1, 1); + Assert.assertEquals("two", r); + } + + public static class Context225 extends MapContext { + public String bar(){ + return "bar"; + } + } + + @Test + public void test225() throws Exception { + Context225 df = new Context225(); + JexlEngine jexl = new JexlBuilder().create(); + + JexlExpression expression = jexl.createExpression("bar()"); + Assert.assertEquals("bar", expression.evaluate(df)); + ObjectContext context = new ObjectContext(jexl, df); + Assert.assertEquals("bar", expression.evaluate(context)); + } + + private static void handle(ExecutorService pool, final JexlScript script, final Map payload) { + pool.submit(new Runnable() { + @Override public void run() { + script.execute(new MapContext(payload)); + } + }); + } + + @Test + public void test241() throws Exception { + ExecutorService pool; + JexlScript script = new JexlBuilder().create().createScript("`${item}`"); + + pool = Executors.newFixedThreadPool(4); + + Map m1 = new HashMap(); + m1.put("item", "A"); + Map m2 = new HashMap(); + m2.put("item", "B"); + + handle(pool, script, m1); + script.execute(new MapContext(m2)); + pool.shutdown(); + } + + @Test + public void test242() throws Exception { + Double a = -40.05d; + Double b = -8.01d; + Double c = a + b; + final JexlContext context = new MapContext(); + context.set("a", a); + context.set("b", b); + JexlEngine JEXL_ENGINE = new JexlBuilder().strict(true).silent(true).create(); + JexlExpression jsp = JEXL_ENGINE.createExpression("a + b"); + Double e = (Double) jsp.evaluate(context); + Assert.assertTrue(Double.doubleToLongBits(e) + " != " + Double.doubleToLongBits(c), c.doubleValue() == e.doubleValue()); + Assert.assertTrue(Double.doubleToLongBits(e) + " != " + Double.doubleToLongBits(c), a + b == e); + } + + + @Test + public void test243a() throws Exception { + JexlEngine jexl = new JexlBuilder().cache(32).create(); + JexlScript script = jexl.createScript("while(true);"); + try { + JexlExpression expr = jexl.createExpression("while(true);"); + Assert.fail("should have failed!, expr do not allow 'while' statement"); + } catch (JexlException.Parsing xparse) { + // ok + } catch (JexlException xother) { + // ok + } + } + + public static class Foo245 { + private Object bar = null; + + void setBar(Object bar) { + this.bar = bar; + } + + public Object getBar() { + return bar; + } + } + + @Test + public void test245() throws Exception { + MapContext ctx = new MapContext(); + Foo245 foo245 = new Foo245(); + ctx.set("foo", foo245); + + JexlEngine engine = new JexlBuilder().strict(true).silent(false).create(); + JexlExpression foobar = engine.createExpression("foo.bar"); + JexlExpression foobaz = engine.createExpression("foo.baz"); + JexlExpression foobarbaz = engine.createExpression("foo.bar.baz"); + // add ambiguity with null & not-null + Object[] args = { null, 245 }; + for(Object arg : args ){ + foo245.setBar(arg); + // ok + Assert.assertEquals(foo245.getBar(), foobar.evaluate(ctx)); + // fail level 1 + try { + foobaz.evaluate(ctx); + Assert.fail("foo.baz is not solvable"); + } catch(JexlException xp) { + Assert.assertTrue(xp instanceof JexlException.Property); + } + // fail level 2 + try { + foobarbaz.evaluate(ctx); + Assert.fail("foo.bar.baz is not solvable"); + } catch(JexlException xp) { + Assert.assertTrue(xp instanceof JexlException.Property); + } + } + } +} diff --git a/src/test/java/org/apache/commons/jexl3/JXLTTest.java b/src/test/java/org/apache/commons/jexl3/JXLTTest.java index 3d808991e..1ae1dbfd6 100644 --- a/src/test/java/org/apache/commons/jexl3/JXLTTest.java +++ b/src/test/java/org/apache/commons/jexl3/JXLTTest.java @@ -680,10 +680,14 @@ public void testOneLinerVar() throws Exception { @Test public void testInterpolation() throws Exception { - context.set("user", "Dimitri"); String expr = "`Hello \n${user}`"; - Object value = JEXL.createScript(expr).execute(context); + JexlScript script = JEXL.createScript(expr); + context.set("user", "Dimitri"); + Object value = script.execute(context); Assert.assertEquals(expr, "Hello \nDimitri", value); + context.set("user", "Rahul"); + value = script.execute(context); + Assert.assertEquals(expr, "Hello \nRahul", value); } @Test @@ -718,10 +722,13 @@ public void testInterpolationParameter() throws Exception { String expr = "(user)->{`Hello \n${user}`}"; Object value = JEXL.createScript(expr).execute(context, "Henrib"); Assert.assertEquals(expr, "Hello \nHenrib", value); + value = JEXL.createScript(expr).execute(context, "Dimitri"); + Assert.assertEquals(expr, "Hello \nDimitri", value); } // // -// @Test public void testDeferredTemplate() throws Exception { +// @Test +// public void testDeferredTemplate() throws Exception { // JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader( // "select * from \n"+ // "##for(var c : tables) {\n"+ diff --git a/src/test/java/org/apache/commons/jexl3/JexlEvalContext.java b/src/test/java/org/apache/commons/jexl3/JexlEvalContext.java index cda8fc13f..4a64f0742 100644 --- a/src/test/java/org/apache/commons/jexl3/JexlEvalContext.java +++ b/src/test/java/org/apache/commons/jexl3/JexlEvalContext.java @@ -24,7 +24,10 @@ /** * A JEXL evaluation environment wrapping variables, namespace and options. */ -public class JexlEvalContext implements JexlContext, JexlContext.NamespaceResolver, JexlEngine.Options { +public class JexlEvalContext implements + JexlContext, + JexlContext.NamespaceResolver, + JexlEngine.Options { /** The marker for the empty vars. */ private static final Map EMPTY_MAP = Collections.emptyMap(); /** The variables.*/ @@ -35,6 +38,8 @@ public class JexlEvalContext implements JexlContext, JexlContext.NamespaceResolv private Boolean silent = null; /** Whether the engine should be strict. */ private Boolean strict = null; + /** Whether the engine should be cancellable. */ + private Boolean cancellable = null; /** Whether the arithmetic should be strict. */ private Boolean mathStrict = null; /** The math scale the arithmetic should use. */ @@ -109,10 +114,22 @@ public Object resolveNamespace(String name) { public void clearOptions() { silent = null; strict = null; + cancellable = null; mathScale = -1; mathContext = null; } + /** + * Set options from engine. + * @param jexl the engine + */ + public void setOptions(JexlEngine jexl) { + silent = jexl.isSilent(); + strict = jexl.isStrict(); + mathScale = jexl.getArithmetic().getMathScale(); + mathContext = jexl.getArithmetic().getMathContext(); + } + /** * Sets whether the engine will throw JexlException during evaluation when an error is triggered. * @param s true means no JexlException will occur, false allows them @@ -126,14 +143,27 @@ public Boolean isSilent() { return this.silent; } + /** + * Sets whether the engine will throw JexlException.Cancel during evaluation when interrupted. + * @param s true means JexlException.Cancel will be thrown, false implies null will be returned + */ + public void setCancellable(boolean c) { + this.cancellable = c ? Boolean.TRUE : Boolean.FALSE; + } + + @Override + public Boolean isCancellable() { + return this.cancellable; + } + /** * Sets the engine and arithmetic strict flags in one call. * @param se the engine strict flag * @param sa the arithmetic strict flag */ - public void setStrict(boolean se, boolean sa) { - this.strict = se ? Boolean.TRUE : Boolean.FALSE; - this.mathStrict = sa ? Boolean.TRUE : Boolean.FALSE; + public void setStrict(Boolean se, Boolean sa) { + this.strict = se == null? null : se ? Boolean.TRUE : Boolean.FALSE; + this.mathStrict = sa == null? null : sa ? Boolean.TRUE : Boolean.FALSE; } /** diff --git a/src/test/java/org/apache/commons/jexl3/JexlTest.java b/src/test/java/org/apache/commons/jexl3/JexlTest.java index 0e2a8bed7..8db318bdb 100644 --- a/src/test/java/org/apache/commons/jexl3/JexlTest.java +++ b/src/test/java/org/apache/commons/jexl3/JexlTest.java @@ -89,7 +89,7 @@ public void testStringLit() throws Exception { */ JexlContext jc = new MapContext(); jc.set("foo", new Foo()); - assertExpression(jc, "foo.get(\"woogie\")", "Repeat : woogie"); + assertExpression(jc, "foo.repeat(\"woogie\")", "Repeat : woogie"); } @Test @@ -213,21 +213,20 @@ public void testSize() throws Exception { // support generic int size() method BitSet bitset = new BitSet(5); jc.set("bitset", bitset); -// -// assertExpression(jc, "size(s)", new Integer(5)); -// assertExpression(jc, "size(array)", new Integer(5)); -// assertExpression(jc, "size(list)", new Integer(5)); -// assertExpression(jc, "size(map)", new Integer(5)); -// assertExpression(jc, "size(set)", new Integer(5)); -// assertExpression(jc, "size(bitset)", new Integer(64)); -// assertExpression(jc, "list.size()", new Integer(5)); -// assertExpression(jc, "map.size()", new Integer(5)); -// assertExpression(jc, "set.size()", new Integer(5)); -// assertExpression(jc, "bitset.size()", new Integer(64)); -// -// assertExpression(jc, "list.get(size(list) - 1)", "5"); -// assertExpression(jc, "list[size(list) - 1]", "5"); - // here + + assertExpression(jc, "size(s)", new Integer(5)); + assertExpression(jc, "size(array)", new Integer(5)); + assertExpression(jc, "size(list)", new Integer(5)); + assertExpression(jc, "size(map)", new Integer(5)); + assertExpression(jc, "size(set)", new Integer(5)); + assertExpression(jc, "size(bitset)", new Integer(64)); + assertExpression(jc, "list.size()", new Integer(5)); + assertExpression(jc, "map.size()", new Integer(5)); + assertExpression(jc, "set.size()", new Integer(5)); + assertExpression(jc, "bitset.size()", new Integer(64)); + + assertExpression(jc, "list.get(size(list) - 1)", "5"); + assertExpression(jc, "list[size(list) - 1]", "5"); assertExpression(jc, "list.get(list.size() - 1)", "5"); } @@ -236,15 +235,17 @@ public void testSizeAsProperty() throws Exception { JexlContext jc = new MapContext(); Map map = new HashMap(); map.put("size", "cheese"); + map.put("si & ze", "cheese"); jc.set("map", map); jc.set("foo", new Foo()); assertExpression(jc, "map['size']", "cheese"); -// PR - unsure whether or not we should support map.size or force usage of the above 'escaped' version -// assertExpression(jc, "map.size", "cheese"); - assertExpression(jc, "foo.getSize()", new Integer(22)); - // failing assertion for size property - //assertExpression(jc, "foo.size", new Integer(22)); + assertExpression(jc, "map['si & ze']", "cheese"); + assertExpression(jc, "map.'si & ze'", "cheese"); + assertExpression(jc, "map.size()", 2); + assertExpression(jc, "size(map)", 2); + assertExpression(jc, "foo.getSize()", 22); + assertExpression(jc, "foo.'size'", 22); } /** @@ -265,7 +266,7 @@ public void testNew() throws Exception { Assert.assertEquals(expr.toString(), new Float(100.0), value); expr = JEXL.createExpression("new(foo).quux"); value = expr.evaluate(jc); - Assert.assertEquals(expr.toString(), "Repeat : quux", value); + Assert.assertEquals(expr.toString(), "String : quux", value); } /** @@ -686,7 +687,7 @@ public void testAssignment() throws Exception { Foo foo = new Foo(); jc.set("foo", foo); Parser parser = new Parser(new StringReader(";")); - parser.parse(null, "aString = 'World';", null, false, false); + parser.parse(null, new JexlFeatures().register(false), "aString = 'World';", null); assertExpression(jc, "hello = 'world'", "world"); Assert.assertEquals("hello variable not changed", "world", jc.get("hello")); diff --git a/src/test/java/org/apache/commons/jexl3/LambdaTest.java b/src/test/java/org/apache/commons/jexl3/LambdaTest.java index cb052f7b1..a4f18e961 100644 --- a/src/test/java/org/apache/commons/jexl3/LambdaTest.java +++ b/src/test/java/org/apache/commons/jexl3/LambdaTest.java @@ -33,6 +33,7 @@ public LambdaTest() { super("LambdaTest"); } + @Test public void testScriptArguments() throws Exception { JexlEngine jexl = new Engine(); JexlScript s = jexl.createScript(" x + x ", "x"); diff --git a/src/test/java/org/apache/commons/jexl3/MethodTest.java b/src/test/java/org/apache/commons/jexl3/MethodTest.java index 8e38d286d..9849fcfc3 100644 --- a/src/test/java/org/apache/commons/jexl3/MethodTest.java +++ b/src/test/java/org/apache/commons/jexl3/MethodTest.java @@ -21,6 +21,7 @@ import org.apache.commons.jexl3.introspection.JexlMethod; import org.apache.commons.jexl3.junit.Asserter; import java.util.Arrays; +import java.util.Date; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -191,6 +192,25 @@ public static int PLUS20(int num) { public static Class NPEIfNull(Object x) { return x.getClass(); } + + public Object over(String f, int i) { + return f + " + " + i; + } + + public Object over(String f, Date g) { + return f + " + " + g; + } + + public Object over(String f, String g) { + return f + " + " + g; + } + } + + public static class FunctorOver extends Functor { + + public Object over(Object f, Object g) { + return f + " + " + g; + } } @Test @@ -210,6 +230,31 @@ public void testInvoke() throws Exception { } catch (Exception xj0) { // ignore } + + Object result; + try { + result = JEXL.invokeMethod(func, "over", "foo", 42); + Assert.assertEquals("foo + 42", result); + } catch (Exception xj0) { + // ignore + result = xj0; + } + + try { + result = JEXL.invokeMethod(func, "over", null, null); + Assert.fail("method should have thrown!"); + } catch (Exception xj0) { + // ignore + result = xj0; + } + + func = new FunctorOver(); + try { + result = JEXL.invokeMethod(func, "over", null, null); + Assert.assertEquals("null + null", result); + } catch (Exception xj0) { + Assert.fail("method should not have thrown!"); + } } /** @@ -513,7 +558,7 @@ public void testScriptCall() throws Exception { final JexlArithmetic ja = JEXL.getArithmetic(); JexlMethod mplus = new JexlMethod() { @Override - public Object invoke(Object obj, Object[] params) throws Exception { + public Object invoke(Object obj, Object ... params) throws Exception { if (obj instanceof Map) { return ja.add(params[0], params[1]); } else { @@ -522,7 +567,7 @@ public Object invoke(Object obj, Object[] params) throws Exception { } @Override - public Object tryInvoke(String name, Object obj, Object[] params) { + public Object tryInvoke(String name, Object obj, Object ... params) { try { if ("plus".equals(name)) { return invoke(obj, params); @@ -579,4 +624,72 @@ public void testFizzCall() throws Exception { o = bar.execute(context); Assert.assertEquals("Wrong choice", "champaign", o); } + + public static class ZArithmetic extends JexlArithmetic { + public ZArithmetic(boolean astrict) { + super(astrict); + } + + public int zzzz(int z) { + return 38 + z; + } + } + + public static class ZSpace { + public int zzz(int z) { + return 39 + z; + } + } + + public static class ZContext extends MapContext { + public ZContext(Map map) { + super(map); + } + + public int zz(int z) { + return 40 + z; + } + + public int z(int z) { + return 181 + z; + } + } + + @Test + public void testVariousFunctionLocation() throws Exception { + // see JEXL-190 + Map vars = new HashMap(); + Map funcs = new HashMap(); + funcs.put(null, new ZSpace()); + JexlEngine jexl = new JexlBuilder().namespaces(funcs).arithmetic(new ZArithmetic(true)).create(); + + JexlContext zjc = new ZContext(vars); // that implements a z(int x) function + String z41 = "z(41)"; + JexlScript callz41 = jexl.createScript(z41); + Object onovar = callz41.execute(zjc); + Assert.assertEquals(222, onovar); + + // override z() with global var + JexlScript z241 = jexl.createScript("(x)->{ return x + 241}"); + vars.put("z", z241); + Object oglobal = callz41.execute(zjc); + Assert.assertEquals(282, oglobal); + // clear global and execute again + vars.remove("z"); + onovar = callz41.execute(zjc); + Assert.assertEquals(222, onovar); + + // override z() with local var + String slocal = "var z = (x)->{ return x + 141}; z(1)"; + JexlScript jlocal = jexl.createScript(slocal); + Object olocal = jlocal.execute(zjc); + Assert.assertEquals(142, olocal); + + // and now try the context, the null namespace and the arithmetic + Assert.assertEquals(42, jexl.createScript("zz(2)").execute(zjc)); + Assert.assertEquals(42, jexl.createScript("zzz(3)").execute(zjc)); + Assert.assertEquals(42, jexl.createScript("zzzz(4)").execute(zjc)); + } + + } diff --git a/src/test/java/org/apache/commons/jexl3/PragmaTest.java b/src/test/java/org/apache/commons/jexl3/PragmaTest.java index 0463aac71..3eff5790d 100644 --- a/src/test/java/org/apache/commons/jexl3/PragmaTest.java +++ b/src/test/java/org/apache/commons/jexl3/PragmaTest.java @@ -39,16 +39,31 @@ public PragmaTest() { public void testPragmas() throws Exception { JexlContext jc = new MapContext(); try { - JexlScript script = JEXL.createScript("#pragma one 1\n#pragma the.very.hard 'truth'\n2;"); - Assert.assertTrue(script != null); - Map pragmas = script.getPragmas(); - Assert.assertEquals(2, pragmas.size()); - Assert.assertEquals(1, pragmas.get("one")); - Assert.assertEquals("truth", pragmas.get("the.very.hard")); - } catch(JexlException xjexl) { + JexlScript script = JEXL.createScript("#pragma one 1\n#pragma the.very.hard 'truth'\n2;"); + Assert.assertTrue(script != null); + Map pragmas = script.getPragmas(); + Assert.assertEquals(2, pragmas.size()); + Assert.assertEquals(1, pragmas.get("one")); + Assert.assertEquals("truth", pragmas.get("the.very.hard")); + } catch (JexlException xjexl) { String s = xjexl.toString(); } } + @Test + public void testJxltPragmas() throws Exception { + JexlContext jc = new MapContext(); + try { + JxltEngine engine = new JexlBuilder().create().createJxltEngine(); + JxltEngine.Template tscript = engine.createTemplate("$$ #pragma one 1\n$$ #pragma the.very.hard 'truth'\n2;"); + Assert.assertTrue(tscript != null); + Map pragmas = tscript.getPragmas(); + Assert.assertEquals(2, pragmas.size()); + Assert.assertEquals(1, pragmas.get("one")); + Assert.assertEquals("truth", pragmas.get("the.very.hard")); + } catch (JexlException xjexl) { + String s = xjexl.toString(); + } + } } diff --git a/src/test/java/org/apache/commons/jexl3/PropertyAccessTest.java b/src/test/java/org/apache/commons/jexl3/PropertyAccessTest.java index 09137c57b..7d81cdb0f 100644 --- a/src/test/java/org/apache/commons/jexl3/PropertyAccessTest.java +++ b/src/test/java/org/apache/commons/jexl3/PropertyAccessTest.java @@ -20,6 +20,7 @@ import java.util.Map; import org.apache.commons.jexl3.internal.Debugger; +import org.apache.commons.jexl3.internal.introspection.IndexedType; import org.apache.commons.jexl3.junit.Asserter; import org.junit.Assert; import org.junit.Before; @@ -45,8 +46,8 @@ public void setUp() { asserter = new Asserter(JEXL); } - - @Test public void testPropertyProperty() throws Exception { + @Test + public void testPropertyProperty() throws Exception { Integer i42 = Integer.valueOf(42); Integer i43 = Integer.valueOf(43); String s42 = "fourty-two"; @@ -71,11 +72,14 @@ public void setUp() { } } - public static class Container { + /** + * A base property container; can only set from string. + */ + public static class PropertyContainer { String value0; int value1; - public Container(String name, int number) { + public PropertyContainer(String name, int number) { value0 = name; value1 = number; } @@ -90,16 +94,104 @@ public Object getProperty(String name) { } } - public Object getProperty(int ref) { - if (0 == ref) { - return value0; - } else if (1 == ref) { - return value1; - } else { + public void setProperty(String name, String value) { + if ("name".equals(name)) { + this.value0 = value.toUpperCase(); + } + if ("number".equals(name)) { + this.value1 = Integer.parseInt(value) + 1000; + } + } + } + + + /** + * Overloads propertySet. + */ + public static class PropertyArithmetic extends JexlArithmetic { + int ncalls = 0; + + public PropertyArithmetic(boolean astrict) { + super(astrict); + } + + public Object propertySet(IndexedType.IndexedContainer map, String key, Integer value) { + if (map.getContainerClass().equals(PropertyContainer.class) + && map.getContainerName().equals("property")) { + try { + map.set(key, value.toString()); + ncalls += 1; + } catch (Exception xany) { + throw new JexlException.Operator(null, key + "." + value.toString(), xany); + } return null; } + return JexlEngine.TRY_FAILED; } + public int getCalls() { + return ncalls; + } + } + + @Test + public void testInnerViaArithmetic() throws Exception { + PropertyArithmetic pa = new PropertyArithmetic(true); + JexlEngine jexl = new JexlBuilder().arithmetic(pa).debug(true).strict(true).cache(32).create(); + PropertyContainer quux = new PropertyContainer("bar", 169); + Object result; + + JexlScript getName = jexl.createScript("foo.property.name", "foo"); + result = getName.execute(null, quux); + Assert.assertEquals("bar", result); + int calls = pa.getCalls(); + JexlScript setName = jexl.createScript("foo.property.name = $0", "foo", "$0"); + setName.execute(null, quux, 123); + result = getName.execute(null, quux); + Assert.assertEquals("123", result); + setName.execute(null, quux, 456); + result = getName.execute(null, quux); + Assert.assertEquals("456", result); + Assert.assertEquals(calls + 2, pa.getCalls()); + setName.execute(null, quux, "quux"); + result = getName.execute(null, quux); + Assert.assertEquals("QUUX", result); + Assert.assertEquals(calls + 2, pa.getCalls()); + + JexlScript getNumber = jexl.createScript("foo.property.number", "foo"); + result = getNumber.execute(null, quux); + Assert.assertEquals(169, result); + JexlScript setNumber = jexl.createScript("foo.property.number = $0", "foo", "$0"); + setNumber.execute(null, quux, 42); + result = getNumber.execute(null, quux); + Assert.assertEquals(1042, result); + setNumber.execute(null, quux, 24); + result = getNumber.execute(null, quux); + Assert.assertEquals(1024, result); + Assert.assertEquals(calls + 4, pa.getCalls()); + setNumber.execute(null, quux, "42"); + result = getNumber.execute(null, quux); + Assert.assertEquals(1042, result); + Assert.assertEquals(calls + 4, pa.getCalls()); + } + + public static class Container extends PropertyContainer { + public Container(String name, int number) { + super(name, number); + } + + public Object getProperty(int ref) { + switch (ref) { + case 0: + return value0; + case 1: + return value1; + default: + return null; + } + } + + @Override public void setProperty(String name, String value) { if ("name".equals(name)) { this.value0 = value; @@ -125,11 +217,15 @@ public void setProperty(int ref, int value) { } } - @Test public void testInnerProperty() throws Exception { + @Test + public void testInnerProperty() throws Exception { + PropertyArithmetic pa = new PropertyArithmetic(true); + JexlEngine jexl = new JexlBuilder().arithmetic(pa).debug(true).strict(true).cache(32).create(); Container quux = new Container("quux", 42); JexlScript get; Object result; + int calls = pa.getCalls(); JexlScript getName = JEXL.createScript("foo.property.name", "foo"); result = getName.execute(null, quux); Assert.assertEquals("quux", result); @@ -173,10 +269,13 @@ public void setProperty(int ref, int value) { Assert.assertEquals(24, result); result = get1.execute(null, quux); Assert.assertEquals(24, result); + + Assert.assertEquals(calls, pa.getCalls()); } - @Test public void testStringIdentifier() throws Exception { + @Test + public void testStringIdentifier() throws Exception { Map foo = new HashMap(); JexlContext jc = new MapContext(); diff --git a/src/test/java/org/apache/commons/jexl3/PublicFieldsTest.java b/src/test/java/org/apache/commons/jexl3/PublicFieldsTest.java index 54edd7f3a..1f594daf3 100644 --- a/src/test/java/org/apache/commons/jexl3/PublicFieldsTest.java +++ b/src/test/java/org/apache/commons/jexl3/PublicFieldsTest.java @@ -33,6 +33,7 @@ public class PublicFieldsTest extends JexlTestCase { */ public static class Inner { public double aDouble = 42.0; + public static double NOT42 = -42.0; } /** @@ -61,14 +62,16 @@ public void setUp() { ctxt.set("pub", pub); } - @Test public void testGetInt() throws Exception { + @Test + public void testGetInt() throws Exception { JexlExpression get = JEXL.createExpression("pub.anInt"); Assert.assertEquals(42, get.evaluate(ctxt)); JEXL.setProperty(pub, "anInt", -42); Assert.assertEquals(-42, get.evaluate(ctxt)); } - @Test public void testSetInt() throws Exception { + @Test + public void testSetInt() throws Exception { JexlExpression set = JEXL.createExpression("pub.anInt = value"); ctxt.set("value", -42); Assert.assertEquals(-42, set.evaluate(ctxt)); @@ -83,14 +86,16 @@ public void setUp() { } catch(JexlException xjexl) {} } - @Test public void testGetString() throws Exception { + @Test + public void testGetString() throws Exception { JexlExpression get = JEXL.createExpression("pub.aString"); Assert.assertEquals(LOWER42, get.evaluate(ctxt)); JEXL.setProperty(pub, "aString", UPPER42); Assert.assertEquals(UPPER42, get.evaluate(ctxt)); } - @Test public void testSetString() throws Exception { + @Test + public void testSetString() throws Exception { JexlExpression set = JEXL.createExpression("pub.aString = value"); ctxt.set("value", UPPER42); Assert.assertEquals(UPPER42, set.evaluate(ctxt)); @@ -100,14 +105,16 @@ public void setUp() { Assert.assertEquals(LOWER42, JEXL.getProperty(pub, "aString")); } - @Test public void testGetInnerDouble() throws Exception { + @Test + public void testGetInnerDouble() throws Exception { JexlExpression get = JEXL.createExpression("pub.inner.aDouble"); Assert.assertEquals(42.0, get.evaluate(ctxt)); JEXL.setProperty(pub, "inner.aDouble", -42); Assert.assertEquals(-42.0, get.evaluate(ctxt)); } - @Test public void testSetInnerDouble() throws Exception { + @Test + public void testSetInnerDouble() throws Exception { JexlExpression set = JEXL.createExpression("pub.inner.aDouble = value"); ctxt.set("value", -42.0); Assert.assertEquals(-42.0, set.evaluate(ctxt)); @@ -122,4 +129,25 @@ public void setUp() { } catch(JexlException xjexl) {} } + public enum Gender { MALE, FEMALE }; + + @Test + public void testGetEnum() throws Exception { + ctxt.set("com.jexl.gender", Gender.class); + String src = "x = com.jexl.gender.FEMALE"; + JexlScript script = JEXL.createScript(src); + Object result = script.execute(ctxt); + Assert.assertEquals(Gender.FEMALE, result); + Assert.assertEquals(Gender.FEMALE, ctxt.get("x")); + } + + @Test + public void testGetStaticField() throws Exception { + ctxt.set("com.jexl", Inner.class); + String src = "x = com.jexl.NOT42"; + JexlScript script = JEXL.createScript(src); + Object result = script.execute(ctxt); + Assert.assertEquals(Inner.NOT42, result); + Assert.assertEquals(Inner.NOT42, ctxt.get("x")); + } } diff --git a/src/test/java/org/apache/commons/jexl3/ReadonlyContext.java b/src/test/java/org/apache/commons/jexl3/ReadonlyContext.java index e1f3e8e64..97fe8c18f 100644 --- a/src/test/java/org/apache/commons/jexl3/ReadonlyContext.java +++ b/src/test/java/org/apache/commons/jexl3/ReadonlyContext.java @@ -66,10 +66,14 @@ public Boolean isSilent() { @Override public Boolean isStrict() { - // egnie return options == null? null : options.isStrict(); } + @Override + public Boolean isCancellable() { + return options == null? null : options.isCancellable(); + } + @Override public Boolean isStrictArithmetic() { return options == null? null : options.isStrict(); diff --git a/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java b/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java index ec15877e3..0e4e34bc2 100644 --- a/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java +++ b/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java @@ -16,21 +16,29 @@ */ package org.apache.commons.jexl3; +import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.apache.commons.jexl3.internal.Script; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.junit.Assert; import org.junit.Test; /** * Tests around asynchronous script execution and interrupts. */ +@SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) public class ScriptCallableTest extends JexlTestCase { - //Logger LOGGER = Logger.getLogger(VarTest.class.getName()); + //private Log logger = LogFactory.getLog(JexlEngine.class); public ScriptCallableTest() { super("ScriptCallableTest"); } @@ -42,50 +50,104 @@ public void testFuture() throws Exception { ExecutorService executor = Executors.newFixedThreadPool(1); executor.submit(future); + Object t = 42; try { - future.get(100, TimeUnit.MILLISECONDS); + t = future.get(100, TimeUnit.MILLISECONDS); Assert.fail("should have timed out"); } catch (TimeoutException xtimeout) { // ok, ignore + future.cancel(true); + } finally { + executor.shutdown(); } - Thread.sleep(100); - future.cancel(true); Assert.assertTrue(future.isCancelled()); + Assert.assertEquals(42, t); } @Test - public void testCallable() throws Exception { - JexlScript e = JEXL.createScript("while(true);"); - Callable c = e.callable(null); + public void testCallableCancel() throws Exception { + List lr = null; + final Semaphore latch = new Semaphore(0); + JexlContext ctxt = new MapContext(); + ctxt.set("latch", latch); + + JexlScript e = JEXL.createScript("latch.release(); while(true);"); + final Script.Callable c = (Script.Callable) e.callable(ctxt); + Object t = 42; + Callable kc = new Callable() { + @Override + public Object call() throws Exception { + latch.acquire(); + return c.cancel(); + } + }; + ExecutorService executor = Executors.newFixedThreadPool(2); + Future future = executor.submit(c); + Future kfc = executor.submit(kc); + try { + Assert.assertTrue((Boolean) kfc.get()); + t = future.get(); + Assert.fail("should have been cancelled"); + } catch (ExecutionException xexec) { + // ok, ignore + Assert.assertTrue(xexec.getCause() instanceof JexlException.Cancel); + } finally { + lr = executor.shutdownNow(); + } + Assert.assertTrue(c.isCancelled()); + Assert.assertTrue(lr == null || lr.isEmpty()); + } + + @Test + public void testCallableTimeout() throws Exception { + List lr = null; + final Semaphore latch = new Semaphore(0); + JexlContext ctxt = new MapContext(); + ctxt.set("latch", latch); + + JexlScript e = JEXL.createScript("latch.release(); while(true);"); + Callable c = e.callable(ctxt); + Object t = 42; ExecutorService executor = Executors.newFixedThreadPool(1); Future future = executor.submit(c); try { - future.get(100, TimeUnit.MILLISECONDS); + latch.acquire(); + t = future.get(100, TimeUnit.MILLISECONDS); Assert.fail("should have timed out"); } catch (TimeoutException xtimeout) { // ok, ignore + future.cancel(true); + } finally { + lr = executor.shutdownNow(); } - future.cancel(true); Assert.assertTrue(future.isCancelled()); + Assert.assertEquals(42, t); + Assert.assertTrue(lr.isEmpty()); } @Test public void testCallableClosure() throws Exception { + List lr = null; JexlScript e = JEXL.createScript("function(t) {while(t);}"); Callable c = e.callable(null, Boolean.TRUE); + Object t = 42; ExecutorService executor = Executors.newFixedThreadPool(1); Future future = executor.submit(c); try { - future.get(100, TimeUnit.MILLISECONDS); + t = future.get(100, TimeUnit.MILLISECONDS); Assert.fail("should have timed out"); } catch (TimeoutException xtimeout) { // ok, ignore + future.cancel(true); + } finally { + lr = executor.shutdownNow(); } - future.cancel(true); Assert.assertTrue(future.isCancelled()); + Assert.assertEquals(42, t); + Assert.assertTrue(lr.isEmpty()); } public static class TestContext extends MapContext implements JexlContext.NamespaceResolver { @@ -110,104 +172,327 @@ public int waitInterrupt(int s) { } public int runForever() { - boolean x = false; while (true) { - if (x) { + if (Thread.currentThread().isInterrupted()) { break; } } return 1; } + + public int interrupt() throws InterruptedException { + Thread.currentThread().interrupt(); + return 42; + } + + public void sleep(long millis) throws InterruptedException { + try { + Thread.sleep(millis); + } catch (InterruptedException xint) { + throw xint; + } + } + + public int hangs(Object t) { + return 1; + } } @Test public void testNoWait() throws Exception { + List lr = null; JexlScript e = JEXL.createScript("wait(0)"); Callable c = e.callable(new TestContext()); ExecutorService executor = Executors.newFixedThreadPool(1); - Future future = executor.submit(c); - Object t = future.get(2, TimeUnit.SECONDS); - Assert.assertTrue(future.isDone()); - Assert.assertEquals(0, t); + try { + Future future = executor.submit(c); + Object t = future.get(2, TimeUnit.SECONDS); + Assert.assertTrue(future.isDone()); + Assert.assertEquals(0, t); + } finally { + lr = executor.shutdownNow(); + } + Assert.assertTrue(lr.isEmpty()); } @Test public void testWait() throws Exception { + List lr = null; JexlScript e = JEXL.createScript("wait(1)"); Callable c = e.callable(new TestContext()); ExecutorService executor = Executors.newFixedThreadPool(1); - Future future = executor.submit(c); - Object t = future.get(2, TimeUnit.SECONDS); - Assert.assertEquals(1, t); + try { + Future future = executor.submit(c); + Object t = future.get(2, TimeUnit.SECONDS); + Assert.assertEquals(1, t); + } finally { + lr = executor.shutdownNow(); + } + Assert.assertTrue(lr.isEmpty()); } @Test public void testCancelWait() throws Exception { + List lr = null; JexlScript e = JEXL.createScript("wait(10)"); Callable c = e.callable(new TestContext()); ExecutorService executor = Executors.newFixedThreadPool(1); - Future future = executor.submit(c); try { - future.get(100, TimeUnit.MILLISECONDS); - Assert.fail("should have timed out"); - } catch (TimeoutException xtimeout) { - // ok, ignore + Future future = executor.submit(c); + Object t = 42; + try { + t = future.get(100, TimeUnit.MILLISECONDS); + Assert.fail("should have timed out"); + } catch (TimeoutException xtimeout) { + // ok, ignore + future.cancel(true); + } + Assert.assertTrue(future.isCancelled()); + Assert.assertEquals(42, t); + } finally { + lr = executor.shutdownNow(); } - future.cancel(true); - Assert.assertTrue(future.isCancelled()); + Assert.assertTrue(lr.isEmpty()); } @Test public void testCancelWaitInterrupt() throws Exception { + List lr = null; JexlScript e = JEXL.createScript("waitInterrupt(42)"); Callable c = e.callable(new TestContext()); ExecutorService executor = Executors.newFixedThreadPool(1); Future future = executor.submit(c); + Object t = 42; + try { - future.get(100, TimeUnit.MILLISECONDS); + t = future.get(100, TimeUnit.MILLISECONDS); Assert.fail("should have timed out"); } catch (TimeoutException xtimeout) { // ok, ignore + future.cancel(true); + } finally { + lr = executor.shutdownNow(); } - future.cancel(true); Assert.assertTrue(future.isCancelled()); + Assert.assertEquals(42, t); + Assert.assertTrue(lr.isEmpty()); } @Test public void testCancelForever() throws Exception { - JexlScript e = JEXL.createScript("runForever()"); - Callable c = e.callable(new TestContext()); + List lr = null; + final Semaphore latch = new Semaphore(0); + JexlContext ctxt = new TestContext(); + ctxt.set("latch", latch); + + JexlScript e = JEXL.createScript("latch.release(); runForever()"); + Callable c = e.callable(ctxt); ExecutorService executor = Executors.newFixedThreadPool(1); Future future = executor.submit(c); + Object t = 42; + try { - future.get(100, TimeUnit.MILLISECONDS); + latch.acquire(); + t = future.get(100, TimeUnit.MILLISECONDS); Assert.fail("should have timed out"); } catch (TimeoutException xtimeout) { // ok, ignore + future.cancel(true); + } finally { + lr = executor.shutdownNow(); } - future.cancel(true); Assert.assertTrue(future.isCancelled()); + Assert.assertEquals(42, t); + Assert.assertTrue(lr.isEmpty()); } @Test public void testCancelLoopWait() throws Exception { + List lr = null; JexlScript e = JEXL.createScript("while (true) { wait(10) }"); Callable c = e.callable(new TestContext()); ExecutorService executor = Executors.newFixedThreadPool(1); Future future = executor.submit(c); + Object t = 42; + try { - future.get(100, TimeUnit.MILLISECONDS); + t = future.get(100, TimeUnit.MILLISECONDS); Assert.fail("should have timed out"); } catch (TimeoutException xtimeout) { - // ok, ignore + future.cancel(true); + } finally { + lr = executor.shutdownNow(); } - future.cancel(true); Assert.assertTrue(future.isCancelled()); + Assert.assertEquals(42, t); + Assert.assertTrue(lr.isEmpty()); + } + + @Test + public void testInterruptVerboseStrict() throws Exception { + runInterrupt(new JexlBuilder().silent(false).strict(true).create()); + } + + @Test + public void testInterruptVerboseLenient() throws Exception { + runInterrupt(new JexlBuilder().silent(false).strict(false).create()); + } + + @Test + public void testInterruptSilentStrict() throws Exception { + runInterrupt(new JexlBuilder().silent(true).strict(true).create()); + } + + @Test + public void testInterruptSilentLenient() throws Exception { + runInterrupt(new JexlBuilder().silent(true).strict(false).create()); + } + + @Test + public void testInterruptCancellable() throws Exception { + runInterrupt(new JexlBuilder().silent(true).strict(true).cancellable(true).create()); + } + + /** + * Redundant test with previous ones but impervious to JEXL engine configuation. + * @param silent silent engine flag + * @param strict strict (aka not lenient) engine flag + * @throws Exception if there is a regression + */ + private void runInterrupt(JexlEngine jexl) throws Exception { + List lr = null; + ExecutorService exec = Executors.newFixedThreadPool(2); + try { + JexlContext ctxt = new TestContext(); + + // run an interrupt + JexlScript sint = jexl.createScript("interrupt(); return 42"); + Object t = null; + Script.Callable c = (Script.Callable) sint.callable(ctxt); + try { + t = c.call(); + if (c.isCancellable()) { + Assert.fail("should have thrown a Cancel"); + } + } catch (JexlException.Cancel xjexl) { + if (!c.isCancellable()) { + Assert.fail("should not have thrown " + xjexl); + } + } + Assert.assertTrue(c.isCancelled()); + Assert.assertNotEquals(42, t); + + // self interrupt + Future f = null; + c = (Script.Callable) sint.callable(ctxt); + try { + f = exec.submit(c); + t = f.get(); + if (c.isCancellable()) { + Assert.fail("should have thrown a Cancel"); + } + } catch (ExecutionException xexec) { + if (!c.isCancellable()) { + Assert.fail("should not have thrown " + xexec); + } + } + Assert.assertTrue(c.isCancelled()); + Assert.assertNotEquals(42, t); + + // timeout a sleep + JexlScript ssleep = jexl.createScript("sleep(30000); return 42"); + try { + f = exec.submit(ssleep.callable(ctxt)); + t = f.get(100L, TimeUnit.MILLISECONDS); + Assert.fail("should timeout"); + } catch (TimeoutException xtimeout) { + if (f != null) { + f.cancel(true); + } + } + Assert.assertNotEquals(42, t); + + // cancel a sleep + try { + final Future fc = exec.submit(ssleep.callable(ctxt)); + Runnable cancels = new Runnable() { + @Override + public void run() { + try { + Thread.sleep(200L); + } catch (Exception xignore) { + + } + fc.cancel(true); + } + }; + exec.submit(cancels); + t = f.get(100L, TimeUnit.MILLISECONDS); + Assert.fail("should be cancelled"); + } catch (CancellationException xexec) { + // this is the expected result + } + + // timeout a while(true) + JexlScript swhile = jexl.createScript("while(true); return 42"); + try { + f = exec.submit(swhile.callable(ctxt)); + t = f.get(100L, TimeUnit.MILLISECONDS); + Assert.fail("should timeout"); + } catch (TimeoutException xtimeout) { + if (f != null) { + f.cancel(true); + } + } + Assert.assertNotEquals(42, t); + + // cancel a while(true) + try { + final Future fc = exec.submit(swhile.callable(ctxt)); + Runnable cancels = new Runnable() { + @Override + public void run() { + try { + Thread.sleep(200L); + } catch (Exception xignore) { + + } + fc.cancel(true); + } + }; + exec.submit(cancels); + t = fc.get(); + Assert.fail("should be cancelled"); + } catch (CancellationException xexec) { + // this is the expected result + } + Assert.assertNotEquals(42, t); + } finally { + lr = exec.shutdownNow(); + } + Assert.assertTrue(lr.isEmpty()); + } + + @Test + public void testHangs() throws Exception { + JexlScript e = JEXL.createScript("hangs()"); + Callable c = e.callable(new TestContext()); + + ExecutorService executor = Executors.newFixedThreadPool(1); + try { + Future future = executor.submit(c); + Object t = future.get(1, TimeUnit.SECONDS); + Assert.fail("hangs should not be solved"); + } catch(ExecutionException xexec) { + Assert.assertTrue(xexec.getCause() instanceof JexlException.Method); + } finally { + executor.shutdown(); + } } } diff --git a/src/test/java/org/apache/commons/jexl3/ScriptTest.java b/src/test/java/org/apache/commons/jexl3/ScriptTest.java index 1bdf2c56c..0921c168f 100644 --- a/src/test/java/org/apache/commons/jexl3/ScriptTest.java +++ b/src/test/java/org/apache/commons/jexl3/ScriptTest.java @@ -28,6 +28,7 @@ @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) public class ScriptTest extends JexlTestCase { static final String TEST1 = "src/test/scripts/test1.jexl"; + static final String TEST_ADD = "src/test/scripts/testAdd.jexl"; // test class for testScriptUpdatesContext // making this class private static will cause the test to fail. @@ -53,7 +54,8 @@ public ScriptTest() { /** * Test creating a script from a string. */ - @Test public void testSimpleScript() throws Exception { + @Test + public void testSimpleScript() throws Exception { String code = "while (x < 10) x = x + 1;"; JexlScript s = JEXL.createScript(code); JexlContext jc = new MapContext(); @@ -64,7 +66,8 @@ public ScriptTest() { Assert.assertEquals("getText is wrong", code, s.getSourceText()); } - @Test public void testScriptFromFile() throws Exception { + @Test + public void testScriptFromFile() throws Exception { File testScript = new File(TEST1); JexlScript s = JEXL.createScript(testScript); JexlContext jc = new MapContext(); @@ -74,8 +77,20 @@ public ScriptTest() { Assert.assertEquals("Wrong result", new Integer(7), result); } - @Test public void testScriptFromURL() throws Exception { - URL testUrl = new File("src/test/scripts/test1.jexl").toURI().toURL(); + @Test + public void testArgScriptFromFile() throws Exception { + File testScript = new File(TEST_ADD); + JexlScript s = JEXL.createScript(testScript,new String[]{"x","y"}); + JexlContext jc = new MapContext(); + jc.set("out", System.out); + Object result = s.execute(jc, 13, 29); + Assert.assertNotNull("No result", result); + Assert.assertEquals("Wrong result", new Integer(42), result); + } + + @Test + public void testScriptFromURL() throws Exception { + URL testUrl = new File(TEST1).toURI().toURL(); JexlScript s = JEXL.createScript(testUrl); JexlContext jc = new MapContext(); jc.set("out", System.out); @@ -84,7 +99,19 @@ public ScriptTest() { Assert.assertEquals("Wrong result", new Integer(7), result); } - @Test public void testScriptUpdatesContext() throws Exception { + @Test + public void testArgScriptFromURL() throws Exception { + URL testUrl = new File(TEST_ADD).toURI().toURL(); + JexlScript s = JEXL.createScript(testUrl,new String[]{"x","y"}); + JexlContext jc = new MapContext(); + jc.set("out", System.out); + Object result = s.execute(jc, 13, 29); + Assert.assertNotNull("No result", result); + Assert.assertEquals("Wrong result", new Integer(42), result); + } + + @Test + public void testScriptUpdatesContext() throws Exception { String jexlCode = "resultat.setCode('OK')"; JexlExpression e = JEXL.createExpression(jexlCode); JexlScript s = JEXL.createScript(jexlCode); diff --git a/src/test/java/org/apache/commons/jexl3/SetLiteralTest.java b/src/test/java/org/apache/commons/jexl3/SetLiteralTest.java index 8617789ec..218077b4b 100644 --- a/src/test/java/org/apache/commons/jexl3/SetLiteralTest.java +++ b/src/test/java/org/apache/commons/jexl3/SetLiteralTest.java @@ -17,6 +17,7 @@ package org.apache.commons.jexl3; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.junit.Assert; @@ -87,6 +88,16 @@ public void testSetLiteralWithOneEntryBlock() throws Exception { Assert.assertTrue(check.equals(o)); } + @Test + public void testSetLiteralWithOneNestedSet() throws Exception { + JexlScript e = JEXL.createScript("{ { 'foo' } }"); + JexlContext jc = new MapContext(); + + Object o = e.execute(jc); + Set check = createSet(createSet("foo")); + Assert.assertTrue(check.equals(o)); + } + @Test public void testSetLiteralWithNumbers() throws Exception { JexlExpression e = JEXL.createExpression("{ 5.0 , 10 }"); @@ -100,6 +111,7 @@ public void testSetLiteralWithNumbers() throws Exception { @Test public void testSetLiteralWithNulls() throws Exception { String[] exprs = { + "{ }", "{ 10 }", "{ 10 , null }", "{ 10 , null , 20}", @@ -107,6 +119,7 @@ public void testSetLiteralWithNulls() throws Exception { "{ null, '10' , 20 }" }; Set[] checks = { + Collections.emptySet(), createSet(new Integer(10)), createSet(new Integer(10), null), createSet(new Integer(10), null, new Integer(20)), diff --git a/src/test/java/org/apache/commons/jexl3/SideEffectTest.java b/src/test/java/org/apache/commons/jexl3/SideEffectTest.java index f90f69f0c..55f5ac0eb 100644 --- a/src/test/java/org/apache/commons/jexl3/SideEffectTest.java +++ b/src/test/java/org/apache/commons/jexl3/SideEffectTest.java @@ -16,9 +16,17 @@ */ package org.apache.commons.jexl3; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Map; +import java.util.logging.Level; import org.apache.commons.jexl3.junit.Asserter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -83,6 +91,45 @@ public void testSideEffectVar() throws Exception { Assert.assertEquals(context.get("foo"), (long)(i41 ^ 2)); } + @Test + public void testSideEffectVarDots() throws Exception { + Map context = asserter.getVariables(); + Integer i41 = Integer.valueOf(4141); + Object foo = i41; + + context.put("foo.bar.quux", foo); + asserter.assertExpression("foo.bar.quux += 2", i41 + 2); + Assert.assertEquals(context.get("foo.bar.quux"), i41 + 2); + + context.put("foo.bar.quux", foo); + asserter.assertExpression("foo.bar.quux -= 2", i41 - 2); + Assert.assertEquals(context.get("foo.bar.quux"), i41 - 2); + + context.put("foo.bar.quux", foo); + asserter.assertExpression("foo.bar.quux *= 2", i41 * 2); + Assert.assertEquals(context.get("foo.bar.quux"), i41 * 2); + + context.put("foo.bar.quux", foo); + asserter.assertExpression("foo.bar.quux /= 2", i41 / 2); + Assert.assertEquals(context.get("foo.bar.quux"), i41 / 2); + + context.put("foo.bar.quux", foo); + asserter.assertExpression("foo.bar.quux %= 2", i41 % 2); + Assert.assertEquals(context.get("foo.bar.quux"), i41 % 2); + + context.put("foo.bar.quux", foo); + asserter.assertExpression("foo.bar.quux &= 3", (long) (i41 & 3)); + Assert.assertEquals(context.get("foo.bar.quux"), (long)(i41 & 3)); + + context.put("foo.bar.quux", foo); + asserter.assertExpression("foo.bar.quux |= 2", (long)(i41 | 2)); + Assert.assertEquals(context.get("foo.bar.quux"), (long)(i41 | 2)); + + context.put("foo.bar.quux", foo); + asserter.assertExpression("foo.bar.quux ^= 2", (long)(i41 ^ 2)); + Assert.assertEquals(context.get("foo.bar.quux"), (long)(i41 ^ 2)); + } + @Test public void testSideEffectArray() throws Exception { Integer i41 = Integer.valueOf(4141); @@ -120,6 +167,43 @@ public void testSideEffectArray() throws Exception { Assert.assertEquals(foo[0], (long)(i41 ^ 2)); } + @Test + public void testSideEffectDotArray() throws Exception { + Integer i41 = Integer.valueOf(4141); + Integer i42 = Integer.valueOf(42); + Integer i43 = Integer.valueOf(43); + String s42 = "fourty-two"; + String s43 = "fourty-three"; + Object[] foo = new Object[3]; + foo[1] = i42; + foo[2] = i43; + asserter.setVariable("foo", foo); + foo[0] = i41; + asserter.assertExpression("foo.0 += 2", i41 + 2); + Assert.assertEquals(foo[0], i41 + 2); + foo[0] = i41; + asserter.assertExpression("foo.0 -= 2", i41 - 2); + Assert.assertEquals(foo[0], i41 - 2); + foo[0] = i41; + asserter.assertExpression("foo.0 *= 2", i41 * 2); + Assert.assertEquals(foo[0], i41 * 2); + foo[0] = i41; + asserter.assertExpression("foo.0 /= 2", i41 / 2); + Assert.assertEquals(foo[0], i41 / 2); + foo[0] = i41; + asserter.assertExpression("foo.0 %= 2", i41 % 2); + Assert.assertEquals(foo[0], i41 % 2); + foo[0] = i41; + asserter.assertExpression("foo.0 &= 3", (long) (i41 & 3)); + Assert.assertEquals(foo[0], (long)(i41 & 3)); + foo[0] = i41; + asserter.assertExpression("foo.0 |= 2", (long)(i41 | 2)); + Assert.assertEquals(foo[0], (long)(i41 | 2)); + foo[0] = i41; + asserter.assertExpression("foo.0 ^= 2", (long)(i41 ^ 2)); + Assert.assertEquals(foo[0], (long)(i41 ^ 2)); + } + @Test public void testSideEffectAntishArray() throws Exception { Integer i41 = Integer.valueOf(4141); @@ -440,4 +524,147 @@ public JexlOperator selfXor(Var lhs, Var rhs) { return JexlOperator.ASSIGN; } } + + /** + * An arithmetic that implements 2 selfAdd methods. + */ + public static class Arithmetic246 extends JexlArithmetic { + public Arithmetic246(boolean astrict) { + super(astrict); + } + + public JexlOperator selfAdd(Collection c, String item) throws IOException { + c.add(item); + return JexlOperator.ASSIGN; + } + + public JexlOperator selfAdd(Appendable c, String item) throws IOException { + c.append(item); + return JexlOperator.ASSIGN; + } + + @Override + public Object add(Object right, Object left) { + return super.add(left, right); + } + } + + public static class Arithmetic246b extends Arithmetic246 { + public Arithmetic246b(boolean astrict) { + super(astrict); + } + + public Object selfAdd(Object c, String item) throws IOException { + if (c == null) { + return new ArrayList(Arrays.asList(item)); + } + if (c instanceof Appendable) { + ((Appendable) c).append(item); + return JexlOperator.ASSIGN; + } + return JexlEngine.TRY_FAILED; + } + } + + @Test + public void test246() throws Exception { + run246(new Arithmetic246(true)); + } + + @Test + public void test246b() throws Exception { + run246(new Arithmetic246b(true)); + } + + private void run246(JexlArithmetic j246) throws Exception { + Log log246 = LogFactory.getLog(SideEffectTest.class); + // quiesce the logger + java.util.logging.Logger ll246 = java.util.logging.LogManager.getLogManager().getLogger(SideEffectTest.class.getName()); + // ll246.setLevel(Level.WARNING); + JexlEngine jexl = new JexlBuilder().arithmetic(j246).cache(32).debug(true).logger(log246).create(); + JexlScript script = jexl.createScript("z += x", "x"); + MapContext ctx = new MapContext(); + List z = new ArrayList(1); + Object zz = null; + + // no ambiguous, std case + ctx.set("z", z); + zz = script.execute(ctx, "42"); + Assert.assertTrue(zz == z); + Assert.assertEquals(1, z.size()); + z.clear(); + ctx.clear(); + + boolean t246 = false; + // call with null + try { + script.execute(ctx, "42"); + zz = ctx.get("z"); + Assert.assertTrue(zz instanceof List); + z = (List) zz; + Assert.assertEquals(1, z.size()); + } catch(JexlException xjexl) { + t246 = true; + Assert.assertTrue(j246.getClass().equals(Arithmetic246.class)); + } catch(ArithmeticException xjexl) { + t246 = true; + Assert.assertTrue(j246.getClass().equals(Arithmetic246.class)); + } + ctx.clear(); + + // a non ambiguous call still succeeds + ctx.set("z", z); + zz = script.execute(ctx, "-42"); + Assert.assertTrue(zz == z); + Assert.assertEquals(t246? 1 : 2, z.size()); + } + + // an arithmetic that performs side effects + public static class Arithmetic248 extends JexlArithmetic { + public Arithmetic248(boolean strict) { + super(strict); + } + + public Object arrayGet(List list, Collection range) { + List rl = new ArrayList(range.size()); + for(int i : range) { + rl.add(list.get(i)); + } + return rl; + } + + public Object arraySet(List list, Collection range, Object value) { + for(int i : range) { + list.set(i, value); + } + return list; + } + } + + @Test + public void test248() throws Exception { + MapContext ctx = new MapContext(); + List foo = new ArrayList(); + foo.addAll(Arrays.asList(10, 20, 30, 40)); + ctx.set("foo", foo); + + JexlEngine engine = new JexlBuilder().arithmetic(new Arithmetic248(true)).create(); + JexlScript foo12 = engine.createScript("foo[1..2]"); + try { + Object r = foo12.execute(ctx); + Assert.assertEquals(Arrays.asList(20, 30), r); + } catch (JexlException xp) { + Assert.assertTrue(xp instanceof JexlException.Property); + } + + JexlScript foo12assign = engine.createScript("foo[1..2] = x", "x"); + try { + Object r = foo12assign.execute(ctx, 25); + Assert.assertEquals(25, r); + Assert.assertEquals(Arrays.asList(10, 25, 25, 40), foo); + } catch (JexlException xp) { + Assert.assertTrue(xp instanceof JexlException.Property); + } + } + } diff --git a/src/test/java/org/apache/commons/jexl3/SynchronizedArithmetic.java b/src/test/java/org/apache/commons/jexl3/SynchronizedArithmetic.java new file mode 100644 index 000000000..9938b2bdd --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/SynchronizedArithmetic.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.lang.reflect.Field; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicInteger; +import sun.misc.Unsafe; + +/** + * + * An example arithmetic that uses object intrinsic monitors to synchronize get/set/iteration on Maps. + */ +public class SynchronizedArithmetic extends JexlArithmetic { + /** + * Monitor/synchronized protected access to gets/set/iterator on maps. + */ + private final Monitor monitor; + + /** + * A base synchronized arithmetic. + * @param monitor the synchronization monitor + * @param strict whether the arithmetic is strict or not + */ + protected SynchronizedArithmetic(Monitor monitor, boolean strict) { + super(strict); + this.monitor = monitor; + } + + + /** + * An indirect way to determine we are actually calling what is needed. + *

          + * This class counts how many times we called enter & exit; they should be balanced + */ + public static abstract class Monitor{ + /* Counts the number of times enter is called. */ + private final AtomicInteger enters = new AtomicInteger(0); + /* Counts the number of times exit is called. */ + private final AtomicInteger exits = new AtomicInteger(0); + + /** + * Enter an object monitor. + * @param o the monitored object + */ + protected void monitorEnter(Object o) { + UNSAFE.monitorEnter(o); + enters.incrementAndGet(); + } + + /** + * Exits an object monitor. + * @param o the monitored object + */ + protected void monitorExit(Object o) { + UNSAFE.monitorExit(o); + exits.incrementAndGet(); + } + + /** + * Whether the number of monitor enter is equals to the number of exits. + * @return true if balanced, false otherwise + */ + public boolean isBalanced() { + return enters.get() == exits.get(); + } + + /** + * The number of enter calls. + * @return how many enter were executed + */ + public int getCount() { + return enters.get(); + } + + } + + /** + * You should know better than to use this... + */ + private static Unsafe UNSAFE; + static { + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + UNSAFE = (Unsafe) f.get(null); + } catch (Exception e) { + UNSAFE = null; + } + } + + /** + * Using the unsafe to enter & exit object intrinsic monitors. + */ + static class UnsafeMonitor extends Monitor { + @Override protected void monitorEnter(Object o) { + UNSAFE.monitorEnter(o); + super.monitorEnter(o); + } + + @Override protected void monitorExit(Object o) { + UNSAFE.monitorExit(o); + super.monitorExit(o); + } + } + + /** + * An iterator that implements Closeable (at least implements a close method) + * and uses monitors to protect iteration. + */ + public class SynchronizedIterator implements /*Closeable,*/ Iterator { + private final Object monitored; + private Iterator iterator; + + SynchronizedIterator(Object locked, Iterator ii) { + monitored = locked; + monitor.monitorEnter(monitored); + try { + iterator = ii; + } finally { + if (iterator == null) { + monitor.monitorExit(monitored); + } + } + } + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + + //@Override + public void close() { + if (iterator != null) { + monitor.monitorExit(monitored); + iterator = null; + } + } + + @Override + public boolean hasNext() { + if (iterator == null) { + return false; + } + boolean n = iterator.hasNext(); + if (!n) { + close(); + } + return n; + } + + @Override + public Object next() { + if (iterator == null) { + throw new NoSuchElementException(); + } + return iterator.next(); + } + + @Override + public void remove() { + if (iterator != null) { + iterator.remove(); + } + } + } + + /** + * Jexl pseudo-overload for array-like access get operator (map[key]) for maps. + *

          synchronized(map) { return map.get(key); } + * @param map the map + * @param key the key + * @return the value associated to the key in the map + */ + public Object arrayGet(Map map, Object key) { + monitor.monitorEnter(map); + try { + return map.get(key); + } finally { + monitor.monitorExit(map); + } + } + /** + * Jexl pseudo-overload for array-like access set operator (map[key] = value) for maps. + *

          synchronized(map) { map.put(key, value); } + * @param map the map + * @param key the key + * @param value the value + */ + public void arraySet(Map map, Object key, Object value) { + monitor.monitorEnter(map); + try { + map.put(key, value); + } finally { + monitor.monitorExit(map); + } + } + /** + * Jexl pseudo-overload for property access get operator (map.key) for maps. + *

          synchronized(map) { return map.get(key); } + * + * @param map the map + * @param key the key + * @return the value associated to the key in the map + */ + public Object propertyGet(Map map, Object key) { + monitor.monitorEnter(map); + try { + return map.get(key); + } finally { + monitor.monitorExit(map); + } + } + + /** + * Jexl pseudo-overload for array-like access set operator (map.key = value) for maps. + *

          synchronized(map) { map.put(key, value); } + * @param map the map + * @param key the key + * @param value the value + */ + public void propertySet(Map map, Object key, Object value) { + monitor.monitorEnter(map); + try { + map.put(key, value); + } finally { + monitor.monitorExit(map); + } + } + + /** + * Creates an iterator on the map values that executes under the map monitor. + * @param map the map + * @return the iterator + */ + public Iterator forEach(Map map) { + return new SynchronizedIterator(map, map.values().iterator()); + } +} diff --git a/src/test/java/org/apache/commons/jexl3/SynchronizedContext.java b/src/test/java/org/apache/commons/jexl3/SynchronizedContext.java new file mode 100644 index 000000000..b9f9fed11 --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/SynchronizedContext.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.util.concurrent.Callable; + +/** + * Exposes a synchronized call to a script and synchronizes access to get/set methods. + */ +public class SynchronizedContext extends MapContext implements JexlContext.AnnotationProcessor { + private final JexlContext context; + + public SynchronizedContext(JexlContext ctxt) { + this.context = ctxt; + } + + /** + * Calls a script synchronized by an object monitor. + * @param var the object used for sync + * @param script the script + * @return the script value + */ + public Object call(Object var, JexlScript script) { + String[] parms = script.getParameters(); + boolean varisarg = parms != null && parms.length == 1; + if (var == null) { + return varisarg ? script.execute(context, var) : script.execute(context); + } else { + synchronized (var) { + return varisarg ? script.execute(context, var) : script.execute(context); + } + } + } + + @Override + public Object get(String name) { + synchronized (this) { + return super.get(name); + } + } + + @Override + public void set(String name, Object value) { + synchronized (this) { + super.set(name, value); + } + } + + @Override + public Object processAnnotation(String name, Object[] args, Callable statement) throws Exception { + if ("synchronized".equals(name)) { + final Object arg = args[0]; + synchronized(arg) { + return statement.call(); + } + } + throw new IllegalArgumentException("unknown annotation " + name); + } + +} diff --git a/src/test/java/org/apache/commons/jexl3/SynchronizedOverloadsTest.java b/src/test/java/org/apache/commons/jexl3/SynchronizedOverloadsTest.java new file mode 100644 index 000000000..cf98bce3d --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/SynchronizedOverloadsTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.util.Map; +import java.util.TreeMap; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + + +/** + * Test cases for synchronized calls. + *

          May be a base for synchronized calls. + */ +@SuppressWarnings({"boxing", "UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) +public class SynchronizedOverloadsTest extends JexlTestCase { + public SynchronizedOverloadsTest() { + super("SynchronizedOverloadsTest", null); + } + + @Before + @Override + public void setUp() throws Exception { + // ensure jul logging is only error to avoid warning in silent mode + java.util.logging.Logger.getLogger(JexlEngine.class.getName()).setLevel(java.util.logging.Level.SEVERE); + } + + + @Test + public void testSynchronizer() throws Exception { + Map ns = new TreeMap(); + ns.put("synchronized", SynchronizedContext.class); + JexlContext jc = new MapContext(); + JexlEngine jexl = new JexlBuilder().namespaces(ns).create(); + JexlScript js0 = jexl.createScript("synchronized:call(x, (y)->{y.size()})", "x"); + Object size = js0.execute(jc, "foobar"); + Assert.assertEquals(6, size); + } + + @Test + public void testSynchronized() throws Exception { + Map ns = new TreeMap(); + JexlContext jc = new SynchronizedContext(new MapContext()); + JexlEngine jexl = new JexlBuilder().namespaces(ns).create(); + JexlScript js0 = jexl.createScript("@synchronized(y) {return y.size(); }", "y"); + Object size = js0.execute(jc, "foobar"); + Assert.assertEquals(6, size); + } + + @Test + public void testUnsafeMonitor() throws Exception { + SynchronizedArithmetic.Monitor monitor = new SynchronizedArithmetic.UnsafeMonitor(); + Map foo = new TreeMap(); + foo.put("one", 1); + foo.put("two", 2); + foo.put("three", 3); + JexlContext jc = new SynchronizedContext(new MapContext()); + JexlEngine jexl = new JexlBuilder().arithmetic(new SynchronizedArithmetic(monitor, true)).create(); + JexlScript js0 = jexl.createScript("x['four'] = 4; var t = 0.0; for(var z: x) { t += z; }; call(t, (y)->{return y});", "x"); + Object t = js0.execute(jc, foo); + Assert.assertEquals(10.0d, t); + Assert.assertTrue(monitor.isBalanced()); + Assert.assertEquals(2, monitor.getCount()); + t = js0.execute(jc, foo); + Assert.assertEquals(10.0d, t); + Assert.assertTrue(monitor.isBalanced()); + Assert.assertEquals(4, monitor.getCount()); + } +} diff --git a/src/test/java/org/apache/commons/jexl3/examples/ArrayTest.java b/src/test/java/org/apache/commons/jexl3/examples/ArrayTest.java index 620d656ac..7cac5abd2 100644 --- a/src/test/java/org/apache/commons/jexl3/examples/ArrayTest.java +++ b/src/test/java/org/apache/commons/jexl3/examples/ArrayTest.java @@ -67,7 +67,8 @@ static void example(Output out) throws Exception { * Unit test entry point. * @throws Exception */ - @Test public void testExample() throws Exception { + @Test + public void testExample() throws Exception { example(Output.JUNIT); } diff --git a/src/test/java/org/apache/commons/jexl3/examples/MethodPropertyTest.java b/src/test/java/org/apache/commons/jexl3/examples/MethodPropertyTest.java index 7f914472f..247842fa0 100644 --- a/src/test/java/org/apache/commons/jexl3/examples/MethodPropertyTest.java +++ b/src/test/java/org/apache/commons/jexl3/examples/MethodPropertyTest.java @@ -17,19 +17,19 @@ package org.apache.commons.jexl3.examples; -import junit.framework.TestCase; import org.apache.commons.jexl3.JexlBuilder; import org.apache.commons.jexl3.JexlExpression; import org.apache.commons.jexl3.JexlContext; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.MapContext; +import org.junit.Test; /** * Simple example to show how to access method and properties. * * @since 1.0 */ -public class MethodPropertyTest extends TestCase { +public class MethodPropertyTest { /** * An example for method access. */ @@ -120,6 +120,7 @@ public String convert(long i) { * Unit test entry point. * @throws Exception */ + @Test public void testExample() throws Exception { example(Output.JUNIT); } diff --git a/src/test/java/org/apache/commons/jexl3/examples/Output.java b/src/test/java/org/apache/commons/jexl3/examples/Output.java index ea67e9c45..826ee7bf4 100644 --- a/src/test/java/org/apache/commons/jexl3/examples/Output.java +++ b/src/test/java/org/apache/commons/jexl3/examples/Output.java @@ -16,7 +16,8 @@ */ package org.apache.commons.jexl3.examples; -import junit.framework.TestCase; + +import org.junit.Assert; /** * Abstracts using a test within Junit or through a main method. @@ -43,7 +44,7 @@ private Output() { public static final Output JUNIT = new Output() { @Override public void print(String expr, Object actual, Object expected) { - TestCase.assertEquals(expr, expected, actual); + Assert.assertEquals(expr, expected, actual); } }; diff --git a/src/test/java/org/apache/commons/jexl3/internal/Util.java b/src/test/java/org/apache/commons/jexl3/internal/Util.java index 9d3f65136..378567b50 100644 --- a/src/test/java/org/apache/commons/jexl3/internal/Util.java +++ b/src/test/java/org/apache/commons/jexl3/internal/Util.java @@ -21,12 +21,13 @@ import java.util.Map; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JexlScript; import org.apache.commons.jexl3.parser.ASTJexlScript; import org.apache.commons.jexl3.parser.JexlNode; /** - * Helper methods for debug sessions. + * Helper methods for validate sessions. */ public class Util { /** @@ -44,17 +45,18 @@ public static void debuggerCheck(JexlEngine ijexl) throws Exception { Engine jdbg = new Engine(); jdbg.parser.allowRegisters(true); Debugger dbg = new Debugger(); - // iterate over all expression in cache - Iterator> inodes = jexl.cache.entrySet().iterator(); + // iterate over all expression in + Iterator> inodes = jexl.cache.entries().iterator(); while (inodes.hasNext()) { - Map.Entry entry = inodes.next(); + Map.Entry entry = inodes.next(); JexlNode node = entry.getValue(); // recreate expr string from AST dbg.debug(node); String expressiondbg = dbg.toString(); + JexlFeatures features = entry.getKey().getFeatures(); // recreate expr from string try { - Script exprdbg = jdbg.createScript(null, expressiondbg, null); + Script exprdbg = jdbg.createScript(features, null, expressiondbg, null); // make arg cause become the root cause JexlNode root = exprdbg.script; while (root.jjtGetParent() != null) { @@ -137,7 +139,7 @@ private static String checkEquals(JexlNode lhs, JexlNode rhs) { } /** - * A helper class to help debug AST problems. + * A helper class to help validate AST problems. * @param e the script * @return an indented version of the AST */ diff --git a/src/test/java/org/apache/commons/jexl3/internal/introspection/MethodKeyTest.java b/src/test/java/org/apache/commons/jexl3/internal/introspection/MethodKeyTest.java index e75dccfbc..5c02df54f 100644 --- a/src/test/java/org/apache/commons/jexl3/internal/introspection/MethodKeyTest.java +++ b/src/test/java/org/apache/commons/jexl3/internal/introspection/MethodKeyTest.java @@ -16,13 +16,13 @@ */ package org.apache.commons.jexl3.internal.introspection; -import junit.framework.TestCase; +import org.junit.Assert; import org.junit.Test; /** * Checks the CacheMap.MethodKey implementation */ -public class MethodKeyTest extends TestCase { +public class MethodKeyTest { // A set of classes (most of them primitives) private static final Class[] PRIMS = { Boolean.TYPE, @@ -75,25 +75,25 @@ public class MethodKeyTest extends TestCase { "invokeIt" }; /** from key to string */ - private static final java.util.Map< MethodKey, String> byKey; + private static final java.util.Map< MethodKey, String> BY_KEY; /** form string to key */ - private static final java.util.Map byString; + private static final java.util.Map BY_STRING; /** the list of keys we generated & test against */ - private static final MethodKey[] keyList; + private static final MethodKey[] KEY_LIST; - /** Creates & inserts a key into the byKey & byString map */ + /** * Creates & inserts a key into the BY_KEY & byString map */ private static void setUpKey(String name, Class[] parms) { MethodKey key = new MethodKey(name, parms); String str = key.toString(); - byKey.put(key, str); - byString.put(str, key); + BY_KEY.put(key, str); + BY_STRING.put(str, key); } /** Generate a list of method*(prims*), method(prims*, prims*), method*(prims*,prims*,prims*) */ static { - byKey = new java.util.HashMap< MethodKey, String>(); - byString = new java.util.HashMap(); + BY_KEY = new java.util.HashMap< MethodKey, String>(); + BY_STRING = new java.util.HashMap(); for (int m = 0; m < METHODS.length; ++m) { String method = METHODS[m]; for (int p0 = 0; p0 < PRIMS.length; ++p0) { @@ -109,7 +109,7 @@ private static void setUpKey(String name, Class[] parms) { } } } - keyList = byKey.keySet().toArray(new MethodKey[byKey.size()]); + KEY_LIST = BY_KEY.keySet().toArray(new MethodKey[BY_KEY.size()]); } /** Builds a string key */ @@ -124,8 +124,8 @@ String makeStringKey(String method, Class... params) { /** Checks that a string key does exist */ void checkStringKey(String method, Class... params) { String key = makeStringKey(method, params); - MethodKey out = byString.get(key); - assertTrue(out != null); + MethodKey out = BY_STRING.get(key); + Assert.assertTrue(out != null); } /** Builds a method key */ @@ -136,30 +136,37 @@ MethodKey makeKey(String method, Class... params) { /** Checks that a method key exists */ void checkKey(String method, Class... params) { MethodKey key = makeKey(method, params); - String out = byKey.get(key); - assertTrue(out != null); + String out = BY_KEY.get(key); + Assert.assertTrue(out != null); + } + + @Test + public void testDebugString() throws Exception { + MethodKey c = KEY_LIST[0]; + String str = c.debugString(); + Assert.assertNotNull(str); } @Test public void testObjectKey() throws Exception { - for (int k = 0; k < keyList.length; ++k) { - MethodKey ctl = keyList[k]; + for (int k = 0; k < KEY_LIST.length; ++k) { + MethodKey ctl = KEY_LIST[k]; MethodKey key = makeKey(ctl.getMethod(), ctl.getParameters()); - String out = byKey.get(key); - assertTrue(out != null); - assertTrue(ctl.toString() + " != " + out, ctl.toString().equals(out)); + String out = BY_KEY.get(key); + Assert.assertTrue(out != null); + Assert.assertTrue(ctl.toString() + " != " + out, ctl.toString().equals(out)); } } @Test public void testStringKey() throws Exception { - for (int k = 0; k < keyList.length; ++k) { - MethodKey ctl = keyList[k]; + for (int k = 0; k < KEY_LIST.length; ++k) { + MethodKey ctl = KEY_LIST[k]; String key = makeStringKey(ctl.getMethod(), ctl.getParameters()); - MethodKey out = byString.get(key); - assertTrue(out != null); - assertTrue(ctl.toString() + " != " + key, ctl.equals(out)); + MethodKey out = BY_STRING.get(key); + Assert.assertTrue(out != null); + Assert.assertTrue(ctl.toString() + " != " + key, ctl.equals(out)); } } @@ -168,11 +175,11 @@ public void testStringKey() throws Exception { @Test public void testPerfKey() throws Exception { for (int l = 0; l < LOOP; ++l) { - for (int k = 0; k < keyList.length; ++k) { - MethodKey ctl = keyList[k]; + for (int k = 0; k < KEY_LIST.length; ++k) { + MethodKey ctl = KEY_LIST[k]; MethodKey key = makeKey(ctl.getMethod(), ctl.getParameters()); - String out = byKey.get(key); - assertTrue(out != null); + String out = BY_KEY.get(key); + Assert.assertTrue(out != null); } } } @@ -180,11 +187,11 @@ public void testPerfKey() throws Exception { @Test public void testPerfString() throws Exception { for (int l = 0; l < LOOP; ++l) { - for (int k = 0; k < keyList.length; ++k) { - MethodKey ctl = keyList[k]; + for (int k = 0; k < KEY_LIST.length; ++k) { + MethodKey ctl = KEY_LIST[k]; String key = makeStringKey(ctl.getMethod(), ctl.getParameters()); - MethodKey out = byString.get(key); - assertTrue(out != null); + MethodKey out = BY_STRING.get(key); + Assert.assertTrue(out != null); } } } diff --git a/src/test/java/org/apache/commons/jexl3/introspection/SandboxTest.java b/src/test/java/org/apache/commons/jexl3/introspection/SandboxTest.java index b243b90b4..82eeae3cf 100644 --- a/src/test/java/org/apache/commons/jexl3/introspection/SandboxTest.java +++ b/src/test/java/org/apache/commons/jexl3/introspection/SandboxTest.java @@ -40,7 +40,13 @@ public class SandboxTest extends JexlTestCase { public SandboxTest() { super("SandboxTest"); - JEXL.setClassLoader(getClass().getClassLoader()); + } + + + public static class CantSeeMe { + public boolean doIt() { + return false; + } } @NoJexl @@ -88,6 +94,10 @@ public void setName(String name) { public String Quux() { return name + "-quux"; } + + public int doIt() { + return 42; + } @NoJexl public String cantCallMe() { @@ -195,6 +205,30 @@ public void testSetBlack() throws Exception { LOGGER.info(xvar.toString()); } } + + @Test + public void testCantSeeMe() throws Exception { + JexlContext jc = new MapContext(); + String expr = "foo.doIt()"; + JexlScript script; + Object result = null; + + JexlSandbox sandbox = new JexlSandbox(false); + sandbox.white(Foo.class.getName()); + JexlEngine sjexl = new JexlBuilder().sandbox(sandbox).strict(true).create(); + + jc.set("foo", new CantSeeMe()); + script = sjexl.createScript(expr); + try { + result = script.execute(jc); + Assert.fail("should have failed, doIt()"); + } catch (JexlException xany) { + // + } + jc.set("foo", new Foo("42")); + result = script.execute(jc); + Assert.assertEquals(42, ((Integer) result).intValue()); + } @Test public void testCtorWhite() throws Exception { diff --git a/src/test/java/org/apache/commons/jexl3/junit/Asserter.java b/src/test/java/org/apache/commons/jexl3/junit/Asserter.java index eb97ac60d..93f2c7069 100644 --- a/src/test/java/org/apache/commons/jexl3/junit/Asserter.java +++ b/src/test/java/org/apache/commons/jexl3/junit/Asserter.java @@ -16,11 +16,11 @@ */ package org.apache.commons.jexl3.junit; +import java.lang.reflect.Array; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; -import junit.framework.Assert; import org.apache.commons.jexl3.JexlEvalContext; import org.apache.commons.jexl3.JexlArithmetic; @@ -28,6 +28,8 @@ import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlException; import org.apache.commons.jexl3.JexlScript; +import org.junit.Assert; +import static org.junit.Assert.fail; /** * A utility class for performing JUnit based assertions using Jexl @@ -99,16 +101,26 @@ public void assertExpression(String expression, Object expected) throws Exceptio Object value = exp.execute(context); if (expected instanceof BigDecimal) { JexlArithmetic jexla = engine.getArithmetic(); - assertTrue("expression: " + expression, ((BigDecimal) expected).compareTo(jexla.toBigDecimal(value)) == 0); + Assert.assertTrue("expression: " + expression, ((BigDecimal) expected).compareTo(jexla.toBigDecimal(value)) == 0); } if (expected != null && value != null) { - assertEquals("expression: " + expression + ", " - + expected.getClass().getSimpleName() - + " ?= " - + value.getClass().getSimpleName(), - expected, value); + if (expected.getClass().isArray() && value.getClass().isArray()) { + int esz = Array.getLength(expected); + int vsz = Array.getLength(value); + String report = "expression: " + expression; + Assert.assertEquals(report + ", array size", esz, vsz); + for (int i = 0; i < vsz; ++i) { + Assert.assertEquals(report + ", value@[]" + i, Array.get(expected, i), Array.get(value, i)); + } + } else { + Assert.assertEquals("expression: " + expression + ", " + + expected.getClass().getSimpleName() + + " ?= " + + value.getClass().getSimpleName(), + expected, value); + } } else { - assertEquals("expression: " + expression, expected, value); + Assert.assertEquals("expression: " + expression, expected, value); } } diff --git a/src/test/java/org/apache/commons/jexl3/junit/AsserterTest.java b/src/test/java/org/apache/commons/jexl3/junit/AsserterTest.java index 9483047da..480152116 100644 --- a/src/test/java/org/apache/commons/jexl3/junit/AsserterTest.java +++ b/src/test/java/org/apache/commons/jexl3/junit/AsserterTest.java @@ -17,8 +17,6 @@ /* $Id$ */ package org.apache.commons.jexl3.junit; -import junit.framework.AssertionFailedError; - import org.apache.commons.jexl3.Foo; import org.apache.commons.jexl3.JexlTestCase; import org.junit.Assert; @@ -39,12 +37,12 @@ public AsserterTest() { public void testThis() throws Exception { Asserter asserter = new Asserter(JEXL); asserter.setVariable("this", new Foo()); - asserter.assertExpression("this.get('abc')", "Repeat : abc"); + asserter.assertExpression("this.repeat('abc')", "Repeat : abc"); try { asserter.assertExpression("this.count", "Wrong Value"); Assert.fail("This method should have thrown an assertion exception"); } - catch (AssertionFailedError e) { + catch (AssertionError e) { // it worked! } } @@ -66,7 +64,7 @@ public void testVariable() throws Exception { asserter.assertExpression("bar.count", new Integer(5)); Assert.fail("This method should have thrown an assertion exception"); } - catch (AssertionFailedError e) { + catch (AssertionError e) { // it worked! } } diff --git a/src/test/java/org/apache/commons/jexl3/parser/ParserTest.java b/src/test/java/org/apache/commons/jexl3/parser/ParserTest.java index c128acb0e..9bc5fce24 100644 --- a/src/test/java/org/apache/commons/jexl3/parser/ParserTest.java +++ b/src/test/java/org/apache/commons/jexl3/parser/ParserTest.java @@ -18,52 +18,62 @@ import java.io.StringReader; -import junit.framework.TestCase; import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlFeatures; +import org.junit.Assert; +import org.junit.Test; /** * @since 1.0 * */ -public class ParserTest extends TestCase { - public ParserTest(String testName) { - super(testName); - } +public class ParserTest { + static final JexlFeatures features = new JexlFeatures(); + public ParserTest() {} /** * See if we can parse simple scripts */ + @Test public void testParse() throws Exception { Parser parser = new Parser(new StringReader(";")); - JexlNode sn; - sn = parser.parse(null, "foo = 1;", null, false, false); - assertNotNull("parsed node is null", sn); + sn = parser.parse(null, features, "foo = 1;", null); + Assert.assertNotNull("parsed node is null", sn); - sn = parser.parse(null, "foo = \"bar\";", null, false, false); - assertNotNull("parsed node is null", sn); + sn = parser.parse(null, features, "foo = \"bar\";", null); + Assert.assertNotNull("parsed node is null", sn); - sn = parser.parse(null, "foo = 'bar';", null, false, false); - assertNotNull("parsed node is null", sn); + sn = parser.parse(null, features, "foo = 'bar';", null); + Assert.assertNotNull("parsed node is null", sn); } + @Test public void testErrorAssign() throws Exception { - Parser parser = new Parser(new StringReader(";")); - try { - JexlNode sn = parser.parse(null, "foo() = 1;", null, false, false); - fail("should have failed on invalid assignment"); - } catch (JexlException.Parsing xparse) { - // ok + String[] ops = { "=", "+=", "-=", "/=", "*=", "^=", "&=", "|=" }; + for(String op : ops) { + Parser parser = new Parser(new StringReader(";")); + try { + JexlNode sn = parser.parse(null, features, "foo() "+op+" 1;", null); + Assert.fail("should have failed on invalid assignment " + op); + } catch (JexlException.Parsing xparse) { + // ok + String ss = xparse.getDetail(); + String sss = xparse.toString(); + } } } + @Test public void testErrorAmbiguous() throws Exception { Parser parser = new Parser(new StringReader(";")); try { - JexlNode sn = parser.parse(null, "x = 1 y = 5", null, false, false); - fail("should have failed on ambiguous statement"); + JexlNode sn = parser.parse(null, features, "x = 1 y = 5", null); + Assert.fail("should have failed on ambiguous statement"); } catch (JexlException.Ambiguous xambiguous) { // ok + } catch(JexlException xother) { + Assert.fail(xother.toString()); } } } diff --git a/src/test/java/org/apache/commons/jexl3/scripting/JexlScriptEngineOptionalTest.java b/src/test/java/org/apache/commons/jexl3/scripting/JexlScriptEngineOptionalTest.java index c2c89c5a3..ab739dc97 100644 --- a/src/test/java/org/apache/commons/jexl3/scripting/JexlScriptEngineOptionalTest.java +++ b/src/test/java/org/apache/commons/jexl3/scripting/JexlScriptEngineOptionalTest.java @@ -24,37 +24,41 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; -import junit.framework.TestCase; +import org.junit.Assert; +import org.junit.Test; -public class JexlScriptEngineOptionalTest extends TestCase { +public class JexlScriptEngineOptionalTest { private final JexlScriptEngineFactory factory = new JexlScriptEngineFactory(); private final ScriptEngineManager manager = new ScriptEngineManager(); private final ScriptEngine engine = manager.getEngineByName("jexl"); + @Test public void testOutput() throws Exception { String output = factory.getOutputStatement("foo\u00a9bar"); - assertEquals("JEXL.out.print('foo\\u00a9bar')", output); + Assert.assertEquals("JEXL.out.print('foo\\u00a9bar')", output); // redirect output to capture evaluation result final StringWriter outContent = new StringWriter(); engine.getContext().setWriter(outContent); engine.eval(output); - assertEquals("foo\u00a9bar", outContent.toString()); + Assert.assertEquals("foo\u00a9bar", outContent.toString()); } + @Test public void testError() throws Exception { String error = "JEXL.err.print('ERROR')"; // redirect error to capture evaluation result final StringWriter outContent = new StringWriter(); engine.getContext().setErrorWriter(outContent); engine.eval(error); - assertEquals("ERROR", outContent.toString()); + Assert.assertEquals("ERROR", outContent.toString()); } + @Test public void testCompilable() throws Exception { - assertTrue("Engine should implement Compilable", engine instanceof Compilable); + Assert.assertTrue("Engine should implement Compilable", engine instanceof Compilable); Compilable cengine = (Compilable) engine; CompiledScript script = cengine.compile("40 + 2"); - assertEquals(Integer.valueOf(42), script.eval()); - assertEquals(Integer.valueOf(42), script.eval()); + Assert.assertEquals(Integer.valueOf(42), script.eval()); + Assert.assertEquals(Integer.valueOf(42), script.eval()); } } diff --git a/src/test/java/org/apache/commons/jexl3/scripting/JexlScriptEngineTest.java b/src/test/java/org/apache/commons/jexl3/scripting/JexlScriptEngineTest.java index 5341396ec..ed6c4e026 100644 --- a/src/test/java/org/apache/commons/jexl3/scripting/JexlScriptEngineTest.java +++ b/src/test/java/org/apache/commons/jexl3/scripting/JexlScriptEngineTest.java @@ -25,9 +25,10 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; -import junit.framework.TestCase; +import org.junit.Assert; +import org.junit.Test; -public class JexlScriptEngineTest extends TestCase { +public class JexlScriptEngineTest { private static final List NAMES = Arrays.asList("JEXL", "Jexl", "jexl", "JEXL2", "Jexl2", "jexl2", "JEXL3", "Jexl3", "jexl3"); @@ -36,89 +37,94 @@ public class JexlScriptEngineTest extends TestCase { "application/x-jexl2", "application/x-jexl3"); + @Test public void testScriptEngineFactory() throws Exception { JexlScriptEngineFactory factory = new JexlScriptEngineFactory(); - assertEquals("JEXL Engine", factory.getParameter(ScriptEngine.ENGINE)); - assertEquals("3.0", factory.getParameter(ScriptEngine.ENGINE_VERSION)); - assertEquals("JEXL", factory.getParameter(ScriptEngine.LANGUAGE)); - assertEquals("3.0", factory.getParameter(ScriptEngine.LANGUAGE_VERSION)); - assertNull(factory.getParameter("THREADING")); - assertEquals(NAMES, factory.getParameter(ScriptEngine.NAME)); - assertEquals(EXTENSIONS, factory.getExtensions()); - assertEquals(MIMES, factory.getMimeTypes()); + Assert.assertEquals("JEXL Engine", factory.getParameter(ScriptEngine.ENGINE)); + Assert.assertEquals("3.2", factory.getParameter(ScriptEngine.ENGINE_VERSION)); + Assert.assertEquals("JEXL", factory.getParameter(ScriptEngine.LANGUAGE)); + Assert.assertEquals("3.2", factory.getParameter(ScriptEngine.LANGUAGE_VERSION)); + Assert.assertNull(factory.getParameter("THREADING")); + Assert.assertEquals(NAMES, factory.getParameter(ScriptEngine.NAME)); + Assert.assertEquals(EXTENSIONS, factory.getExtensions()); + Assert.assertEquals(MIMES, factory.getMimeTypes()); - assertEquals("42;", factory.getProgram(new String[]{"42"})); - assertEquals("str.substring(3,4)", factory.getMethodCallSyntax("str", "substring", new String[]{"3", "4"})); + Assert.assertEquals("42;", factory.getProgram(new String[]{"42"})); + Assert.assertEquals("str.substring(3,4)", factory.getMethodCallSyntax("str", "substring", new String[]{"3", "4"})); } + @Test public void testScriptingGetBy() throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); - assertNotNull("Manager should not be null", manager); + Assert.assertNotNull("Manager should not be null", manager); for (String name : NAMES) { ScriptEngine engine = manager.getEngineByName(name); - assertNotNull("Engine should not be null (name)", engine); + Assert.assertNotNull("Engine should not be null (name)", engine); } for (String extension : EXTENSIONS) { ScriptEngine engine = manager.getEngineByExtension(extension); - assertNotNull("Engine should not be null (extension)", engine); + Assert.assertNotNull("Engine should not be null (extension)", engine); } for (String mime : MIMES) { ScriptEngine engine = manager.getEngineByMimeType(mime); - assertNotNull("Engine should not be null (mime)", engine); + Assert.assertNotNull("Engine should not be null (mime)", engine); } } + @Test public void testScripting() throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); - assertNotNull("Manager should not be null", manager); + Assert.assertNotNull("Manager should not be null", manager); ScriptEngine engine = manager.getEngineByName("jexl3"); final Integer initialValue = Integer.valueOf(123); - assertEquals(initialValue,engine.eval("123")); - assertEquals(initialValue,engine.eval("0;123"));// multiple statements + Assert.assertEquals(initialValue,engine.eval("123")); + Assert.assertEquals(initialValue,engine.eval("0;123"));// multiple statements long time1 = System.currentTimeMillis(); Long time2 = (Long) engine.eval( "sys=context.class.forName(\"java.lang.System\");" +"now=sys.currentTimeMillis();" ); - assertTrue("Must take some time to process this",time1 <= time2.longValue()); + Assert.assertTrue("Must take some time to process this",time1 <= time2.longValue()); engine.put("value", initialValue); - assertEquals(initialValue,engine.get("value")); + Assert.assertEquals(initialValue,engine.get("value")); final Integer newValue = Integer.valueOf(124); - assertEquals(newValue,engine.eval("old=value;value=value+1")); - assertEquals(initialValue,engine.get("old")); - assertEquals(newValue,engine.get("value")); - assertEquals(engine.getContext(),engine.get(JexlScriptEngine.CONTEXT_KEY)); + Assert.assertEquals(newValue,engine.eval("old=value;value=value+1")); + Assert.assertEquals(initialValue,engine.get("old")); + Assert.assertEquals(newValue,engine.get("value")); + Assert.assertEquals(engine.getContext(),engine.get(JexlScriptEngine.CONTEXT_KEY)); // Check behaviour of JEXL object - assertEquals(engine.getContext().getReader(),engine.eval("JEXL.in")); - assertEquals(engine.getContext().getWriter(),engine.eval("JEXL.out")); - assertEquals(engine.getContext().getErrorWriter(),engine.eval("JEXL.err")); - assertEquals(System.class,engine.eval("JEXL.System")); + Assert.assertEquals(engine.getContext().getReader(),engine.eval("JEXL.in")); + Assert.assertEquals(engine.getContext().getWriter(),engine.eval("JEXL.out")); + Assert.assertEquals(engine.getContext().getErrorWriter(),engine.eval("JEXL.err")); + Assert.assertEquals(System.class,engine.eval("JEXL.System")); } + @Test public void testNulls() throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); - assertNotNull("Manager should not be null", manager); + Assert.assertNotNull("Manager should not be null", manager); ScriptEngine engine = manager.getEngineByName("jexl3"); - assertNotNull("Engine should not be null (name)", engine); + Assert.assertNotNull("Engine should not be null (name)", engine); try { engine.eval((String)null); - fail("Should have caused NPE"); + Assert.fail("Should have caused NPE"); } catch (NullPointerException e) { // NOOP } try { engine.eval((Reader)null); - fail("Should have caused NPE"); + Assert.fail("Should have caused NPE"); } catch (NullPointerException e) { // NOOP } } + @Test public void testScopes() throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); - assertNotNull("Manager should not be null", manager); + Assert.assertNotNull("Manager should not be null", manager); ScriptEngine engine = manager.getEngineByName("JEXL"); - assertNotNull("Engine should not be null (JEXL)", engine); + Assert.assertNotNull("Engine should not be null (JEXL)", engine); manager.put("global",Integer.valueOf(1)); engine.put("local", Integer.valueOf(10)); manager.put("both",Integer.valueOf(7)); @@ -127,30 +133,32 @@ public void testScopes() throws Exception { engine.eval("global=global+1"); engine.eval("both=both+1"); // should update engine value only engine.eval("newvar=42;"); - assertEquals(Integer.valueOf(2),manager.get("global")); - assertEquals(Integer.valueOf(11),engine.get("local")); - assertEquals(Integer.valueOf(7),manager.get("both")); - assertEquals(Integer.valueOf(8),engine.get("both")); - assertEquals(Integer.valueOf(42),engine.get("newvar")); - assertNull(manager.get("newvar")); + Assert.assertEquals(Integer.valueOf(2),manager.get("global")); + Assert.assertEquals(Integer.valueOf(11),engine.get("local")); + Assert.assertEquals(Integer.valueOf(7),manager.get("both")); + Assert.assertEquals(Integer.valueOf(8),engine.get("both")); + Assert.assertEquals(Integer.valueOf(42),engine.get("newvar")); + Assert.assertNull(manager.get("newvar")); } + @Test public void testDottedNames() throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); - assertNotNull("Manager should not be null", manager); + Assert.assertNotNull("Manager should not be null", manager); ScriptEngine engine = manager.getEngineByName("JEXL"); - assertNotNull("Engine should not be null (JEXL)", engine); + Assert.assertNotNull("Engine should not be null (JEXL)", engine); engine.eval("this.is.a.test=null"); - assertNull(engine.get("this.is.a.test")); - assertEquals(Boolean.TRUE, engine.eval("empty(this.is.a.test)")); + Assert.assertNull(engine.get("this.is.a.test")); + Assert.assertEquals(Boolean.TRUE, engine.eval("empty(this.is.a.test)")); final Object mymap = engine.eval("testmap={ 'key1' : 'value1', 'key2' : 'value2' }"); - assertTrue(mymap instanceof Map); - assertEquals(2,((Map)mymap).size()); + Assert.assertTrue(mymap instanceof Map); + Assert.assertEquals(2,((Map)mymap).size()); } + @Test public void testDirectNew() throws Exception { ScriptEngine engine = new JexlScriptEngine(); final Integer initialValue = Integer.valueOf(123); - assertEquals(initialValue,engine.eval("123")); + Assert.assertEquals(initialValue,engine.eval("123")); } } diff --git a/src/test/resources/log4j.xml b/src/test/resources/log4j.xml deleted file mode 100644 index d0ae637f6..000000000 --- a/src/test/resources/log4j.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/test/scripts/testAdd.jexl b/src/test/scripts/testAdd.jexl new file mode 100644 index 000000000..ed97e0c88 --- /dev/null +++ b/src/test/scripts/testAdd.jexl @@ -0,0 +1,18 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +//(x, y)->{ x + y } +x + y