From 4f6b6a3530b3fa1166ebfe46a0afa2b25d603f62 Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Sat, 26 Oct 2024 08:09:01 +0200 Subject: [PATCH] [java223] Initial contribution --- bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.automation.java223/NOTICE | 13 + .../org.openhab.automation.java223/README.md | 567 ++++++++++++++++++ .../org.openhab.automation.java223/bnd.bnd | 30 + .../org.openhab.automation.java223/pom.xml | 118 ++++ .../scriptengine/java/Isolation.java | 20 + .../scriptengine/java/JavaCompiledScript.java | 185 ++++++ .../scriptengine/java/JavaScriptEngine.java | 267 +++++++++ .../java/JavaScriptEngineFactory.java | 101 ++++ .../scriptengine/java/MemoryClassLoader.java | 64 ++ .../scriptengine/java/MemoryFileManager.java | 243 ++++++++ .../java/bindings/BindingStrategy.java | 17 + .../java/compilation/CompilationStrategy.java | 44 ++ .../DefaultCompilationStrategy.java | 18 + .../IncrementalCompilationStrategy.java | 33 + .../compilation/NoInterceptorStrategy.java | 10 + .../ScriptInterceptorStrategy.java | 10 + .../java/construct/ConstructorStrategy.java | 17 + .../construct/DefaultConstructorStrategy.java | 111 ++++ .../construct/NullConstructorStrategy.java | 16 + .../execution/DefaultExecutionStrategy.java | 77 +++ .../java/execution/ExecutionStrategy.java | 17 + .../execution/ExecutionStrategyFactory.java | 17 + .../execution/MethodExecutionStrategy.java | 129 ++++ .../java/name/DefaultNameStrategy.java | 36 ++ .../java/name/FixNameStrategy.java | 21 + .../scriptengine/java/name/NameStrategy.java | 45 ++ .../PackageResourceListingStrategy.java | 11 + .../java/util/CompositeIterator.java | 50 ++ .../java/util/ReflectionUtil.java | 53 ++ .../services/javax.script.ScriptEngineFactory | 1 + .../helper/java/helper/rules/Java223Rule.java | 184 ++++++ .../helper/rules/RuleAnnotationParser.java | 249 ++++++++ .../helper/rules/RuleParserException.java | 33 + .../annotations/ChannelEventTrigger.java | 38 ++ .../annotations/ChannelEventTriggers.java | 32 + .../helper/rules/annotations/CronTrigger.java | 34 ++ .../rules/annotations/CronTriggers.java | 32 + .../rules/annotations/DateTimeTrigger.java | 34 ++ .../rules/annotations/DateTimeTriggers.java | 32 + .../rules/annotations/DayOfWeekCondition.java | 32 + .../annotations/EphemerisDaysetCondition.java | 32 + .../EphemerisHolidayCondition.java | 31 + .../EphemerisNotHolidayCondition.java | 31 + .../EphemerisWeekdayCondition.java | 31 + .../EphemerisWeekendCondition.java | 31 + .../annotations/GenericAutomationTrigger.java | 36 ++ .../GenericAutomationTriggers.java | 32 + .../annotations/GenericCompareCondition.java | 42 ++ .../annotations/GenericCompareConditions.java | 32 + .../annotations/GenericEventCondition.java | 40 ++ .../annotations/GenericEventTrigger.java | 40 ++ .../annotations/GenericEventTriggers.java | 32 + .../annotations/GroupCommandTrigger.java | 38 ++ .../annotations/GroupCommandTriggers.java | 32 + .../annotations/GroupStateChangeTrigger.java | 40 ++ .../annotations/GroupStateChangeTriggers.java | 32 + .../annotations/GroupStateUpdateTrigger.java | 38 ++ .../annotations/GroupStateUpdateTriggers.java | 32 + .../rules/annotations/ItemCommandTrigger.java | 38 ++ .../annotations/ItemCommandTriggers.java | 32 + .../annotations/ItemStateChangeTrigger.java | 40 ++ .../annotations/ItemStateChangeTriggers.java | 32 + .../rules/annotations/ItemStateCondition.java | 38 ++ .../annotations/ItemStateConditions.java | 32 + .../annotations/ItemStateUpdateTrigger.java | 38 ++ .../annotations/ItemStateUpdateTriggers.java | 32 + .../java/helper/rules/annotations/Rule.java | 41 ++ .../annotations/SystemStartlevelTrigger.java | 32 + .../annotations/ThingStatusChangeTrigger.java | 40 ++ .../ThingStatusChangeTriggers.java | 32 + .../annotations/ThingStatusUpdateTrigger.java | 38 ++ .../ThingStatusUpdateTriggers.java | 32 + .../rules/annotations/TimeOfDayCondition.java | 36 ++ .../rules/annotations/TimeOfDayTrigger.java | 34 ++ .../rules/annotations/TimeOfDayTriggers.java | 32 + .../helper/rules/eventinfo/ChannelEvent.java | 39 ++ .../helper/rules/eventinfo/EventInfo.java | 34 ++ .../helper/rules/eventinfo/ItemCommand.java | 39 ++ .../rules/eventinfo/ItemStateChange.java | 46 ++ .../rules/eventinfo/ItemStateUpdate.java | 39 ++ .../rules/eventinfo/ThingStatusChange.java | 47 ++ .../rules/eventinfo/ThingStatusUpdate.java | 40 ++ .../src/main/feature/feature.xml | 9 + .../java223/common/BindingInjector.java | 293 +++++++++ .../java223/common/InjectBinding.java | 62 ++ .../java223/common/Java223Constants.java | 38 ++ .../java223/common/Java223Exception.java | 34 ++ .../java223/common/ReuseScriptInstance.java | 36 ++ .../automation/java223/common/RunScript.java | 32 + .../java223/common/ScriptLoadedTrigger.java | 31 + .../java223/common/ScriptUnloadedTrigger.java | 31 + .../internal/Java223CompiledScriptCache.java | 84 +++ .../Java223CompiledScriptInstanceWrapper.java | 66 ++ .../java223/internal/Java223ScriptEngine.java | 154 +++++ .../internal/Java223ScriptEngineFactory.java | 321 ++++++++++ .../codegeneration/DependencyGenerator.java | 292 +++++++++ .../codegeneration/SourceGenerator.java | 359 +++++++++++ .../internal/codegeneration/SourceWriter.java | 104 ++++ .../internal/strategy/Java223Strategy.java | 310 ++++++++++ .../strategy/ScriptWrappingStrategy.java | 114 ++++ .../strategy/jarloader/JarClassLoader.java | 90 +++ .../strategy/jarloader/JarFileManager.java | 258 ++++++++ .../strategy/jarloader/JarFileObject.java | 146 +++++ .../src/main/resources/OH-INF/addon/addon.xml | 37 ++ .../src/main/resources/generated/Actions.ftl | 53 ++ .../src/main/resources/generated/Items.ftl | 40 ++ .../resources/generated/Java223Script.ftl | 98 +++ .../main/resources/generated/ThingAction.ftl | 55 ++ .../src/main/resources/generated/Things.ftl | 39 ++ .../codegeneration/ClassGeneratorTest.java | 84 +++ bundles/pom.xml | 1 + 112 files changed, 7822 insertions(+) create mode 100644 bundles/org.openhab.automation.java223/NOTICE create mode 100644 bundles/org.openhab.automation.java223/README.md create mode 100644 bundles/org.openhab.automation.java223/bnd.bnd create mode 100644 bundles/org.openhab.automation.java223/pom.xml create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/Isolation.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaCompiledScript.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaScriptEngine.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaScriptEngineFactory.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/MemoryClassLoader.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/MemoryFileManager.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/bindings/BindingStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/CompilationStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/DefaultCompilationStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/IncrementalCompilationStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/NoInterceptorStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/ScriptInterceptorStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/ConstructorStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/DefaultConstructorStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/NullConstructorStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/DefaultExecutionStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/ExecutionStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/ExecutionStrategyFactory.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/MethodExecutionStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/DefaultNameStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/FixNameStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/NameStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/packagelisting/PackageResourceListingStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/util/CompositeIterator.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/util/ReflectionUtil.java create mode 100644 bundles/org.openhab.automation.java223/src/3rdparty/resources/META-INF/services/javax.script.ScriptEngineFactory create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/Java223Rule.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/RuleAnnotationParser.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/RuleParserException.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ChannelEventTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ChannelEventTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/CronTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/CronTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DateTimeTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DateTimeTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DayOfWeekCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisDaysetCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisHolidayCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisNotHolidayCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisWeekdayCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisWeekendCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericAutomationTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericAutomationTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericCompareCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericCompareConditions.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupCommandTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupCommandTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateChangeTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateChangeTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateUpdateTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateUpdateTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemCommandTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemCommandTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateChangeTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateChangeTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateConditions.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateUpdateTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateUpdateTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/Rule.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/SystemStartlevelTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusChangeTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusChangeTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusUpdateTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusUpdateTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayCondition.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayTriggers.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ChannelEvent.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/EventInfo.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemCommand.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemStateChange.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemStateUpdate.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ThingStatusChange.java create mode 100644 bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ThingStatusUpdate.java create mode 100644 bundles/org.openhab.automation.java223/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/BindingInjector.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/InjectBinding.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/Java223Constants.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/Java223Exception.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ReuseScriptInstance.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/RunScript.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ScriptLoadedTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ScriptUnloadedTrigger.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223CompiledScriptCache.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223CompiledScriptInstanceWrapper.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223ScriptEngine.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223ScriptEngineFactory.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/DependencyGenerator.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/SourceGenerator.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/SourceWriter.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/Java223Strategy.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/ScriptWrappingStrategy.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarClassLoader.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarFileManager.java create mode 100644 bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarFileObject.java create mode 100644 bundles/org.openhab.automation.java223/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.automation.java223/src/main/resources/generated/Actions.ftl create mode 100644 bundles/org.openhab.automation.java223/src/main/resources/generated/Items.ftl create mode 100644 bundles/org.openhab.automation.java223/src/main/resources/generated/Java223Script.ftl create mode 100644 bundles/org.openhab.automation.java223/src/main/resources/generated/ThingAction.ftl create mode 100644 bundles/org.openhab.automation.java223/src/main/resources/generated/Things.ftl create mode 100644 bundles/org.openhab.automation.java223/src/test/java/org/openhab/automation/java223/internal/codegeneration/ClassGeneratorTest.java diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index b2983e2cc2df8..603538afb9113 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -21,6 +21,11 @@ org.openhab.automation.groovyscripting ${project.version} + + org.openhab.addons.bundles + org.openhab.automation.java223 + ${project.version} + org.openhab.addons.bundles org.openhab.automation.jrubyscripting diff --git a/bundles/org.openhab.automation.java223/NOTICE b/bundles/org.openhab.automation.java223/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.automation.java223/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.automation.java223/README.md b/bundles/org.openhab.automation.java223/README.md new file mode 100644 index 0000000000000..d68674c661e3a --- /dev/null +++ b/bundles/org.openhab.automation.java223/README.md @@ -0,0 +1,567 @@ +# openHAB Java223 Scripting + +Write OpenHAB scripts in Java as a JSR223 language. + +Features : +- full JSR 223 support (use in files, in GUI, transformations, inline rule action, etc...) +- auto injection of OpenHAB variable/preset for simplicity +- library support for sharing code (.jar and .java) +- rule annotations available in the helper library for creating rules the easiest way +- helper library files auto generation for items, things, and actions, with strong typing and ease of use +- cache compiled scripts in memory for blazingly fast executions after the first one (sub millisecond overload) +- no boilerplate code for simple script: you can do a one liner script, as declaring a class and a method is optional. +- optional reuse of instances script to share values between execution occurrences +- designed to be easily used with your favorite IDE + +It makes heavy use of Eric Obermühlner's Java JSR 223 ScriptEngine [java-scriptengine](https://github.com/eobermuhlner/java-scriptengine), and is partially based on work from other OpenHAB contributors that create their own JSR 223 java automation bundle (many thanks to them). + +# What you can do ? + +All JSR223 OpenHAB related thing, and surely a bit more, thanks to your scripts sharing the same JVM as OpenHAB. +If you just want to see how to use it, see the [Examples](#examples) section. + +# How it works + +You should first take a look at the [official documentation about OpenHAB JSR223 support](https://www.openhab.org/docs/configuration/jsr223.html). +That said, keep reading for useful insider informations. + +## Script location: where can I use Java223 ? + +### First location option: GUI + +As a full featured JSR223 automation bundle, you can use the GUI to use Java223 scripts, everywhere JSR223 scripts are allowed. Including, but not limited to: +- Creating `Scripts` in the so-called GUI section +- Inside a `Rule`, as an inline script action in the `Then` or the `Only If` section +- When linking a channel to an item, as a transformation `Profile` of type `Script Java` + +### Second location option: File script + +A JSR223 script file is a script located in your configuration directory, under the `automation/jsr223` sub directory. + +At startup, or each time a file is created (or modified) in this directory, OpenHAB will handle it to the relevant JSR223 scripting language for **immediate** execution (using the extension as a discriminating value). So in our case, every `.java` files will be handled by the Java223 automation bundle. + +As a script can create and register rules during its execution (by accessing and using the OpenHAB automation manager), **this 'file mode' is then especially useful for defining rules**. And icing on the cake: when a script that created rules is deleted, the linked rules are also deleted, thanks to the way OpenHAB registers a rule (same for modification, the associated rules are deleted and recreated). See the [rules](#rules) section for more information on how to create a rule. + +## Execution + +A Java223 script does not need to have any dependency to anything in order to be compiled and executed. It can just be a plain, simple java class like this one: + +```java +public class SimpleClass { + public void main() { + int sum = 2 + 2; + } +} +``` +(In fact it can even be a simpler one-liner, see the [no boilerplate section](#noboilerplate) + +When OpenHAB presents a java script to the Java223 automation bundle, it searches for methods with name like `main`, or `eval`, or `run`, or `exec`, or any methods annotated with `@RunScript` and then runs them (from here we will refer to those as the "runnable methods"). Returning a value is supported but optional. That's all you need for a very simple script ! + +A note about the context : each script has its own context, its own ClassLoader. It means that scripts are perfectly separated, and cannot interact with, or even see, each other. But do not worry, because there are dedicated features for this ([shared cache](#sharedcache) for sharing values, [library](#library) for sharing code). + +## Variable injection + +Of course, a script needs to communicate with OpenHAB to be useful. We will call 'OpenHAB inputs' those objects, values, references, that OpenHAB gives to the automation bundles, in order for it to expose them to user. For example, a reference to the items registry will allow a script to interact with items by checking their state or giving them command. + +With this Java223 bundle, it is done by the way of automatic injection. It means that you don't need to do anything special. You just have to declare variable in your script and the bundle will take care of injecting the corresponding value in it. There are three input injection possibilities: +- as a field in your script (see [example](#fieldinjection)) +- as a method parameter in your runnable methods (see [example](#parameterinjection)) +- as a method parameter in the constructor of the script. (see [example](#constructorinjection)) + +The variable name is used to find the correct value to inject, so take care of your spelling (full reference in [official documentation about OpenHAB JSR223 support](https://www.openhab.org/docs/configuration/jsr223.html#scriptextension-objects-all-jsr223-languages) ), or inherit the [Java223Script helper class](#java223script) to directly have the right variable names. + +### Advanced injection + +You can control the injection further (i.e. overriding default behavior, or directly injecting something from a preset) with the @InjectBinding annotation. See [example](#injectbinding). + + + +## Defining Rules + +As a JSR223 OpenHAB language, you can define rule with the OpenHAB DSL. All needed classes and instance (SimpleRule, TriggerBuilder, automationManager instance, etc.) are of course exposed natively. You can see an example of how to use it [here](https://www.openhab.org/docs/configuration/jsr223.html#example-rules-for-a-first-impression) (examples are written with other languages, but concepts and objects for Java223 are the same). + +**However**, keep in mind that there is a much, much more convenient way to do this. You can jump to the relevant section [here](#helperrules). But the following sections also exposes some prerequisites if you want to have a better comprehension before jumping in. + + + +## Library for sharing code + +To share reusable code between your scripts, you have to define a library. A library is a .java file (or a .jar archive containing several compiled class) located in your configuration directory, under the `automation\lib\java` subdirectory. + +The Java223 bundle will monitor this directory, and automatically adds everything inside to the compilation unit of your script (although, it's not applied retrospectively). The script still have its dedicated ClassLoader, but inside this ClassLoader, all your library classes are also available. + +Be careful : it also means that other scripts have their own library classes inside their own ClassLoader. **You cannot share value between scripts this way**, even by using a static property inside a library class. + +### Auto injection of library + +Your library probably also needs to communicate with OpenHAB. You can of course pass OpenHAB input references as a parameter to your library methods, or by a setter. For example, if we imagine a library `MyLibrary` that need access to the items and things registries : + +```java +... + ItemRegistry ir; // <- auto injected in your script + ThingRegistry things; // <- auto injected in your script + public void main() { + var myUsefulParameter1 = ... + var myUsefulParameter2 = ... + MyLibrary myLib = new MyLibrary(); + myLib.doSomethingInteresting(ir, things, myUsefulParameter1, myUsefulParameter2); // <- then pass 'ir' and 'things' for your library to use + } +... +``` + +**But**, as you can see, it can be cumbersome and unnecessarily hard to read. This Java223 bundle provides a much simpler way to do this : letting it instantiate your library and auto inject all OpenHAB inputs value into them. It works on field, or method/constructor parameter. See [example](#libraryautoinjection). It can even works recursively : a lib can reference another lib, itself referencing some OpenHAB inputs, and all this will work out of the box. + +Getting back to our example : As the library instantiation and injection with the items and things registries are taken care of, the same code can then become : + +```java + public void main(MyLibrary myLib) { // <- myLib will be instantiated by the bundle, and auto injected with the OpenHAB input variables declared in it. + var myUsefulParameter1 = ... + var myUsefulParameter2 = ... + myLib.doSomethingInteresting(myUsefulParameter1, myUsefulParameter2); // <-- myLib already have registries reference injected, I do not need to pass them here + } +``` + +Tip: The Java223 automation bundle recognizes a library by its type, so you don't have to worry about respecting a naming convention for the variable. Feel free to use anything. + + +## Generated helper library + +The helper library is totally optional, but you should seriously consider using it, as it will make your code experience much more streamlined. It consists of two parts: dynamically .java generated files, and a JAR file with some already compiled class. + +### Java dynamic classes + +The Java223 bundle generates some ready-to-use libraries in the `automation\lib\java` directory. These classes are dynamic and contains information about your OpenHAB setup. + +You will get several java files in the package `helper.generated` : +- Items.java : contains all your items name as static String, and label as their javadoc. Also contains methods to directly get the Item, casted to the right Class. (see [example](#itemsandthings)) +- Things.java : contains all your Thing UID as static String, with label as their javadoc. Also contains methods to directly get the Thing. (see [example](#itemsandthings)) +- Actions.java : contains strongly typed, ready to use methods, to get the actions available on your things. (see [example](#actions)) + +- Java223Script.java : this abstract class will come **very** handy. In fact, it is so handy that all your scripts should inherit it ! It already contains all OpenHAB inputs variables, as well as some others useful shortcut. Take a look at it. + +As these files are no more no less standard library files, you can of course use them as candidates for auto injection in your script. Be careful though, do not use the variable names `items`, `things`, or `actions`, as they are already reserved as OpenHAB input values for the ItemRegistry, ThingsRegistry, and ScriptThingActions respectively. +As a reference, in the super handy Java223Script helper abstract class, we are using `_items`, `_things`, `_actions` for them. + +**Tip : all your scripts, *including libraries*, can extend the `Java223Script` class. This way they will automatically obtain easy access to all OpenHAB inputs, to some shortcuts, etc.** + + + +### helper-lib.jar and rules + +**This is the most useful feature of this entire bundle.** + +The Java223 bundle also copies in your `automation\lib\java` a pre-compiled jar with a set of library files inside. This jar is no more, no less, a standard library jar, and is an example of how powerful the OpenHAB JSR223 feature is. It contains all you need to define Rules with the help of simple-to-use annotations. The entry point is the `RuleAnnotationParser` class. The `parse` method automatically scans your script, searching for annotated method defining rules, and then creates and registers them. + +Tip : The best way to use this functionality is to extend the `Java223Script`, as it already contains a call to the `parse` method in a `@RunScript` annotated method. + +When combined with all the aforementioned helpers, see how easy it is to define a rule. + +```java +import ...; + +public class MyRule extends Java223Script { + + @Rule + @ItemStateUpdateTrigger(itemName = Items.my_detector_item, state = OnOffType.ON.toString()) + public void myRule() { + _items.my_bulb_item.send(OnOffType.ON); + } +} +``` + +This rule above is triggered by a 'ON' state update of an item linked to a detector, and then light a bulb : **Here really shines the JSR223 for Java : no random strings, full auto completion from your IDE, strongly typed code.** + +You can also use automatic injection **in your rule method parameter**. It is especially useful for strongly typed parameter. Take a look at this rule, triggered by two different detectors: + +```java +import ...; + +public class MyRule extends Java223Script { + + @Rule(name = "detecting.people", description = "Detecting people and light") + @ItemStateUpdateTrigger(itemName = Items.my_detector_item, state = OnOffType.ON.toString()) + @ItemStateUpdateTrigger(itemName = Items.my_otherdetector_item, state = OnOffType.ON.toString()) + public void myRule(ItemStateChange inputs) { // HERE, strongly typed parameter + _items.my_bulb_item.send(OnOffType.ON); + logger.info("Movement detected at " + inputs.getItemName()); // inputs.getItemName() give me the triggering detector name + } +} +``` +`ItemStateChange` is available in the helper-lib.jar, alongside other strongly typed events. As it is a Java223 library class like others, it leverages the autoinjection feature: its fields are automatically injected with the corresponding parameter given by OpenHAB. So, by using the right event object for your trigger, such as `ItemStateChange` in this example, you don't have to check the documentation to search for how the event parameter you need is named, and you won't miss the parameter because you misspelled it. You should find in the helper lib the other event objects matching the triggers of your rules. + +Here are all functionalities of the helper-lib: +- Many different `@Trigger` class. Check the `helper.rules.annotation` package for a list. +- You can add (multiple) `@Condition` to a Rule. It exposes a pre-condition for the rule to execute. Check the `helper.rules.annotation` package +- `@Trigger`, `@Conditions`, `@Rule` have many parameter. Some parameters add functionality, others can overwrite default behavior (for example using the method name for the label of a rule). +- Pre-made event objects that you can use as a parameter in a rule are defined in the package `helper.rules.eventinfo`. You can define your own if some are missing (do not hesitate to make a Pull Request) +- If you want all the triggering event input parameters in a map for a rule, you can use the parameter `Map inputs`. +- You can set the `@Rule` annotation on a method, but also on many type of field containing code to execute, such as Function, Runnable... Take a look at the class `Java223Rule`. You can even switch the value of the field at runtime, thus making the code your rule execute even more dynamic. + + + +## Share value between scripts + +To share value between different scripts, you can use the shared cache available in the `cache` preset. Auto-inject it with : + +```java + protected @InjectBinding(preset = "cache", named = "sharedCache") ValueCache sharedCache; +``` + +This cache is accessible the same way a `Map` is. + +Tip : it is automatically available to scripts inheriting the Java223Script helper class. + +## Share value between script executions + +The Java223 automation bundle has an option `allowInstanceReuse`. If set to true, the default engine behavior will be to reuse script instance between executions, instead of re-instantiating with a `new` operator every time. If you run the same script over and over, it will try to use the same instance, thus allowing you to store information in its field (in memory, so only for the duration of the OpenHAB process). Be careful for read/write concurrency issue. + +Of course your script has to remain the same. So script file in the `automation/jsr223` directory cannot use this functionality, as they are only executed once by nature, when OpenHAB start, or when they are created or modified (which is another way of saying deleted/recreated). + +You can also overwrite this default behavior for individual script by using the `@ReuseScriptInstance` annotation on the class level. + +Take note that it uses the compilation cache. So if your cache is not big enough (50 script by default), persistence of your fields values is not assured. + +You should also note that Rule inner working is different: your rule method code is always executed on the same instance, no matter the value of `allowInstanceReuse` or the presence of a `@ReuseScriptInstance` configuration. So you can also share information here, as a field in classes defining Rule. + + + +## No boilerplate code + +Sometimes, you 'real' (useful) code is very short, and you don't need complex logic, custom auto injection, etc. +In this case, you can omit the 'boilerplate' code, and just write your 'useful' code. +Under the hood, the Java223 bundle will 'wrap' your code inside a class inheriting `Java223Script`, with a bunch of standard import (mainly item state types) and a main method. + +For example, this one-line script is perfectly valid: + +```java + _items.myitem().send(OFF); // let there be light +``` + +This 'wrapping' will take place if nowhere in your code a trimmed line starts with `public class`. + +If you need to import some class, you can also do it. The import statements (lines starting with `import `) will be parsed and added in the beginning of the resulting script, before the wrapping class and method. + +You can return a value. The line returning the value MUST begins with `return `. This is useful for Transformation. + +But because your code is wrapped, the following functionalities are not available: +- definition of methods (your code is already inside one) +- customize the auto-injection (class field members or method parameters, are not available for addition/modification). You have to rely on what is already injected by the parent Java223Script. + + +## Transformation + +You can use Java223 script in transformation. +A transformation is a piece of code with an input and an output. So you just have to respect this contract: +- you can use the OpenHAB input value named 'input'. Auto injection is possible. Or you can inherit the Java223Script, as it is already declared. +- your runnable method must return a value + +Example of transformation appending the word "Hello" to the input, using the "no boilerplate" functionality: + +```java +return "Hello " + input.toString(); +``` + +# Use your IDE + +## convenience-dependencies.jar + +A jar file, purely for convenience, is exported from the classpath and added to the lib directory when the bundle starts. This jar is EXCLUDED from entering the compilation unit of your script; it sole purpose is for you to use inside an IDE. It contains most of the OpenHAB classes you probably need to write and compile scripts and rules. By using this jar in your project, you probably won't have to setup advanced dependencies management tools such as Maven or Gradle. + +You can ask the Java223 bundle to add to this jar some classes by using the following Java223 configuration properties: +- `additionalBundles`: Additional package name exposed by bundles running in OpenHAB. Use ',' as a separator. +- `additionalClasses`: Additional individual classes. Use ',' as a separator. + +## Configure your project + +In order to use an IDE and write code with autocompletion, javadoc, and all other syntaxic sugar, you just have to add to your project : +- As a source directory: the root directory of your scripts, under `automation/jsr223` (probably `automation/jsr223/java`, but you can use what you want) +- As a source directory: the root directory of the library `automation/lib/java` +- As a library: `automation/lib/java/helper-lib.jar` +- As a library: `automation/lib/java/convenience-dependencies.jar` + +Tip : to access a remote OpenHAB installation, you can copy, use Webdav, a Samba share, a SFTP virtual file system or sync feature (available on your OS or included in your IDE), or any other mean you can think about. + + + +# Examples + + +## Lightning a bulb + +```java +import org.openhab.core.library.types.OnOffType; +import helper.generated.Java223Script; + +public class BasicExample extends Java223Script { + public void main() { + _items.myitem().send(OnOffType.OFF); // let there be light + } +} +``` + + + + +## No boilerplate code + +A one liner can also work + +```java + _items.myitem().send(OnOffType.OFF); // let there be light +``` + + +## Create a simple rule + +This rule is triggered by a 'ON' state update of an item linked to a detector, and then light a bulb. + +```java +import ...; + +public class MyRule extends Java223Script { + + @Rule + @ItemStateUpdateTrigger(itemName = Items.my_detector_item, state = OnOffType.ON.toString()) + public void myRule() { + _items.my_bulb_item.send(OnOffType.ON); + } +} +``` + +## Create a rule with several trigger and options + +This time, the rule is triggered by a 'ON' state update on one of two possible detectors. +The method parameter is a strongly typed library element (`ItemStateChange`) and as such, its field are auto injected with the right value from the input. Thanks to this, it is easy to get the input parameters without risking using a wrong parameter name. For example, we get here the name of the item triggering the detection, for a detailed log. +Instead of the default (the method name used for the label of the rule), it has a description, and a dedicated name for the label, and both will be shown on the OpenHAB GUI. + +```java +import ...; + +public class MyRule extends Java223Script { + + @Rule(name = "detecting.people", description = "Detecting people and light") + @ItemStateUpdateTrigger(itemName = Items.my_detector_item, state = OnOffType.ON.toString()) + @ItemStateUpdateTrigger(itemName = Items.my_otherdetector_item, state = OnOffType.ON.toString()) + public void myRule(ItemStateChange inputs) { // here, strongly typed parameter + _items.my_bulb_item.send(OnOffType.ON); + logger.info("Movement detected at {}", inputs.getItemName()); + } +} +``` + +## Example of different injection types of OpenHAB input variables + + + +If you don't want to extend the `Java223Script` class, then you will have to take care of formatting your script for injection of OpenHAB input value. + +### Field input injection + +```java +import ...; + +public class FieldInjectionExample { + ItemRegistry itemRegistry; // <-- the injection will happen here, 'itemRegistry' is a valid OpenHAB input name + public void main() { + itemRegistry.get("myitem").send(OnOffType.ON);; + } +} +``` + + + +### Method parameter input injection + +```java +import ...; + +public class MethodInjectionExample { + public void main(ItemRegistry itemRegistry) { // <-- the injection will happen here, 'itemRegistry' is a valid OpenHAB input name + itemRegistry.get("myitem").send(OnOffType.ON);; + } +} +``` + + + +### Constructor parameter input injection + +```java +import ...; + +public class ConstructorInjectionExample { + + ItemRegistry myItemRegistry; // <-- the injection WON'T happen here because the variable name is not the name as an available OpenHAB input + + public SimpleClass(ItemRegistry itemRegistry) { // <-- the injection will happen here, 'itemRegistry' is a valid OpenHAB input name + this.myItemRegistry = itemRegistry; + } + + public void main() { + myItemRegistry.get("myitem").send(OnOffType.ON);; + } +} +``` + + + +## Run another rule or script + +You may want to run another rule or a script. The rule manager is not a standard JSR223 variable, but the Java223 automation bundle can nonetheless inject it. +First, inject the rule manager in your script, then use it with the runNow method and the UID of the rule. + +Tip : The ruleManager is already declared as a field in the Java223Script helper class that you can inherit. + +```java +import java.util.Map; +import org.openhab.core.automation.RuleManager; + +public class RunAnotherRule { + public void main(RuleManager ruleManager) { + // simple execution : + ruleManager.runNow("myruleid"); + // execution with parameters in a key / value map + // set the boolean parameter to true if you want to check conditions before execution (in case of a full rule) + ruleManager.runNow("myparameterizedruleid", false, Map.of("key", "value"))) + } +} +``` + +## Disable a thing + +The ThingManager is not a standard JSR223 variable, but the Java223 automation bundle can nonetheless inject it. It is also available in the base class `Java223Script`, as shown in this example. + +```java +import java.util.Map; +import org.openhab.core.automation.RuleManager; + +public class DisableThing extends Java223Script { + public void main() { + thingManager.setEnabled(_things.network_pingdevice_mything().getUID(), false); + } +} +``` + + +## Use metadata + +The MetadataRegistry is not a standard JSR223 variable, but the Java223 automation bundle can nonetheless inject it. This script overwrites Google Assistant metadata every time it is executed (so, at each OpenHAB startup), effectively keeping this file as some kind of external 'database' where you can store all your metadata. + +```java +public class MetadataDatabase { + + protected MetadataRegistry metadataRegistry; // <-- auto injection here + + public void main() { + create("ga", Items.lock, "Lock", Map.of("name", "Front door")); + create("ga", Items.room_light, "Light", Map.of("name", "Master bedroom light")); + } + + private void create(String namespace, String itemName, String value, Map configuration) { + MetadataKey metadataKey = new MetadataKey(namespace, itemName); + metadataRegistry.remove(metadataKey); + metadataRegistry.add(new Metadata(metadataKey, value, configuration)); + } +} +``` + +Tip : The metadataRegistry is already declared as a field in the Java223Script helper class that you can inherit. + + + + +## Advanced injection control + +Control automatic injection behavior by using the `@InjectBinding` annotation. You can use it on field or on method/constructor parameter. + +```java +public class InjectBindingExample { + + // inject something from a preset : + protected @InjectBinding(preset = "RuleSupport", named = "automationManager") ScriptedAutomationManager automationManager; + // disable injection even if the field name should trigger it : + protected @InjectBinding(enable = "false") ItemRegistry itemRegistry; + // name your variable as you wish : + protected @InjectBinding(named = "itemRegistry") ItemRegistry otherVariableName; + // make it mandatory (the script will not run if the value cannot be found). Note : mandatory = true is the default value when using the annotation. + protected @InjectBinding(mandatory = true) ThingRegistry things; + + public void main(ItemRegistry itemRegistry) { + myItemRegistry.get("myitem"); + } +} +``` + + + +## Library use and auto injection + +Inside the `automation/lib/java` directory, let's define a library that will be available to all scripts. + +```java +import ...; + +public class MyGreatLibrary { + ItemRegistry itemRegistry; // will be auto-injected if instantiation is taken care of by the bundle + + public void myUsefullLibraryMethod(String itemName) { + itemRegistry.get(itemName); + //something usefull... + } +} +``` + +Here is how to use it with auto injection your script (in automation/jsr223/), for example with field injection : + +```java +import ...; + +public class MyScript { + + MyGreatLibrary mylib; // will be auto instantiated and then auto injected with all needed OpenHAB input value + + public void exec() { + mylib.myUsefullLibraryMethod("myitemName"); + } +} +``` + + + +Tip : do not forget that all classes, including libraries, can extend Java223Script + +## Items and Things helper libraries + +By extending the Java223Script class (optional, you can inject them the way you want), the variable _items and _things are directly accessible. + +```java +import ...; + +public class ItemsAndThingAccessExample extends Java223Script { // <-- take the Java223Script class as a base class + // to access _items and _things more easily + + public void exec() { + _items.myLightItem().send(OnOffType.ON); // <-- light on ! + logger.info(_things.zwave_device_2ecfa3a2_node68().getStatus().toString()); // <-- get thing info + } +} +``` + + + +## Actions helper libraries + +With the auto generated `Actions` class (here referenced by the `_actions` variable), you can call a method to get strongly typed actions (and auto completion) linked to your Thing. + +```java +import ...; + +public class ActionExample extends Java223Script { // <-- take the Java223Script class as a base class + // to access _actions more easily + + public void exec() { + _actions.getSmsmodem_SMSModemActions(Things.mySMSthing).sendSMS("+3312345678", "Hello world");; + } +} +``` + diff --git a/bundles/org.openhab.automation.java223/bnd.bnd b/bundles/org.openhab.automation.java223/bnd.bnd new file mode 100644 index 0000000000000..f5b4582c49ab9 --- /dev/null +++ b/bundles/org.openhab.automation.java223/bnd.bnd @@ -0,0 +1,30 @@ +Bundle-SymbolicName: ${project.artifactId} +DynamicImport-Package: * +Import-Package: \ +javax.measure,\ +javax.measure.quantity,\ +org.openhab.core.audio,\ +org.openhab.core.automation,\ +org.openhab.core.automation.util,\ +org.openhab.core.automation.module.script,\ +org.openhab.core.automation.module.script.defaultscope,\ +org.openhab.core.automation.module.script.rulesupport.shared,\ +org.openhab.core.automation.module.script.rulesupport.shared.simple,\ +org.openhab.core.common,\ +org.openhab.core.common.registry,\ +org.openhab.core.config.core,\ +org.openhab.core.items,\ +org.openhab.core.library.types,\ +org.openhab.core.library.items,\ +org.openhab.core.library.dimension,\ +org.openhab.core.model.script,\ +org.openhab.core.model.script.actions,\ +org.openhab.core.model.rule,\ +org.openhab.core.persistence,\ +org.openhab.core.persistence.extensions,\ +org.openhab.core.thing.binding,\ +org.openhab.core.transform,\ +org.openhab.core.transform.actions,\ +org.openhab.core.types,\ +org.openhab.core.voice,\ +com.google.gson \ No newline at end of file diff --git a/bundles/org.openhab.automation.java223/pom.xml b/bundles/org.openhab.automation.java223/pom.xml new file mode 100644 index 0000000000000..c17847fb930da --- /dev/null +++ b/bundles/org.openhab.automation.java223/pom.xml @@ -0,0 +1,118 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.automation.java223 + + openHAB Add-ons :: Bundles :: Automation :: Java Scripting + + + + + org.freemarker + freemarker + 2.3.32 + compile + + + + org.eclipse.jdt + org.eclipse.jdt.annotation + 2.2.600 + compile + + + com.github.ben-manes.caffeine + caffeine + 3.1.8 + + + + + + + + src/helper/java + + **/*.java + + + + src/main/resources/ + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + create-helper-jar + compile + + jar + + + helper + lib + ${basedir}/target/classes + + helper/** + + + + + default-jar + package + + jar + + + + helper/** + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + add-source + + generate-sources + + + src/3rdparty/java + src/3rdparty/resources + src/helper/java + + + + + + + + + + diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/Isolation.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/Isolation.java new file mode 100644 index 0000000000000..54b0410eac0b4 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/Isolation.java @@ -0,0 +1,20 @@ +package ch.obermuhlner.scriptengine.java; + +/** + * The isolation levels of the script at execution time. + */ +public enum Isolation { + /** + * The caller {@link ClassLoader} is visible to the script during execution. + * + * This allows to see all classes from the script that are visible in the calling application. + */ + CallerClassLoader, + + /** + * The script executes in an isolated {@link ClassLoader}. + * + * This hides all classes of the calling application. + */ + IsolatedClassLoader +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaCompiledScript.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaCompiledScript.java new file mode 100644 index 0000000000000..7be5f6033960b --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaCompiledScript.java @@ -0,0 +1,185 @@ +package ch.obermuhlner.scriptengine.java; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import javax.script.Bindings; +import javax.script.CompiledScript; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import ch.obermuhlner.scriptengine.java.bindings.BindingStrategy; +import ch.obermuhlner.scriptengine.java.execution.ExecutionStrategy; + +/** + * The compiled Java script created by a {@link JavaScriptEngine}. + */ +public class JavaCompiledScript extends CompiledScript { + private final JavaScriptEngine engine; + private final Class compiledClass; + private final Object compiledInstance; + private ExecutionStrategy executionStrategy; + private BindingStrategy bindingStrategy; + + /** + * Construct a {@link JavaCompiledScript}. + * + * @param engine the {@link JavaScriptEngine} that compiled this script + * @param compiledClass the compiled {@link Class} + * @param compiledInstance the instance of the compiled {@link Class} or {@code null} + * if no instance was created and only static methods will be called + * by the the {@link ExecutionStrategy}. + * @param executionStrategy the {@link ExecutionStrategy} + */ + public JavaCompiledScript(JavaScriptEngine engine, Class compiledClass, Object compiledInstance, + ExecutionStrategy executionStrategy, BindingStrategy bindingStrategy) { + this.engine = engine; + this.compiledClass = compiledClass; + this.compiledInstance = compiledInstance; + this.executionStrategy = executionStrategy; + this.bindingStrategy = bindingStrategy; + } + + /** + * Returns the compiled {@link Class}. + * + * @return the compiled {@link Class}. + */ + public Class getCompiledClass() { + return compiledClass; + } + + /** + * Returns the instance of the compiled {@link Class}. + * + * @return the instance of the compiled {@link Class} or {@code null} + * if no instance was created and only static methods will be called + * by the the {@link ExecutionStrategy}. + */ + public Object getCompiledInstance() { + return compiledInstance; + } + + /** + * Returns the compiled {@link Class}. + * + * @return the compiled {@link Class}. + * @deprecated in release 1.1.0 this method was deprecated, + * use {@link #getCompiledClass()} instead. + */ + @Deprecated + public Class getInstanceClass() { + return getCompiledClass(); + } + + /** + * Returns the instance of the compiled {@link Class}. + * + * @return the instance of the compiled {@link Class} or {@code null} + * if no instance was created and only static methods will be called + * by the the {@link ExecutionStrategy}. + * @deprecated in release 1.1.0 this method was deprecated, + * use {@link #getCompiledInstance()} instead. + */ + @Deprecated + public Object getInstance() { + return getCompiledInstance(); + } + + /** + * Sets the {@link ExecutionStrategy} to be used when evaluating the compiled class instance. + * + * @param executionStrategy the {@link ExecutionStrategy} + */ + public void setExecutionStrategy(ExecutionStrategy executionStrategy) { + this.executionStrategy = executionStrategy; + } + + @Override + public ScriptEngine getEngine() { + return engine; + } + + @Override + public Object eval(ScriptContext context) throws ScriptException { + Bindings globalBindings = context.getBindings(ScriptContext.GLOBAL_SCOPE); + Bindings engineBindings = context.getBindings(ScriptContext.ENGINE_SCOPE); + + pushVariables(globalBindings, engineBindings); + Object result = executionStrategy.execute(compiledInstance); + pullVariables(globalBindings, engineBindings); + + return result; + } + + private void pushVariables(Bindings globalBindings, Bindings engineBindings) throws ScriptException { + Map mergedBindings = mergeBindings(globalBindings, engineBindings); + + if (bindingStrategy != null) { + bindingStrategy.associateBindings(compiledClass, compiledInstance, mergedBindings); + return; + } + + for (Map.Entry entry : mergedBindings.entrySet()) { + String name = entry.getKey(); + Object value = entry.getValue(); + + try { + Field field = compiledClass.getField(name); + field.set(compiledInstance, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new ScriptException(e); + } + } + } + + private void pullVariables(Bindings globalBindings, Bindings engineBindings) throws ScriptException { + + if (bindingStrategy != null) { + Map retrievedBindings = bindingStrategy.retrieveBindings(compiledClass, compiledInstance); + + for (Map.Entry entry : retrievedBindings.entrySet()) { + String name = entry.getKey(); + Object value = entry.getValue(); + + setBindingsValue(globalBindings, engineBindings, name, value); + } + + return; + } + + for (Field field : compiledClass.getFields()) { + try { + String name = field.getName(); + Object value = field.get(compiledInstance); + setBindingsValue(globalBindings, engineBindings, name, value); + } catch (IllegalAccessException e) { + throw new ScriptException(e); + } + } + } + + private void setBindingsValue(Bindings globalBindings, Bindings engineBindings, String name, Object value) { + if (!engineBindings.containsKey(name) && globalBindings.containsKey(name)) { + globalBindings.put(name, value); + } else { + engineBindings.put(name, value); + } + } + + private Map mergeBindings(Bindings... bindingsToMerge) { + Map variables = new HashMap<>(); + + for (Bindings bindings : bindingsToMerge) { + if (bindings != null) { + for (Map.Entry globalEntry : bindings.entrySet()) { + variables.put(globalEntry.getKey(), globalEntry.getValue()); + } + } + } + + return variables; + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaScriptEngine.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaScriptEngine.java new file mode 100644 index 0000000000000..9dbc7d1af8711 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaScriptEngine.java @@ -0,0 +1,267 @@ +package ch.obermuhlner.scriptengine.java; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; +import javax.script.SimpleBindings; +import javax.script.SimpleScriptContext; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import ch.obermuhlner.scriptengine.java.bindings.BindingStrategy; +import ch.obermuhlner.scriptengine.java.compilation.CompilationStrategy; +import ch.obermuhlner.scriptengine.java.compilation.DefaultCompilationStrategy; +import ch.obermuhlner.scriptengine.java.compilation.NoInterceptorStrategy; +import ch.obermuhlner.scriptengine.java.compilation.ScriptInterceptorStrategy; +import ch.obermuhlner.scriptengine.java.construct.ConstructorStrategy; +import ch.obermuhlner.scriptengine.java.construct.DefaultConstructorStrategy; +import ch.obermuhlner.scriptengine.java.execution.DefaultExecutionStrategy; +import ch.obermuhlner.scriptengine.java.execution.ExecutionStrategy; +import ch.obermuhlner.scriptengine.java.execution.ExecutionStrategyFactory; +import ch.obermuhlner.scriptengine.java.name.DefaultNameStrategy; +import ch.obermuhlner.scriptengine.java.name.NameStrategy; +import ch.obermuhlner.scriptengine.java.packagelisting.PackageResourceListingStrategy; + +/** + * Script engine to compile and run a Java class on the fly. + */ +public class JavaScriptEngine implements ScriptEngine, Compilable { + + private NameStrategy nameStrategy = new DefaultNameStrategy(); + private ConstructorStrategy constructorStrategy = DefaultConstructorStrategy.byDefaultConstructor(); + private ExecutionStrategyFactory executionStrategyFactory = clazz -> new DefaultExecutionStrategy(clazz); + private Isolation isolation = Isolation.CallerClassLoader; + private List compilationOptions = null; + private PackageResourceListingStrategy packageResourceListingStrategy = null; + private BindingStrategy bindingStrategy = null; + private CompilationStrategy compilationStrategy = new DefaultCompilationStrategy(); + private ScriptInterceptorStrategy scriptInterceptorStrategy = new NoInterceptorStrategy(); + + private ScriptContext context = new SimpleScriptContext(); + + private ClassLoader executionClassLoader = getClass().getClassLoader(); + + /** + * Sets the name strategy used to determine the Java class name from a script. + * + * @param nameStrategy the {@link NameStrategy} to use in this script engine + */ + public void setNameStrategy(NameStrategy nameStrategy) { + this.nameStrategy = nameStrategy; + } + + /** + * Sets the constructor strategy used to construct a Java instance of a class. + * + * @param constructorStrategy the {@link ConstructorStrategy} to use in this script engine + */ + public void setConstructorStrategy(ConstructorStrategy constructorStrategy) { + this.constructorStrategy = constructorStrategy; + } + + public void setPackageResourceListingStrategy(PackageResourceListingStrategy packageResourceListingStrategy) { + this.packageResourceListingStrategy = packageResourceListingStrategy; + } + + public void setBindingStrategy(BindingStrategy bindingStrategy) { + this.bindingStrategy = bindingStrategy; + } + + public void setCompilationStrategy(CompilationStrategy compilationStrategy) { + this.compilationStrategy = compilationStrategy; + } + + public void setScriptInterceptorStrategy(ScriptInterceptorStrategy scriptInterceptorStrategy) { + this.scriptInterceptorStrategy = scriptInterceptorStrategy; + } + + /** + * Sets the factory for the execution strategy used to execute a method of a class instance. + * + * @param executionStrategyFactory the {@link ExecutionStrategyFactory} to use in this script engine + */ + public void setExecutionStrategyFactory(ExecutionStrategyFactory executionStrategyFactory) { + this.executionStrategyFactory = executionStrategyFactory; + } + + /** + * Sets the {@link ClassLoader} used to load and execute the class. + * + * @param executionClassLoader the execution {@link ClassLoader} + */ + public void setExecutionClassLoader(ClassLoader executionClassLoader) { + this.executionClassLoader = executionClassLoader; + } + + /** + * Sets the isolation of the script. + * + * @param isolation the {@link Isolation} + */ + public void setIsolation(Isolation isolation) { + this.isolation = isolation; + } + + /** + * Sets options used in java compilation + * + * @param compilationOptions options to use in java compilation + */ + public void setCompilationOptions(List compilationOptions) { + this.compilationOptions = compilationOptions; + } + + @Override + public ScriptContext getContext() { + return context; + } + + @Override + public void setContext(ScriptContext context) { + Objects.requireNonNull(context); + this.context = context; + } + + @Override + public Bindings createBindings() { + return new SimpleBindings(); + } + + @Override + public Bindings getBindings(int scope) { + return context.getBindings(scope); + } + + @Override + public void setBindings(Bindings bindings, int scope) { + context.setBindings(bindings, scope); + } + + @Override + public void put(String key, Object value) { + getBindings(ScriptContext.ENGINE_SCOPE).put(key, value); + } + + @Override + public Object get(String key) { + return getBindings(ScriptContext.ENGINE_SCOPE).get(key); + } + + @Override + public Object eval(Reader reader) throws ScriptException { + return eval(readScript(reader)); + } + + @Override + public Object eval(String script) throws ScriptException { + return eval(script, context); + } + + @Override + public Object eval(Reader reader, ScriptContext context) throws ScriptException { + return eval(readScript(reader), context); + } + + @Override + public Object eval(String script, ScriptContext context) throws ScriptException { + return eval(script, context.getBindings(ScriptContext.ENGINE_SCOPE)); + } + + @Override + public Object eval(Reader reader, Bindings bindings) throws ScriptException { + return eval(readScript(reader), bindings); + } + + @Override + public Object eval(String script, Bindings bindings) throws ScriptException { + CompiledScript compile = compile(script); + + return compile.eval(bindings); + } + + @Override + public CompiledScript compile(Reader reader) throws ScriptException { + return compile(readScript(reader)); + } + + @Override + public JavaCompiledScript compile(String originalScript) throws ScriptException { + + String script = scriptInterceptorStrategy.intercept(originalScript); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + ClassLoader parentClassLoader = isolation == Isolation.CallerClassLoader ? executionClassLoader : null; + JavaFileManager fileManager = compilationStrategy.getJavaFileManager( + ToolProvider.getSystemJavaCompiler().getStandardFileManager(diagnostics, null, null)); + if (fileManager == null) { + fileManager = compiler.getStandardFileManager(diagnostics, null, null); + } else { + parentClassLoader = fileManager.getClassLoader(StandardLocation.CLASS_PATH); + } + MemoryFileManager memoryFileManager = new MemoryFileManager(fileManager, parentClassLoader); + + memoryFileManager.setPackageResourceListingStrategy(packageResourceListingStrategy); + + String fullClassName = nameStrategy.getFullName(script); + String simpleClassName = NameStrategy.extractSimpleName(fullClassName); + + List toCompile = compilationStrategy.getJavaFileObjectsToCompile(simpleClassName, script); + + JavaCompiler.CompilationTask task = compiler.getTask(null, memoryFileManager, diagnostics, compilationOptions, + null, toCompile); + if (!task.call()) { + String message = diagnostics.getDiagnostics().stream().map(d -> d.toString()) + .collect(Collectors.joining("\n")); + throw new ScriptException(message); + } + + ClassLoader classLoader = memoryFileManager.getClassLoader(StandardLocation.CLASS_OUTPUT); + + try { + Class clazz = classLoader.loadClass(fullClassName); + compilationStrategy.compilationResult(clazz); + Object instance = constructorStrategy.construct(clazz); + ExecutionStrategy executionStrategy = executionStrategyFactory.create(clazz); + return new JavaCompiledScript(this, clazz, instance, executionStrategy, bindingStrategy); + } catch (ClassNotFoundException e) { + throw new ScriptException(e); + } + } + + @Override + public ScriptEngineFactory getFactory() { + return new JavaScriptEngineFactory(); + } + + private String readScript(Reader reader) throws ScriptException { + try { + StringBuilder s = new StringBuilder(); + BufferedReader bufferedReader = new BufferedReader(reader); + String line; + while ((line = bufferedReader.readLine()) != null) { + s.append(line); + s.append("\n"); + } + return s.toString(); + } catch (IOException e) { + throw new ScriptException(e); + } + } + +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaScriptEngineFactory.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaScriptEngineFactory.java new file mode 100644 index 0000000000000..5b575bd6176c3 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/JavaScriptEngineFactory.java @@ -0,0 +1,101 @@ +package ch.obermuhlner.scriptengine.java; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import java.util.Arrays; +import java.util.List; + +/** + * Factory for the {@link JavaScriptEngine}. + */ +public class JavaScriptEngineFactory implements ScriptEngineFactory { + @Override + public String getEngineName() { + return "Java ScriptEngine"; + } + + @Override + public String getEngineVersion() { + return "2.0.0"; + } + + @Override + public List getExtensions() { + return Arrays.asList("java"); + } + + @Override + public List getMimeTypes() { + return Arrays.asList("text/x-java-source"); + } + + @Override + public List getNames() { + return Arrays.asList("Java", "java", "ch.obermuhlner:java-scriptengine", "obermuhlner-java"); + } + + @Override + public String getLanguageName() { + return "Java"; + } + + @Override + public String getLanguageVersion() { + return System.getProperty("java.version"); + } + + @Override + public Object getParameter(String key) { + switch (key) { + case ScriptEngine.ENGINE: + return getEngineName(); + case ScriptEngine.ENGINE_VERSION: + return getEngineVersion(); + case ScriptEngine.LANGUAGE: + return getLanguageName(); + case ScriptEngine.LANGUAGE_VERSION: + return getLanguageVersion(); + case ScriptEngine.NAME: + return getNames().get(0); + default: + return null; + } + } + + @Override + public String getMethodCallSyntax(String obj, String method, String... args) { + StringBuilder s = new StringBuilder(); + s.append(obj); + s.append("."); + s.append(method); + s.append("("); + for(int i = 0; i < args.length; i++) { + if (i > 0) { + s.append(","); + } + s.append(args[i]); + } + s.append(")"); + return s.toString(); + } + + @Override + public String getOutputStatement(String toDisplay) { + return "System.out.println(" + toDisplay + ")"; + } + + @Override + public String getProgram(String... statements) { + StringBuilder s = new StringBuilder(); + for(String statement : statements) { + s.append(statement); + s.append(";\n"); + } + return s.toString(); + } + + @Override + public ScriptEngine getScriptEngine() { + return new JavaScriptEngine(); + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/MemoryClassLoader.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/MemoryClassLoader.java new file mode 100644 index 0000000000000..f99b129e4407f --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/MemoryClassLoader.java @@ -0,0 +1,64 @@ +package ch.obermuhlner.scriptengine.java; + +import java.net.MalformedURLException; +import java.net.URL; +import java.security.CodeSource; +import java.security.Principal; +import java.security.ProtectionDomain; +import java.security.cert.Certificate; +import java.util.Map; + +/** + * A {@link ClassLoader} that loads classes from memory. + */ +public class MemoryClassLoader extends ClassLoader { + + /** + * URL used to identify the {@link CodeSource} of the {@link ProtectionDomain} used by this class loader. + * + * This is useful to identify classes loaded by this class loader in a policy file. + * + *
+    grant codeBase "jrt:/ch.obermuhlner.scriptengine.java/memory-class" {
+    permission java.lang.RuntimePermission "exitVM";
+    };
+     * 
+ */ + public static final String MEMORY_CLASS_URL = "http://ch.obermuhlner/ch.obermuhlner.scriptengine.java/memory-class"; + + private ProtectionDomain protectionDomain; + private Map mapClassBytes; + + /** + * Creates a {@link MemoryClassLoader}. + * + * @param mapClassBytes the map of class names to compiled classes + * @param parent the parent {@link ClassLoader} + */ + public MemoryClassLoader(Map mapClassBytes, ClassLoader parent) { + super(parent); + this.mapClassBytes = mapClassBytes; + + try { + URL url = new URL(MEMORY_CLASS_URL); + CodeSource codeSource = new CodeSource(url, (Certificate[]) null); + protectionDomain = new ProtectionDomain(codeSource, null, this, new Principal[0]); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + byte[] bytes = mapClassBytes.get(name); + if (bytes == null) { + return super.loadClass(name); + } + + return defineClass(name, bytes, 0, bytes.length, protectionDomain); + } + + public boolean isLoadedClass(String className) { + return mapClassBytes.containsKey(className); + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/MemoryFileManager.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/MemoryFileManager.java new file mode 100644 index 0000000000000..9910c4495abf3 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/MemoryFileManager.java @@ -0,0 +1,243 @@ +package ch.obermuhlner.scriptengine.java; + +import ch.obermuhlner.scriptengine.java.packagelisting.PackageResourceListingStrategy; +import ch.obermuhlner.scriptengine.java.util.CompositeIterator; + +import javax.tools.*; + + +import java.io.*; +import java.net.URI; +import java.net.URL; +import java.util.*; + +import static javax.tools.StandardLocation.CLASS_OUTPUT; +import static javax.tools.StandardLocation.CLASS_PATH; + +/** + * A {@link JavaFileManager} that manages some files in memory, + * delegating the other files to the parent {@link JavaFileManager}. + */ +public class MemoryFileManager extends ForwardingJavaFileManager { + + private final Map mapNameToClasses = new HashMap<>(); + private final ClassLoader parentClassLoader; + + private PackageResourceListingStrategy packageResourceListingStrategy = null; + + /** + * Creates a MemoryJavaFileManager. + * + * @param fileManager the {@link JavaFileManager} + * @param parentClassLoader the parent {@link ClassLoader} + */ + public MemoryFileManager(JavaFileManager fileManager, ClassLoader parentClassLoader) { + super(fileManager); + + this.parentClassLoader = parentClassLoader; + } + + public void setPackageResourceListingStrategy(PackageResourceListingStrategy packageResourceListingStrategy) { + this.packageResourceListingStrategy = packageResourceListingStrategy; + } + + + private Collection memoryClasses() { + return mapNameToClasses.values(); + } + + public static JavaFileObject createSourceFileObject(Object origin, String name, String code) { + return new MemoryJavaFileObject(origin, name, JavaFileObject.Kind.SOURCE, code); + } + + public ClassLoader getClassLoader(JavaFileManager.Location location) { + ClassLoader classLoader = super.getClassLoader(location); + + if (location == CLASS_OUTPUT) { + if (parentClassLoader != null) { + classLoader = parentClassLoader; + } + + Map mapNameToBytes = new HashMap<>(); + + for (ClassMemoryJavaFileObject outputMemoryJavaFileObject : memoryClasses()) { + mapNameToBytes.put( + outputMemoryJavaFileObject.getName(), + outputMemoryJavaFileObject.getBytes()); + } + + return new MemoryClassLoader(mapNameToBytes, classLoader); + } + + return classLoader; + } + + @Override + public Iterable list( + JavaFileManager.Location location, + String packageName, + Set kinds, + boolean recurse) throws IOException { + Iterable list = super.list(location, packageName, kinds, recurse); + + if (location == CLASS_OUTPUT) { + Collection generatedClasses = memoryClasses(); + return () -> new CompositeIterator( + list.iterator(), + generatedClasses.iterator()); + } + else if (location == CLASS_PATH) + { + if (packageResourceListingStrategy != null) + { + Collection resources = packageResourceListingStrategy.listResources(packageName); + + List classPathClasses = new ArrayList(); + + for (JavaFileObject jfo : list) + { + classPathClasses.add(jfo); + } + + for (String resource : resources) { + if (resource.endsWith(".class")) { + JavaFileObject javaFileObject = new ClasspathMemoryJavaFileObject(parentClassLoader, resource); + classPathClasses.add(javaFileObject); + } + } + + return classPathClasses; + } + } + + + return list; + } + + @Override + public String inferBinaryName(JavaFileManager.Location location, JavaFileObject file) { + if (file instanceof AbstractMemoryJavaFileObject) { + return file.getName(); + } else { + return super.inferBinaryName(location, file); + } + } + + @Override + public JavaFileObject getJavaFileForOutput( + JavaFileManager.Location location, + String className, + JavaFileObject.Kind kind, + FileObject sibling) + throws IOException { + if (kind == JavaFileObject.Kind.CLASS) { + ClassMemoryJavaFileObject file = new ClassMemoryJavaFileObject(className); + mapNameToClasses.put(className, file); + return file; + } + + return super.getJavaFileForOutput(location, className, kind, sibling); + } + + static abstract class AbstractMemoryJavaFileObject extends SimpleJavaFileObject { + public AbstractMemoryJavaFileObject(String name, JavaFileObject.Kind kind) { + super(URI.create("memory:///" + + name.replace('.', '/') + + kind.extension), kind); + } + } + + static class MemoryJavaFileObject extends AbstractMemoryJavaFileObject { + private final Object origin; + private final String code; + + MemoryJavaFileObject(Object origin, String className, JavaFileObject.Kind kind, String code) { + super(className, kind); + + this.origin = origin; + this.code = code; + } + + public Object getOrigin() { + return origin; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return code; + } + } + + static class ClasspathMemoryJavaFileObject extends AbstractMemoryJavaFileObject { + + String className; + String resource; + ClassLoader classLoader; + + ClasspathMemoryJavaFileObject(ClassLoader classLoader, String resource) throws IOException { + super(resource, JavaFileObject.Kind.CLASS); + + this.classLoader = classLoader; + this.resource = resource; + + className = resource.substring(0, resource.lastIndexOf(".")); + className = className.replace('/', '.'); + } + + @Override + public String getName() { + return className; + } + + @Override + public InputStream openInputStream() throws IOException { + URL url = classLoader.getResource(resource); + InputStream is = url.openStream(); + return is; + } + + @Override + public OutputStream openOutputStream() throws IOException { + return super.openOutputStream(); + } + } + + + static class ClassMemoryJavaFileObject extends AbstractMemoryJavaFileObject { + + private ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + private transient byte[] bytes = null; + + private final String className; + + public ClassMemoryJavaFileObject(String className) { + super(className, JavaFileObject.Kind.CLASS); + + this.className = className; + } + + public byte[] getBytes() { + if (bytes == null) { + bytes = byteOutputStream.toByteArray(); + byteOutputStream = null; + } + return bytes; + } + + @Override + public String getName() { + return className; + } + + @Override + public OutputStream openOutputStream() { + return byteOutputStream; + } + + @Override + public InputStream openInputStream() { + return new ByteArrayInputStream(getBytes()); + } + } + +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/bindings/BindingStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/bindings/BindingStrategy.java new file mode 100644 index 0000000000000..3bd93b7297028 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/bindings/BindingStrategy.java @@ -0,0 +1,17 @@ +package ch.obermuhlner.scriptengine.java.bindings; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The strategy used to set/get the Bindings around invoke + */ +public interface BindingStrategy { + + void associateBindings(@NonNull Class compiledClass, @NonNull Object compiledInstance, + @NonNull Map mergedBindings); + + @NonNull + Map retrieveBindings(@NonNull Class compiledClass, @NonNull Object compiledInstance); +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/CompilationStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/CompilationStrategy.java new file mode 100644 index 0000000000000..86f772da343a0 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/CompilationStrategy.java @@ -0,0 +1,44 @@ +package ch.obermuhlner.scriptengine.java.compilation; + +import java.util.List; + +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * This strategy is used to decide what to compile + */ +public interface CompilationStrategy { + + /** + * Generate a list of JavaFileObject to compile + * + * @param simpleClassName the class name of the script + * @param currentSource The current source script we want to execute + * @return A list of java file object to compile alongside the main script + */ + @NonNull + List getJavaFileObjectsToCompile(@NonNull String simpleClassName, @NonNull String currentSource); + + /** + * As the script is compiled, this is an opportunity to see if we still want to + * keep it. + * + * @param clazz + */ + default void compilationResult(Class clazz) { + } + + /** + * Get a file manager to use, during compilation, as a parent of the in-memory file manager + * + * @param parentJavaFileManager The parent javaFileManager of the returned file manager + * @return a file manager. It will be the parent of the in-memory file manager managing the script. + */ + default JavaFileManager getJavaFileManager(JavaFileManager parentJavaFileManager) { + return null; + } + +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/DefaultCompilationStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/DefaultCompilationStrategy.java new file mode 100644 index 0000000000000..86f64dac66173 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/DefaultCompilationStrategy.java @@ -0,0 +1,18 @@ +package ch.obermuhlner.scriptengine.java.compilation; + +import java.util.Collections; +import java.util.List; + +import javax.tools.JavaFileObject; + +import ch.obermuhlner.scriptengine.java.MemoryFileManager; + +public class DefaultCompilationStrategy implements CompilationStrategy { + + @Override + public List getJavaFileObjectsToCompile(String simpleClassName, String currentSource) { + return Collections + .singletonList(MemoryFileManager.createSourceFileObject(null, simpleClassName, currentSource)); + } + +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/IncrementalCompilationStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/IncrementalCompilationStrategy.java new file mode 100644 index 0000000000000..7e8a5cc17b75d --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/IncrementalCompilationStrategy.java @@ -0,0 +1,33 @@ +package ch.obermuhlner.scriptengine.java.compilation; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Stream; + +import javax.tools.JavaFileObject; + +import ch.obermuhlner.scriptengine.java.MemoryFileManager; + +public class IncrementalCompilationStrategy implements CompilationStrategy { + + Map previousFileObject = new HashMap<>(); + + JavaFileObject currentJavaFileObject; + + @Override + public List getJavaFileObjectsToCompile(String simpleClassName, String currentSource) { + currentJavaFileObject = MemoryFileManager.createSourceFileObject(null, simpleClassName, currentSource); + Stream previousFileObjects = previousFileObject.entrySet().stream() + .filter(entry -> !entry.getKey().equals(simpleClassName)) // do no keep the old file + .map(Entry::getValue); + return Stream.concat(previousFileObjects, Stream.of(currentJavaFileObject)).toList(); + } + + @Override + public void compilationResult(Class clazz) { + previousFileObject.put(clazz.getSimpleName(), currentJavaFileObject); + } + +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/NoInterceptorStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/NoInterceptorStrategy.java new file mode 100644 index 0000000000000..cab677ac3126a --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/NoInterceptorStrategy.java @@ -0,0 +1,10 @@ +package ch.obermuhlner.scriptengine.java.compilation; + +public class NoInterceptorStrategy implements ScriptInterceptorStrategy { + + @Override + public String intercept(String actualScript) { + return actualScript; + } + +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/ScriptInterceptorStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/ScriptInterceptorStrategy.java new file mode 100644 index 0000000000000..8b2424d6b8a13 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/compilation/ScriptInterceptorStrategy.java @@ -0,0 +1,10 @@ +package ch.obermuhlner.scriptengine.java.compilation; + +/** + * This strategy allows to modify a script before compiling it. + */ +public interface ScriptInterceptorStrategy { + + public String intercept(String script); + +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/ConstructorStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/ConstructorStrategy.java new file mode 100644 index 0000000000000..f490d454891bf --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/ConstructorStrategy.java @@ -0,0 +1,17 @@ +package ch.obermuhlner.scriptengine.java.construct; + +import javax.script.ScriptException; + +/** + * The strategy used to construct an instance of a {@link Class}. + */ +public interface ConstructorStrategy { + /** + * Constructs an instance of a {@link Class}. + * + * @param clazz the {@link Class} + * @return the constructed instance or {@code null} + * @throws ScriptException if the instance could not be constructed + */ + Object construct(Class clazz) throws ScriptException; +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/DefaultConstructorStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/DefaultConstructorStrategy.java new file mode 100644 index 0000000000000..2f6b752e008ca --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/DefaultConstructorStrategy.java @@ -0,0 +1,111 @@ +package ch.obermuhlner.scriptengine.java.construct; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.script.ScriptException; + +import ch.obermuhlner.scriptengine.java.util.ReflectionUtil; + +/** + * The default {@link ConstructorStrategy} implementation. + * + * This implementation has three static constructor methods to define the constructor that should be called: + *
    + *
  • {@link #byDefaultConstructor()} to call the public default no-argument constructor.
  • + *
  • {@link #byArgumentTypes(Class[], Object...)} to call the public constructor with the + * specified argument types and pass it the specified arguments.
  • + *
  • {@link #byMatchingArguments(Object...)} to call a public constructor that matches the + * specified arguments.
  • + *
+ */ +public class DefaultConstructorStrategy implements ConstructorStrategy { + + private Class[] argumentTypes; + private final Object[] arguments; + + private DefaultConstructorStrategy(Class[] argumentTypes, Object[] arguments) { + this.argumentTypes = argumentTypes; + this.arguments = arguments; + } + + @Override + public Object construct(Class clazz) throws ScriptException { + try { + Constructor constructor = findConstructor(clazz, argumentTypes, arguments); + return constructor.newInstance(arguments); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException + | NoSuchMethodException e) { + throw new ScriptException(e); + } + } + + /** + * Creates a {@link DefaultConstructorStrategy} that will call the public default no-argument constructor. + * + * @return the created {@link DefaultConstructorStrategy} + */ + public static DefaultConstructorStrategy byDefaultConstructor() { + return new DefaultConstructorStrategy(new Class[0], new Object[0]); + } + + /** + * Creates a {@link DefaultConstructorStrategy} that will call the public constructor with the + * specified argument types and passes the specified argument list. + * + * @param argumentTypes the argument types defining the constructor to call + * @param arguments the arguments to pass to the constructor (may contain {@code null}) + * @return the created {@link DefaultConstructorStrategy} + */ + public static DefaultConstructorStrategy byArgumentTypes(Class[] argumentTypes, Object... arguments) { + return new DefaultConstructorStrategy(argumentTypes, arguments); + } + + /** + * Creates a {@link DefaultConstructorStrategy} that will call a public constructor that matches the + * specified arguments. + * + * A constructor must match all specified arguments, except {@code null} values which + * match any non-primitive type. + * The conversion from object types into corresponding primitive types + * (for example {@link Integer} into {@code int}) is handled automatically. + * + * If multiple public constructors match the specified arguments the + * {@link #construct(Class)} method will throw a {@link ScriptException}. + * + * @param arguments the arguments to pass to the constructor (may contain {@code null}) + * @return the created {@link DefaultConstructorStrategy} + */ + public static DefaultConstructorStrategy byMatchingArguments(Object... arguments) { + return new DefaultConstructorStrategy(null, arguments); + } + + private Constructor findConstructor(Class clazz, Class[] argumentTypes, Object[] arguments) + throws NoSuchMethodException, ScriptException { + if (argumentTypes != null) { + return clazz.getConstructor(argumentTypes); + } + + List> matchingConstructors = new ArrayList<>(); + for (Constructor constructor : clazz.getConstructors()) { + if (Modifier.isPublic(constructor.getModifiers()) + && ReflectionUtil.matchesArguments(constructor, arguments)) { + matchingConstructors.add(constructor); + } + } + + int count = matchingConstructors.size(); + if (count == 0) { + throw new ScriptException("No constructor with matching arguments found"); + } else if (count > 1) { + throw new ScriptException("Ambiguous constructors with matching arguments found:\n" + + matchingConstructors.stream().map(Object::toString).collect(Collectors.joining("\n"))); + } + + return matchingConstructors.get(0); + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/NullConstructorStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/NullConstructorStrategy.java new file mode 100644 index 0000000000000..da13651fdcf08 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/construct/NullConstructorStrategy.java @@ -0,0 +1,16 @@ +package ch.obermuhlner.scriptengine.java.construct; + +import javax.script.ScriptException; + +/** + * A {@link ConstructorStrategy} implementation that always returns {@code null}. + * + * Used to indicate that only static methods should be called to evaluate the + * {@link ch.obermuhlner.scriptengine.java.JavaCompiledScript} holding the {@link Class}. + */ +public class NullConstructorStrategy implements ConstructorStrategy { + @Override + public Object construct(Class clazz) throws ScriptException { + return null; + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/DefaultExecutionStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/DefaultExecutionStrategy.java new file mode 100644 index 0000000000000..4b88a9fe8d86f --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/DefaultExecutionStrategy.java @@ -0,0 +1,77 @@ +package ch.obermuhlner.scriptengine.java.execution; + +import javax.script.ScriptException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * The default {@link ExecutionStrategy} implementation. + * + *
    + *
  • class implements `Supplier`: the `get()` method is called
  • + *
  • class implements `Runnable`: the `run()` method is called
  • + *
  • class has exactly one public method without arguments: call it
  • + *
+ */ +public class DefaultExecutionStrategy implements ExecutionStrategy { + + private final Class clazz; + private final Method method; + + /** + * Constructs a {@link DefaultExecutionStrategy} for the specified {@link Class}. + * + * @param clazz the {@link Class} + */ + public DefaultExecutionStrategy(Class clazz) { + method = findCallableMethod(clazz); + this.clazz = clazz; + } + + @Override + public Object execute(Object instance) throws ScriptException { + if (instance instanceof Supplier) { + Supplier supplier = (Supplier) instance; + return supplier.get(); + } + + if (instance instanceof Runnable) { + Runnable runnable = (Runnable) instance; + runnable.run(); + return null; + } + + if (method != null) { + try { + return method.invoke(instance); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new ScriptException(e); + } + } + + if (instance == null) { + throw new ScriptException("No static method found to execute of type " + clazz.getName()); + } + throw new ScriptException("No method found to execute instance of type " + clazz.getName()); + } + + private static Method findCallableMethod(Class clazz) { + List callableMethods = new ArrayList<>(); + for (Method method : clazz.getDeclaredMethods()) { + int modifiers = method.getModifiers(); + if (method.getParameterCount() == 0 && Modifier.isPublic(modifiers)) { + callableMethods.add(method); + } + } + + if (callableMethods.size() == 1) { + return callableMethods.get(0); + } + + return null; + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/ExecutionStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/ExecutionStrategy.java new file mode 100644 index 0000000000000..4ede08dd1ce85 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/ExecutionStrategy.java @@ -0,0 +1,17 @@ +package ch.obermuhlner.scriptengine.java.execution; + +import javax.script.ScriptException; + +/** + * The strategy used to execute a method on an object instance. + */ +public interface ExecutionStrategy { + /** + * Executes a method on an object instance, or a static method if the specified instance is {@code null}. + * + * @param instance the object instance to be executed or {@code null} to execute a static method + * @return the return value of the method, or {@code null} + * @throws ScriptException if no method to execute was found + */ + Object execute(Object instance) throws ScriptException; +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/ExecutionStrategyFactory.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/ExecutionStrategyFactory.java new file mode 100644 index 0000000000000..b9a2fb9a21621 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/ExecutionStrategyFactory.java @@ -0,0 +1,17 @@ +package ch.obermuhlner.scriptengine.java.execution; + +import javax.script.ScriptException; + +/** + * The factory for the execution strategy used to execute a method of a class instance. + */ +public interface ExecutionStrategyFactory { + /** + * Creates an {@link ExecutionStrategy} for the specified {@link Class}. + * + * @param clazz the {@link Class} + * @return the {@link ExecutionStrategy} + * @throws ScriptException if the {@link ExecutionStrategy} could not be created + */ + public ExecutionStrategy create(Class clazz) throws ScriptException; +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/MethodExecutionStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/MethodExecutionStrategy.java new file mode 100644 index 0000000000000..053a3ce2fb3f8 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/execution/MethodExecutionStrategy.java @@ -0,0 +1,129 @@ +package ch.obermuhlner.scriptengine.java.execution; + +import ch.obermuhlner.scriptengine.java.util.ReflectionUtil; + +import javax.script.ScriptException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The {@link ExecutionStrategy} that executes a specific method. + * + * This implementation has three static constructor methods to define the method that should be called: + *
    + *
  • {@link #byMethod(Method, Object...)} + * to call the specified method and pass it the specified arguments.
  • + *
  • {@link #byArgumentTypes(Class, String, Class[], Object...)} + * to call the public method with the + * specified argument types and pass it the specified arguments.
  • + *
  • {@link #byMatchingArguments(Class, String, Object...)} + * to call a public method that matches the specified arguments.
  • + *
+ */ +public class MethodExecutionStrategy implements ExecutionStrategy { + private Method method; + private Object[] arguments; + + private MethodExecutionStrategy(Method method, Object... arguments) { + this.method = method; + this.arguments = arguments; + } + + @Override + public Object execute(Object instance) throws ScriptException { + try { + return method.invoke(instance, arguments); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new ScriptException(e); + } + } + + /** + * Creates a {@link MethodExecutionStrategy} that will call the specified {@link Method}. + * + * @param method the {@link Method} to execute + * @param arguments the arguments to be passed to the method + * @return the value returned by the method, or {@code null} + */ + public static MethodExecutionStrategy byMethod(Method method, Object... arguments) { + return new MethodExecutionStrategy(method, arguments); + } + + /** + * Creates a {@link MethodExecutionStrategy} that will call the {@code public static void main(String[] args)} + * with the specified arguments. + * + * @param clazz the {@link Class} + * @param arguments the arguments to pass to the main method + * @return the created {@link MethodExecutionStrategy} + * @throws ScriptException if no {@code public static void main(String[] args)} method was found + */ + public static MethodExecutionStrategy byMainMethod(Class clazz, String... arguments) throws ScriptException { + try { + Method method = clazz.getMethod("main", String[].class); + return new MethodExecutionStrategy(method, (Object[]) arguments); + } catch (NoSuchMethodException e) { + throw new ScriptException(e); + } + } + + /** + * Creates a {@link MethodExecutionStrategy} that will call the public method with the + * specified argument types and passes the specified argument list. + * + * @param clazz the {@link Class} + * @param methodName the method name + * @param argumentTypes the argument types defining the constructor to call + * @param arguments the arguments to pass to the constructor (may contain {@code null}) + * @return the created {@link MethodExecutionStrategy} + * @throws ScriptException if no matching public method was found + */ + public static MethodExecutionStrategy byArgumentTypes(Class clazz, String methodName, Class[] argumentTypes, Object... arguments) throws ScriptException { + try { + Method method = clazz.getMethod(methodName, argumentTypes); + return byMethod(method, arguments); + } catch (NoSuchMethodException e) { + throw new ScriptException(e); + } + } + + /** + * Creates a {@link MethodExecutionStrategy} that will call a public method that matches the + * specified arguments. + * + * A method must match all specified arguments, except {@code null} values which + * match any non-primitive type. + * The conversion from object types into corresponding primitive types + * (for example {@link Integer} into {@code int}) is handled automatically. + * + * @param clazz the {@link Class} + * @param methodName the method name + * @param arguments the arguments to be passed to the method + * @return the created {@link MethodExecutionStrategy} + * @throws ScriptException if no matching public method was found + */ + public static MethodExecutionStrategy byMatchingArguments(Class clazz, String methodName, Object... arguments) throws ScriptException { + List matchingMethods = new ArrayList<>(); + for (Method method : clazz.getMethods()) { + if (method.getName().equals(methodName) + && (method.getModifiers() & Modifier.PUBLIC) != 0 + && ReflectionUtil.matchesArguments(method, arguments)) { + matchingMethods.add(method); + } + } + + int count = matchingMethods.size(); + if (count == 0) { + throw new ScriptException("No method '" + methodName + "' with matching arguments found"); + } else if (count > 1) { + throw new ScriptException("Ambiguous methods '" + methodName + "' with matching arguments found: " + "\n" + + matchingMethods.stream().map(Object::toString).collect(Collectors.joining("\n"))); + } + + return byMethod(matchingMethods.get(0), arguments); + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/DefaultNameStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/DefaultNameStrategy.java new file mode 100644 index 0000000000000..71f6120207d41 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/DefaultNameStrategy.java @@ -0,0 +1,36 @@ +package ch.obermuhlner.scriptengine.java.name; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.script.ScriptException; + +/** + * A {@link NameStrategy} that scans the Java script to determine the package name and class name defined in the script. + */ +public class DefaultNameStrategy implements NameStrategy { + public static final Pattern NAME_PATTERN = Pattern + .compile("public\\s+(abstract )?(class|interface|@interface)\\s+([A-Za-z][A-Za-z0-9_$]*)"); + private static final Pattern PACKAGE_PATTERN = Pattern.compile("package\\s+([A-Za-z][A-Za-z0-9_$.]*)"); + + @Override + public String getFullName(String script) throws ScriptException { + String fullPackage = null; + Matcher packageMatcher = PACKAGE_PATTERN.matcher(script); + if (packageMatcher.find()) { + fullPackage = packageMatcher.group(1); + } + + Matcher nameMatcher = NAME_PATTERN.matcher(script); + if (nameMatcher.find()) { + String name = nameMatcher.group(3); + if (fullPackage == null) { + return name; + } else { + return fullPackage + "." + name; + } + } + + throw new ScriptException("Could not determine fully qualified class name"); + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/FixNameStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/FixNameStrategy.java new file mode 100644 index 0000000000000..1fa7b8d0fe344 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/FixNameStrategy.java @@ -0,0 +1,21 @@ +package ch.obermuhlner.scriptengine.java.name; + +/** + * A {@link NameStrategy} implementation that returns a fixed name. + */ +public class FixNameStrategy implements NameStrategy { + private final String fullName; + + /** + * Constructs a {@link FixNameStrategy} with the specified fully qualified name. + * @param fullName the fully qualified class name to return + */ + public FixNameStrategy(String fullName) { + this.fullName = fullName; + } + + @Override + public String getFullName(String script) { + return fullName; + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/NameStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/NameStrategy.java new file mode 100644 index 0000000000000..e760927e12cd9 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/name/NameStrategy.java @@ -0,0 +1,45 @@ +package ch.obermuhlner.scriptengine.java.name; + +import javax.script.ScriptException; + +/** + * The strategy used to determine the name of a Java class in a script. + */ +public interface NameStrategy { + /** + * Returns the fully qualified name of the Java class in the specified script. + * + * @param script the Java script + * @return the fully qualified class name + * @throws ScriptException if no class name could be determined + */ + String getFullName(String script) throws ScriptException; + + /** + * Extracts the simple name from a fully qualified class name. + * + * @param fullName the fully qualified class name + * @return the simple class name + */ + static String extractSimpleName(String fullName) { + int lastDotIndex = fullName.lastIndexOf('.'); + if (lastDotIndex < 0) { + return fullName; + } + return fullName.substring(lastDotIndex + 1); + } + + /** + * Extracts the package name from a fully qualified class name. + * + * @param fullName the fully qualified class name + * @return the package name, {@code ""} if it is the default package + */ + static String extractPackageName(String fullName) { + int lastDotIndex = fullName.lastIndexOf('.'); + if (lastDotIndex < 0) { + return ""; + } + return fullName.substring(0, lastDotIndex); + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/packagelisting/PackageResourceListingStrategy.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/packagelisting/PackageResourceListingStrategy.java new file mode 100644 index 0000000000000..368ea13e9e467 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/packagelisting/PackageResourceListingStrategy.java @@ -0,0 +1,11 @@ +package ch.obermuhlner.scriptengine.java.packagelisting; + +import java.util.Collection; + +/** + * The strategy used to list the Java classes in a package + */ +public interface PackageResourceListingStrategy { + + Collection listResources(String packageName); +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/util/CompositeIterator.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/util/CompositeIterator.java new file mode 100644 index 0000000000000..cd0dc21ec3375 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/util/CompositeIterator.java @@ -0,0 +1,50 @@ +package ch.obermuhlner.scriptengine.java.util; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * A {@link Iterator} that will iterate over several iterators. + * + * @param the type of elements returned by this iterator + */ +public class CompositeIterator implements Iterator { + private final Iterator[] iterators; + private int iteratorIndex = 0; + + /** + * Creates a {@link CompositeIterator} over the specified iterators. + * + * @param iterators the {@link Iterator}s + */ + @SafeVarargs + public CompositeIterator(Iterator... iterators) { + this.iterators = iterators; + } + + @Override + public boolean hasNext() { + if (iteratorIndex >= iterators.length) { + return false; + } + if (iterators[iteratorIndex].hasNext()) { + return true; + } + iteratorIndex++; + + if (iteratorIndex >= iterators.length) { + return false; + } + + return iterators[iteratorIndex].hasNext(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + return iterators[iteratorIndex].next(); + } +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/util/ReflectionUtil.java b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/util/ReflectionUtil.java new file mode 100644 index 0000000000000..b527ab0ac3801 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/java/ch/obermuhlner/scriptengine/java/util/ReflectionUtil.java @@ -0,0 +1,53 @@ +package ch.obermuhlner.scriptengine.java.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +public class ReflectionUtil { + private ReflectionUtil() { + // does nothing + } + + public static boolean matchesArguments(Constructor constructor, Object[] arguments) { + return matchesArguments(constructor.getParameterTypes(), arguments); + } + + public static boolean matchesArguments(Method method, Object[] arguments) { + return matchesArguments(method.getParameterTypes(), arguments); + } + + public static boolean matchesArguments(Class[] argumentTypes, Object[] arguments) { + if (arguments.length != argumentTypes.length) { + return false; + } + + for (int i = 0; i < arguments.length; i++) { + if (arguments[i] == null) { + if (argumentTypes[i].isPrimitive()) { + return false; + } + } else { + if (!matchesType(argumentTypes[i], arguments[i].getClass())) { + return false; + } + } + } + + return true; + } + + public static boolean matchesType(Class parameterType, Class argumentType) { + if ((parameterType == int.class && argumentType == Integer.class) + || (parameterType == long.class && argumentType == Long.class) + || (parameterType == short.class && argumentType == Short.class) + || (parameterType == byte.class && argumentType == Byte.class) + || (parameterType == boolean.class && argumentType == Boolean.class) + || (parameterType == float.class && argumentType == Float.class) + || (parameterType == double.class && argumentType == Double.class) + || (parameterType == char.class && argumentType == Character.class)) { + return true; + } + return parameterType.isAssignableFrom(argumentType); + } + +} diff --git a/bundles/org.openhab.automation.java223/src/3rdparty/resources/META-INF/services/javax.script.ScriptEngineFactory b/bundles/org.openhab.automation.java223/src/3rdparty/resources/META-INF/services/javax.script.ScriptEngineFactory new file mode 100644 index 0000000000000..6058f062e3a11 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/3rdparty/resources/META-INF/services/javax.script.ScriptEngineFactory @@ -0,0 +1 @@ +ch.obermuhlner.scriptengine.java.JavaScriptEngineFactory \ No newline at end of file diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/Java223Rule.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/Java223Rule.java new file mode 100644 index 0000000000000..2c9ec3c09654a --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/Java223Rule.java @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.java223.common.BindingInjector; +import org.openhab.automation.java223.common.Java223Exception; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extract code to execute, from diverse runnable field or from a method + * + * @author Gwendal Roulleau - Initial contribution + * + */ +@NonNullByDefault +public class Java223Rule extends SimpleRule { + + private static final Logger logger = LoggerFactory.getLogger(Java223Rule.class); + + private static final Set> ACCEPTABLE_FIELD_MEMBER_CLASSES = Set.of(SimpleRule.class, Function.class, + BiFunction.class, Callable.class, Runnable.class, Consumer.class, BiConsumer.class); + + private BiFunction, @Nullable Object> codeToExecute; + + @Nullable + public Object execute(SimpleRule simpleRule, Action module, Map inputs) { + return simpleRule.execute(module, inputs); + } + + @Nullable + public Object execute(Function, Object> function, Map inputs) { + return function.apply(inputs); + } + + @Nullable + public Object execute(BiFunction, Object> function, Action module, + Map inputs) { + return function.apply(module, inputs); + } + + @Nullable + public Object execute(Callable callable) { + try { + return callable.call(); + } catch (Exception e) { + throw new Java223Exception("Cannot execute callable"); + } + } + + @Nullable + public Object execute(Runnable runnable) { + runnable.run(); + return null; + } + + @Nullable + public Object execute(Consumer> consumer, Map inputs) { + consumer.accept(inputs); + return null; + } + + @Nullable + public Object execute(BiConsumer> consumer, Action module, Map inputs) { + consumer.accept(module, inputs); + return null; + } + + /** + * Prepare some executable code from a method + * + * @param script The instance to execute the method on + * @param method The method to execute + */ + public Java223Rule(Object script, Method method) { + Parameter[] parameters = method.getParameters(); + codeToExecute = (module, inputs) -> { + try { + if (method.getParameters().length == 0) { + return method.invoke(script); + } else { + Object[] parameterValues = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + if (parameters[i].getType().equals(Action.class)) { + parameterValues[i] = module; + } else { + parameterValues[i] = BindingInjector.extractBindingValueForElement(script.getClass(), + inputs, parameters[i]); + } + } + return method.invoke(script, parameterValues); + } + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException + | SecurityException e) { + logger.error("Cannot execute method named {}", method.getName(), e); + throw new Java223Exception("Cannot execute method named " + method.getName(), e); + } + }; + } + + /** + * Prepare some executable code from a field + * + * @param script The instance to execute the method on + * @param method The field member containing some code to execute + */ + @SuppressWarnings({ "unchecked" }) + public Java223Rule(Object script, Field fieldMember) throws RuleParserException { + Class fieldType = fieldMember.getType(); + + if (ACCEPTABLE_FIELD_MEMBER_CLASSES.stream().noneMatch(clazz -> fieldType.isAssignableFrom(clazz))) { + throw new RuleParserException("Field member " + fieldMember.getName() + " cannot be of class " + fieldType + + ". Must be " + ACCEPTABLE_FIELD_MEMBER_CLASSES.stream().map(Class::getSimpleName) + .collect(Collectors.joining(" or "))); + } + + codeToExecute = (module, inputs) -> { + Object objectToExecute; + try { + objectToExecute = fieldMember.get(script); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new Java223Exception("Cannot get field member " + fieldMember.getName() + " on object of class " + + script.getClass().getName(), e); + } + if (objectToExecute == null) { + throw new Java223Exception("Field " + fieldMember.getName() + " is null. Cannot execute anything"); + } + if (objectToExecute instanceof SimpleRule simpleRule) { + return execute(simpleRule, module, inputs); + } else if (objectToExecute instanceof Function function) { + return execute(function, inputs); + } else if (objectToExecute instanceof BiFunction bifunction) { + return execute(bifunction, module, inputs); + } else if (objectToExecute instanceof Callable callable) { + return execute(callable); + } else if (objectToExecute instanceof Runnable runable) { + return execute(runable); + } else if (objectToExecute instanceof Consumer consumer) { + return execute(consumer, inputs); + } else if (objectToExecute instanceof BiConsumer biconsumer) { + return execute(biconsumer, module, inputs); + } else { + throw new Java223Exception("Wrong type of field " + fieldType + ". Should not happened"); + } + }; + } + + @SuppressWarnings("unchecked") + @Override + public Object execute(Action module, Map inputs) { + // special self reference : + ((Map) inputs).put("inputs", inputs); + // actual call : + Object value = codeToExecute.apply(module, (Map) inputs); + return value != null ? value : ""; + } +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/RuleAnnotationParser.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/RuleAnnotationParser.java new file mode 100644 index 0000000000000..439ecade17e6f --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/RuleAnnotationParser.java @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package helper.rules; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager; +import org.openhab.core.automation.util.ModuleBuilder; +import org.openhab.core.automation.util.TriggerBuilder; +import org.openhab.core.config.core.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import helper.rules.annotations.ChannelEventTrigger; +import helper.rules.annotations.CronTrigger; +import helper.rules.annotations.DateTimeTrigger; +import helper.rules.annotations.DayOfWeekCondition; +import helper.rules.annotations.EphemerisDaysetCondition; +import helper.rules.annotations.EphemerisHolidayCondition; +import helper.rules.annotations.EphemerisNotHolidayCondition; +import helper.rules.annotations.EphemerisWeekdayCondition; +import helper.rules.annotations.EphemerisWeekendCondition; +import helper.rules.annotations.GenericAutomationTrigger; +import helper.rules.annotations.GenericCompareCondition; +import helper.rules.annotations.GenericEventCondition; +import helper.rules.annotations.GenericEventTrigger; +import helper.rules.annotations.GroupStateChangeTrigger; +import helper.rules.annotations.GroupStateUpdateTrigger; +import helper.rules.annotations.ItemCommandTrigger; +import helper.rules.annotations.ItemStateChangeTrigger; +import helper.rules.annotations.ItemStateCondition; +import helper.rules.annotations.ItemStateUpdateTrigger; +import helper.rules.annotations.Rule; +import helper.rules.annotations.SystemStartlevelTrigger; +import helper.rules.annotations.ThingStatusChangeTrigger; +import helper.rules.annotations.ThingStatusUpdateTrigger; +import helper.rules.annotations.TimeOfDayCondition; +import helper.rules.annotations.TimeOfDayTrigger; + +/** + * Parse annotated method in a script and create rule accordingly + * + * @author Gwendal Roulleau - Initial contribution, based on work from Jürgen Weber and Jan N. Klug + */ +@NonNullByDefault +public class RuleAnnotationParser { + + private static Logger logger = LoggerFactory.getLogger(RuleAnnotationParser.class); + + public static final Map, String> TRIGGER_FROM_ANNOTATION = Map.ofEntries( + Map.entry(ItemCommandTrigger.class, "core.ItemCommandTrigger"), + Map.entry(ItemStateChangeTrigger.class, "core.ItemStateChangeTrigger"), + Map.entry(ItemStateUpdateTrigger.class, "core.ItemStateUpdateTrigger"), + Map.entry(GroupStateChangeTrigger.class, "core.GroupStateChangeTrigger"), + Map.entry(GroupStateUpdateTrigger.class, "core.GroupStateUpdateTrigger"), + Map.entry(ChannelEventTrigger.class, "core.ChannelEventTrigger"), + Map.entry(CronTrigger.class, "timer.GenericCronTrigger"), + Map.entry(DateTimeTrigger.class, "timer.DateTimeTrigger"), + Map.entry(TimeOfDayTrigger.class, "timer.TimeOfDayTrigger"), + Map.entry(GenericEventTrigger.class, "core.GenericEventTrigger"), + Map.entry(ThingStatusUpdateTrigger.class, "core.ThingStatusUpdateTrigger"), + Map.entry(ThingStatusChangeTrigger.class, "core.ThingStatusChangeTrigger"), + Map.entry(SystemStartlevelTrigger.class, "core.SystemStartlevelTrigger")); + + public static final Map, String> CONDITION_FROM_ANNOTATION = Map.ofEntries( + Map.entry(ItemStateCondition.class, "core.ItemStateCondition"), + Map.entry(GenericCompareCondition.class, "core.GenericCompareCondition"), + Map.entry(DayOfWeekCondition.class, "timer.DayOfWeekCondition"), + Map.entry(EphemerisWeekdayCondition.class, "ephemeris.WeekdayCondition"), + Map.entry(EphemerisWeekendCondition.class, "ephemeris.WeekendCondition"), + Map.entry(EphemerisHolidayCondition.class, "ephemeris.HolidayCondition"), + Map.entry(EphemerisNotHolidayCondition.class, "ephemeris.NotHolidayCondition"), + Map.entry(EphemerisDaysetCondition.class, "ephemeris.DaysetCondition"), + Map.entry(GenericEventCondition.class, "core.GenericEventCondition"), + Map.entry(TimeOfDayCondition.class, "core.TimeOfDayCondition")); + + @SuppressWarnings({ "unused", "null" }) + public static void parse(Object script, ScriptedAutomationManager automationManager) + throws IllegalArgumentException, IllegalAccessException, RuleParserException { + Class c = script.getClass(); + + logger.debug("Parsing: {}", c.getName()); + + List members = new ArrayList<>(); + Collections.addAll(members, c.getDeclaredFields()); + Collections.addAll(members, c.getDeclaredMethods()); + + for (AccessibleObject member : members) { + + Rule ra = member.getAnnotation(Rule.class); + if (ra == null) { + continue; + } + if (ra.disabled()) { + logger.debug("Ignoring disabled rule '{}'"); + continue; + } + + // extract an action from the annotated member + Java223Rule simpleRule; + String memberName; + if (member instanceof Field fieldMember) { + simpleRule = new Java223Rule(script, fieldMember); + memberName = fieldMember.getName(); + } else if (member instanceof Method methodMember) { + simpleRule = new Java223Rule(script, methodMember); + memberName = methodMember.getName(); + } else { + continue; + } + + // name and description + String ruleName = chooseFirstOk(ra.name(), memberName); + String ruleDescription = chooseFirstOk(ra.description(), + script.getClass().getSimpleName() + "/" + ruleName); + simpleRule.setName(ruleName); + simpleRule.setDescription(ruleDescription); + + // tags + simpleRule.setTags(Set.of(ra.tags())); + + // triggers + List triggers = new ArrayList<>(); + TRIGGER_FROM_ANNOTATION + .entrySet().stream().map(annotation -> getModuleForAnnotation(member, annotation.getKey(), + annotation.getValue(), ModuleBuilder::createTrigger, ruleName)) + .flatMap(Collection::stream).forEach(triggers::add); + Arrays.stream(member.getDeclaredAnnotationsByType(GenericAutomationTrigger.class)) + .map(annotation -> getGenericAutomationTrigger(annotation, ruleName)).forEach(triggers::add); + simpleRule.setTriggers(triggers); + + // condition + List conditions = CONDITION_FROM_ANNOTATION.entrySet().stream() + .map(annotationClazz -> getModuleForAnnotation(member, annotationClazz.getKey(), + annotationClazz.getValue(), ModuleBuilder::createCondition, ruleName)) + .flatMap(Collection::stream).collect(Collectors.toList()); + simpleRule.setConditions(conditions); + + // log everything + if (logger.isDebugEnabled()) { + logger.debug("field {}", memberName); + logger.debug("@Rule(name = {}", ruleName); + for (Trigger trigger : simpleRule.getTriggers()) { + logger.debug("Trigger(id = {}, uid = {})", trigger.getId(), trigger.getTypeUID()); + logger.debug("Configuration: {}", trigger.getConfiguration().toString()); + } + } + + // create rule + automationManager.addRule(simpleRule); + } + } + + private static String chooseFirstOk(String... choices) { + for (String choice : choices) { + if (choice == null || choice.isBlank() || choice.equals(ANNOTATION_DEFAULT)) { + continue; + } else { + return choice; + } + } + return ""; + } + + private static Trigger getGenericAutomationTrigger(GenericAutomationTrigger annotation, String ruleName) { + String typeUid = annotation.typeUid(); + Configuration configuration = new Configuration(); + for (String param : annotation.params()) { + String[] parts = param.split("="); + if (parts.length != 2) { + logger.warn("Ignoring '{}' in trigger for '{}', can not determine key and value", param, ruleName); + continue; + } + configuration.put(parts[0], parts[1]); + } + return TriggerBuilder.create().withTypeUID(typeUid).withId(sanitizeTriggerId(ruleName, annotation.hashCode())) + .withConfiguration(configuration).build(); + } + + private static List getModuleForAnnotation( + AccessibleObject accessibleObject, Class clazz, String typeUid, Supplier> builder, + String ruleName) { + T[] annotations = accessibleObject.getDeclaredAnnotationsByType(clazz); + return Arrays.stream(annotations) + .map(annotation -> builder.get().withId(sanitizeTriggerId(ruleName, annotation.hashCode())) + .withTypeUID(typeUid).withConfiguration(getAnnotationConfiguration(annotation)).build()) + .collect(Collectors.toList()); + } + + private static Configuration getAnnotationConfiguration(Annotation annotation) { + Map configuration = new HashMap<>(); + for (Method method : annotation.annotationType().getDeclaredMethods()) { + try { + if (method.getParameterCount() == 0) { + Object parameterValue = method.invoke(annotation); + if (parameterValue == null || ANNOTATION_DEFAULT.equals(parameterValue)) { + continue; + } + if (parameterValue instanceof String[]) { + configuration.put(method.getName(), Arrays.asList((String[]) parameterValue)); + } else if (parameterValue instanceof Integer) { + configuration.put(method.getName(), BigDecimal.valueOf((Integer) parameterValue)); + } else { + configuration.put(method.getName(), parameterValue); + } + } + } catch (IllegalAccessException | InvocationTargetException e) { + // ignore private fields + } + } + return new Configuration(configuration); + } + + private static String sanitizeTriggerId(String ruleName, int index) { + return (ruleName + "_" + index).replaceAll("\\.", "_"); + } +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/RuleParserException.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/RuleParserException.java new file mode 100644 index 0000000000000..c17a24bb38b79 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/RuleParserException.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package helper.rules; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class RuleParserException extends Exception { + + private static final long serialVersionUID = 5744217657057910494L; + + RuleParserException(String message, Throwable e) { + super(message, e); + } + + public RuleParserException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ChannelEventTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ChannelEventTrigger.java new file mode 100644 index 0000000000000..cc3342977e1e0 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ChannelEventTrigger.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault;; + +/** + * The {@link ChannelEventTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(ChannelEventTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ChannelEventTrigger { + String channelUID(); + + String event() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ChannelEventTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ChannelEventTriggers.java new file mode 100644 index 0000000000000..70eb279798305 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ChannelEventTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ChannelEventTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ChannelEventTriggers { + ChannelEventTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/CronTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/CronTrigger.java new file mode 100644 index 0000000000000..d5a48f4eb4760 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/CronTrigger.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CronTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(CronTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface CronTrigger { + String cronExpression(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/CronTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/CronTriggers.java new file mode 100644 index 0000000000000..f00fff5178c15 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/CronTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CronTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface CronTriggers { + CronTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DateTimeTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DateTimeTrigger.java new file mode 100644 index 0000000000000..8e92e34b9518e --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DateTimeTrigger.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link DateTimeTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(DateTimeTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface DateTimeTrigger { + String itemName(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DateTimeTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DateTimeTriggers.java new file mode 100644 index 0000000000000..b5927f2a2284f --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DateTimeTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link DateTimeTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface DateTimeTriggers { + DateTimeTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DayOfWeekCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DayOfWeekCondition.java new file mode 100644 index 0000000000000..ce34f0d08be7e --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/DayOfWeekCondition.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link DayOfWeekCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface DayOfWeekCondition { + String[] days(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisDaysetCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisDaysetCondition.java new file mode 100644 index 0000000000000..142a1c6158129 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisDaysetCondition.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EphemerisDaysetCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface EphemerisDaysetCondition { + String dayset(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisHolidayCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisHolidayCondition.java new file mode 100644 index 0000000000000..e6df9b833c5c5 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisHolidayCondition.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EphemerisHolidayCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface EphemerisHolidayCondition { +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisNotHolidayCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisNotHolidayCondition.java new file mode 100644 index 0000000000000..729b64b111a15 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisNotHolidayCondition.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EphemerisNotHolidayCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface EphemerisNotHolidayCondition { +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisWeekdayCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisWeekdayCondition.java new file mode 100644 index 0000000000000..9c0a84638936a --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisWeekdayCondition.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EphemerisWeekdayCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface EphemerisWeekdayCondition { +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisWeekendCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisWeekendCondition.java new file mode 100644 index 0000000000000..5ae5c4d1b687c --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/EphemerisWeekendCondition.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EphemerisWeekendCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface EphemerisWeekendCondition { +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericAutomationTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericAutomationTrigger.java new file mode 100644 index 0000000000000..81bebc1e9a0d6 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericAutomationTrigger.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GenericAutomationTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(GenericAutomationTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GenericAutomationTrigger { + String typeUid(); + + String[] params() default {}; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericAutomationTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericAutomationTriggers.java new file mode 100644 index 0000000000000..ce909897f227a --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericAutomationTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GenericAutomationTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GenericAutomationTriggers { + GenericAutomationTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericCompareCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericCompareCondition.java new file mode 100644 index 0000000000000..940613cd08b00 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericCompareCondition.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GenericCompareCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(GenericCompareConditions.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GenericCompareCondition { + String input(); + + String inputproperty() default ANNOTATION_DEFAULT; + + String operator(); + + String right(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericCompareConditions.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericCompareConditions.java new file mode 100644 index 0000000000000..eaa8cf20188d2 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericCompareConditions.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GenericCompareConditions} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GenericCompareConditions { + GenericCompareCondition[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventCondition.java new file mode 100644 index 0000000000000..4761ec03fe100 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventCondition.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GenericEventCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GenericEventCondition { + String topic() default ANNOTATION_DEFAULT; + + String eventType() default ANNOTATION_DEFAULT; + + String source() default ANNOTATION_DEFAULT; + + String payload() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventTrigger.java new file mode 100644 index 0000000000000..e6638cd90dcaa --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventTrigger.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GenericEventTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(GenericEventTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GenericEventTrigger { + String topic(); + + String source(); + + String types(); + + String payload(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventTriggers.java new file mode 100644 index 0000000000000..b9c5748493114 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GenericEventTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GenericEventTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GenericEventTriggers { + GenericEventTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupCommandTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupCommandTrigger.java new file mode 100644 index 0000000000000..e9818778a775c --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupCommandTrigger.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GroupCommandTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(GroupCommandTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GroupCommandTrigger { + String groupName(); + + String command() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupCommandTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupCommandTriggers.java new file mode 100644 index 0000000000000..9570a533aba59 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupCommandTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GroupCommandTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GroupCommandTriggers { + GroupCommandTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateChangeTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateChangeTrigger.java new file mode 100644 index 0000000000000..9f388596fe90b --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateChangeTrigger.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GroupStateChangeTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(GroupStateChangeTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GroupStateChangeTrigger { + String groupName(); + + String previousState() default ANNOTATION_DEFAULT; + + String state() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateChangeTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateChangeTriggers.java new file mode 100644 index 0000000000000..da9779f02ea0d --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateChangeTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GroupStateChangeTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GroupStateChangeTriggers { + GroupStateChangeTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateUpdateTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateUpdateTrigger.java new file mode 100644 index 0000000000000..9e72708ed9869 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateUpdateTrigger.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GroupStateUpdateTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(GroupStateUpdateTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GroupStateUpdateTrigger { + String groupName(); + + String state() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateUpdateTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateUpdateTriggers.java new file mode 100644 index 0000000000000..5a1902f312a90 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/GroupStateUpdateTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GroupStateUpdateTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface GroupStateUpdateTriggers { + GroupStateUpdateTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemCommandTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemCommandTrigger.java new file mode 100644 index 0000000000000..69e531c346e6c --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemCommandTrigger.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ItemCommandTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(ItemCommandTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ItemCommandTrigger { + String itemName(); + + String command() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemCommandTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemCommandTriggers.java new file mode 100644 index 0000000000000..927f98f4b8fe3 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemCommandTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ItemCommandTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ItemCommandTriggers { + ItemCommandTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateChangeTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateChangeTrigger.java new file mode 100644 index 0000000000000..a113731a876cc --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateChangeTrigger.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ItemStateChangeTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(ItemStateChangeTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ItemStateChangeTrigger { + String itemName(); + + String previousState() default ANNOTATION_DEFAULT; + + String state() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateChangeTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateChangeTriggers.java new file mode 100644 index 0000000000000..67bb3b3cd2285 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateChangeTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ItemStateChangeTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ItemStateChangeTriggers { + ItemStateChangeTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateCondition.java new file mode 100644 index 0000000000000..a03e28ffbbf54 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateCondition.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ItemStateCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(ItemStateConditions.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ItemStateCondition { + String itemName(); + + String operator(); + + String state(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateConditions.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateConditions.java new file mode 100644 index 0000000000000..6d6ad0862e9b6 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateConditions.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ItemStateConditions} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ItemStateConditions { + ItemStateCondition[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateUpdateTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateUpdateTrigger.java new file mode 100644 index 0000000000000..9c834152aeaf4 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateUpdateTrigger.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ItemStateUpdateTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(ItemStateUpdateTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ItemStateUpdateTrigger { + String itemName(); + + String state() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateUpdateTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateUpdateTriggers.java new file mode 100644 index 0000000000000..23d37cca0b917 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ItemStateUpdateTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ItemStateUpdateTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ItemStateUpdateTriggers { + ItemStateUpdateTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/Rule.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/Rule.java new file mode 100644 index 0000000000000..d22f06a89dde5 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/Rule.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package helper.rules.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Gwendal Roulleau - Initial contribution + */ + +@Retention(RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD }) +@NonNullByDefault +public @interface Rule { + + String name() default ANNOTATION_DEFAULT; + + String description() default ANNOTATION_DEFAULT; + + String[] tags() default {}; + + boolean disabled() default false; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/SystemStartlevelTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/SystemStartlevelTrigger.java new file mode 100644 index 0000000000000..d4f83851cce1b --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/SystemStartlevelTrigger.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SystemStartlevelTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface SystemStartlevelTrigger { + int startlevel(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusChangeTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusChangeTrigger.java new file mode 100644 index 0000000000000..2f93c989bce51 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusChangeTrigger.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ThingStatusChangeTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(ThingStatusChangeTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ThingStatusChangeTrigger { + String thingUID(); + + String status() default ANNOTATION_DEFAULT; + + String previousStatus() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusChangeTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusChangeTriggers.java new file mode 100644 index 0000000000000..9f8d4504f6279 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusChangeTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ThingStatusChangeTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ThingStatusChangeTriggers { + ThingStatusChangeTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusUpdateTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusUpdateTrigger.java new file mode 100644 index 0000000000000..567f7307a27b5 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusUpdateTrigger.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ThingStatusUpdateTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(ThingStatusUpdateTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ThingStatusUpdateTrigger { + String thingUID(); + + String status() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusUpdateTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusUpdateTriggers.java new file mode 100644 index 0000000000000..39a1580648108 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/ThingStatusUpdateTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ThingStatusUpdateTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface ThingStatusUpdateTriggers { + ThingStatusUpdateTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayCondition.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayCondition.java new file mode 100644 index 0000000000000..5dfe94d46dba1 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayCondition.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TimeOfDayCondition} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface TimeOfDayCondition { + String startTime() default ANNOTATION_DEFAULT; + + String endTime() default ANNOTATION_DEFAULT; +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayTrigger.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayTrigger.java new file mode 100644 index 0000000000000..46a9e7c3d46af --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayTrigger.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TimeOfDayTrigger} + * + * @author Jan N. Klug - Initial contribution + */ +@Repeatable(TimeOfDayTriggers.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface TimeOfDayTrigger { + String time(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayTriggers.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayTriggers.java new file mode 100644 index 0000000000000..3f186d9855de9 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/annotations/TimeOfDayTriggers.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-2024 Contributors to the SmartHome/J project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TimeOfDayTriggers} + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +@NonNullByDefault +public @interface TimeOfDayTriggers { + TimeOfDayTrigger[] value(); +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ChannelEvent.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ChannelEvent.java new file mode 100644 index 0000000000000..6068ac9925b4e --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ChannelEvent.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.eventinfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.java223.common.InjectBinding; + +/** + * @author Gwendal Roulleau - Initial contribution + * DTO object to facilitate input injection when used as an argument in a rule annotated method + */ +@NonNullByDefault +public class ChannelEvent extends EventInfo { + + @InjectBinding + protected @NonNullByDefault({}) String channelUUID; + + @InjectBinding + protected @NonNullByDefault({}) String event; + + public String getChannelUUID() { + return channelUUID; + } + + public String getEvent() { + return event; + } + +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/EventInfo.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/EventInfo.java new file mode 100644 index 0000000000000..3bb7c62819b71 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/EventInfo.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.eventinfo; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.java223.common.InjectBinding; + +/** + * Base class : DTO object to facilitate input injection when used as an argument in a rule annotated method + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public abstract class EventInfo { + + @InjectBinding() + protected @NonNullByDefault({}) Map inputs; + + public Map getAllInputs() { + return inputs; + } +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemCommand.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemCommand.java new file mode 100644 index 0000000000000..37eb4cdc39f7c --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemCommand.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.eventinfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.core.types.Command; + +/** + * @author Gwendal Roulleau - Initial contribution + * DTO object to facilitate input injection when used as an argument in a rule annotated method + */ +@NonNullByDefault +public class ItemCommand extends EventInfo { + + @InjectBinding(named = "event.itemName") + protected @NonNullByDefault({}) String itemName; + + @InjectBinding + protected @NonNullByDefault({}) Command command; + + public String getItemName() { + return itemName; + } + + public Command getCommand() { + return command; + } +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemStateChange.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemStateChange.java new file mode 100644 index 0000000000000..ad800ecd2c7d3 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemStateChange.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.eventinfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.core.types.State; + +/** + * @author Gwendal Roulleau - Initial contribution + * DTO object to facilitate input injection when used as an argument in a rule annotated method + */ +@NonNullByDefault +public class ItemStateChange extends EventInfo { + + @InjectBinding(named = "event.itemName") + protected @NonNullByDefault({}) String itemName; + + @InjectBinding() + protected @NonNullByDefault({}) State oldState; + + @InjectBinding() + protected @NonNullByDefault({}) State newState; + + public String getItemName() { + return itemName; + } + + public State getOldState() { + return oldState; + } + + public State getNewState() { + return newState; + } +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemStateUpdate.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemStateUpdate.java new file mode 100644 index 0000000000000..1b25804e766b5 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ItemStateUpdate.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.eventinfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.core.types.State; + +/** + * @author Gwendal Roulleau - Initial contribution + * DTO object to facilitate input injection when used as an argument in a rule annotated method + */ +@NonNullByDefault +public class ItemStateUpdate extends EventInfo { + + @InjectBinding(named = "event.itemName") + protected @NonNullByDefault({}) String itemName; + + @InjectBinding(named = "event.itemState") + protected @NonNullByDefault({}) State state; + + public String getItemName() { + return itemName; + } + + public State getState() { + return state; + } +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ThingStatusChange.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ThingStatusChange.java new file mode 100644 index 0000000000000..735d748b42177 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ThingStatusChange.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.eventinfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; + +/** + * @author Gwendal Roulleau - Initial contribution + * DTO object to facilitate input injection when used as an argument in a rule annotated method + */ +@NonNullByDefault +public class ThingStatusChange extends EventInfo { + + @InjectBinding(named = "event.thingUID") + protected @NonNullByDefault({}) ThingUID thingUID; + + @InjectBinding + protected @NonNullByDefault({}) ThingStatus oldStatus; + + @InjectBinding + protected @NonNullByDefault({}) ThingStatus newStatus; + + public ThingUID getThingUID() { + return thingUID; + } + + public ThingStatus getOldStatus() { + return oldStatus; + } + + public ThingStatus getNewStatus() { + return newStatus; + } +} diff --git a/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ThingStatusUpdate.java b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ThingStatusUpdate.java new file mode 100644 index 0000000000000..9049e31060694 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/helper/java/helper/rules/eventinfo/ThingStatusUpdate.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package helper.rules.eventinfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; + +/** + * @author Gwendal Roulleau - Initial contribution + * DTO object to facilitate input injection when used as an argument in a rule annotated method + */ +@NonNullByDefault +public class ThingStatusUpdate extends EventInfo { + + @InjectBinding(named = "event.thingUID") + protected @NonNullByDefault({}) ThingUID thingUID; + + @InjectBinding + protected @NonNullByDefault({}) ThingStatus status; + + public ThingUID getThingUID() { + return thingUID; + } + + public ThingStatus getStatus() { + return status; + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/feature/feature.xml b/bundles/org.openhab.automation.java223/src/main/feature/feature.xml new file mode 100644 index 0000000000000..6bb3473cdbbaa --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.automation.java223/${project.version} + + diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/BindingInjector.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/BindingInjector.java new file mode 100644 index 0000000000000..453fa370d9edd --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/BindingInjector.java @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.common; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.java223.internal.strategy.jarloader.JarClassLoader; +import org.openhab.core.automation.module.script.ScriptExtensionManagerWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.obermuhlner.scriptengine.java.MemoryClassLoader; + +/** + * Injecting value from binding for script execution + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class BindingInjector { + + private static final Logger logger = LoggerFactory.getLogger(BindingInjector.class); + + /** + * Smart injection of bindings value into an object. + * + * @param sourceScriptClass The Script class initiating the execution + * @param bindings a bindings maps with value to inject + * @param objectToInjectInto An object. Its fields will be filled with value from the + * bindings, if a match is found + */ + public static void injectBindingsInto(Class sourceScriptClass, Map bindings, + Object objectToInjectInto) { + try { + injectBindingsInto(sourceScriptClass, bindings, objectToInjectInto, new HashMap<>()); + } catch (IllegalAccessException | IllegalArgumentException | SecurityException | InstantiationException + | InvocationTargetException e) { + logger.error("Cannot inject bindings or libs", e); + } + } + + private static void injectBindingsInto(Class sourceScriptClass, Map bindings, + Object objectToInjectInto, Map, Object> libAlreadyInstanciated) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + + Class clazz = objectToInjectInto.getClass(); + + for (Field field : getAllFields(clazz)) { + Object valueToInject = extractBindingValueForElement(sourceScriptClass, bindings, field, + libAlreadyInstanciated); + if (valueToInject != null) { + field.setAccessible(true); + field.set(objectToInjectInto, valueToInject); + } + } + } + + /** + * Search what to inject into an element. + * Find a library, or compute a name to use as a key, then use this key to search a value in the bindings data + * + * @param sourceScriptClass The Script initiating the execution + * @param bindings a map where to find the data to inject + * @param annotatedElement the field/parameter element to inject value into + **/ + public static @Nullable Object extractBindingValueForElement(Class sourceScriptClass, + Map bindings, AnnotatedElement annotatedElement) { + try { + return extractBindingValueForElement(sourceScriptClass, bindings, annotatedElement, new HashMap<>()); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new Java223Exception("Cannot extract binding value for an element", e); + } + } + + @SuppressWarnings({ "null", "unused" }) + private static @Nullable Object extractBindingValueForElement(Class sourceScript, Map bindings, + AnnotatedElement annotatedElement, Map, Object> libAlreadyInstanciated) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + + Class fieldType; + String codeName; + if (annotatedElement instanceof Parameter parameter) { + fieldType = parameter.getType(); + codeName = parameter.getName(); + } else if (annotatedElement instanceof Field field) { + fieldType = field.getType(); + codeName = field.getName(); + } else { + logger.warn("Cannot check target class for parameter. Only Parameter or Field accepted. Cannot inject."); + return null; + } + + // step zero : exclusion case + InjectBinding injectBindingAnnotation = annotatedElement.getAnnotation(InjectBinding.class); + if (injectBindingAnnotation != null && !injectBindingAnnotation.enable()) { + return null; + } + + // first, special case, is the field a library ? + if (containsLibrary(sourceScript, fieldType.getName())) { // it's a library + InjectBinding libraryAnnotation = fieldType.getAnnotation(InjectBinding.class); + if (libraryAnnotation != null && !libraryAnnotation.enable()) { // but it's disabled at class level + // no injection + return null; + } + // has it already been instantiated and stored in the store? + Object valueToInject = libAlreadyInstanciated.get(fieldType); + if (valueToInject == null) { // not instantiated, create it + Constructor[] constructors = fieldType.getDeclaredConstructors(); + // use the empty constructor if available, or the first one + Constructor constructor = Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0) + .findFirst().orElseGet(() -> constructors[0]); + Object[] parameterValues = getParameterValuesFor(sourceScript, constructor, bindings, + libAlreadyInstanciated); + valueToInject = constructor.newInstance(parameterValues); + if (valueToInject != null) { + // store it to avoid multiple instantiation + libAlreadyInstanciated.put(fieldType, valueToInject); + // and then also use injection into it + injectBindingsInto(sourceScript, bindings, valueToInject, libAlreadyInstanciated); + } + } + if (valueToInject != null) { + return valueToInject; + } + } + + // second. It's not a library, so search value in bindings map. + // Choose a name to search as a key in the binding map + // the name can be a path inside the object + Queue namePath = new LinkedList<>(); + String named; + if (injectBindingAnnotation != null + && !injectBindingAnnotation.named().equals(Java223Constants.ANNOTATION_DEFAULT)) { + named = injectBindingAnnotation.named(); + } else { + named = codeName; + } + Arrays.stream(named.split("\\.")).forEach(namePath::add); + + // third, choose where to look : in bindings, or deeper, in a preset : + Object value = bindings; + if (injectBindingAnnotation != null + && !injectBindingAnnotation.preset().equals(Java223Constants.ANNOTATION_DEFAULT)) { + ScriptExtensionManagerWrapper se = (ScriptExtensionManagerWrapper) bindings.get("scriptExtension"); + if (se != null) { + Map presetMap = se.importPreset(injectBindingAnnotation.preset()); + if (presetMap != null) { + value = presetMap; + } else { + logger.warn("Cannot find the preset {} for the named parameter {}", + injectBindingAnnotation.preset(), named); + } + } else { + logger.warn("Cannot find scriptExtension in bindings. Should not happen"); + } + } + + // fourth, browse deep inside the object if there is a path to traverse + while (!namePath.isEmpty()) { + if (value == null) { + logger.debug("Find null value for the path {}", named); + break; + } + if (value instanceof Map elementToParseAsMap) { + String key = namePath.poll(); + value = elementToParseAsMap.get(key); + if (value == null) { + logger.debug("Cannot find an element with the key {}", key); + } + } else { + Field targetField; + try { + String namePart = namePath.poll(); + if (namePart != null) { + targetField = getFieldDeep(value.getClass(), namePart); + targetField.setAccessible(true); + value = targetField.get(value); + } else { + logger.warn("Cannot map a value to the path {}", named); + value = null; + break; + } + } catch (NoSuchFieldException | SecurityException e) { + logger.warn("Cannot map a value to the path {}", named); + value = null; + break; + } + } + } + + // fifth, check if it is mandatory + if (value == null && injectBindingAnnotation != null && injectBindingAnnotation.mandatory()) { + throw new Java223Exception("There is no value found for parameter/field named " + named + + ", but it is mandatory. We cannot inject it"); + } else if (value == null) { + return null; + } + + // six, check class compatibility + if (!fieldType.isAssignableFrom(value.getClass())) { + logger.warn( + "Parameter/field entry {} is of class {} and not assignable to type {}. Did you use a reserved variable name ?", + named, value.getClass().getName(), fieldType.getName()); + } + return value; + } + + private static boolean containsLibrary(Class sourceScriptClass, String name) { + // scripts are constructed by the Java223Strategy and by JavaScriptEngine + // we know that the ClassLoader is a MemoryClassLoader (contains all .java lib + the script) + // and that the parent is a JarClassLoader (contains all .jar lib). + // so we ask them if they loaded the class themselves + var memoryClassLoader = (MemoryClassLoader) Optional.ofNullable(sourceScriptClass.getClassLoader()) + .orElseThrow(() -> new IllegalArgumentException("ClassLoader cannot be null")); + var parentJarClassLoader = (JarClassLoader) Optional.ofNullable(memoryClassLoader.getParent()) + .orElseThrow(() -> new IllegalArgumentException("ClassLoader cannot be null")); + return parentJarClassLoader.isLoadedClass(name) || memoryClassLoader.isLoadedClass(name); + } + + /** + * Find the appropriate parameters value in the bindings map, for the executable to run. + * + * @param sourceScriptClass the source script class + * @param executable Method or constructor + * @param bindings The map used to search the appropriate value to inject + * @param libAlreadyInstanciated To avoid looping the instantiation of libraries + * @return + * @throws InstantiationException + * @throws IllegalAccessException + * @throws IllegalArgumentException + * @throws InvocationTargetException + */ + public static Object[] getParameterValuesFor(Class sourceScriptClass, Executable executable, + Map bindings, @Nullable Map, Object> libAlreadyInstanciated) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + Parameter[] parameters = executable.getParameters(); + Object[] parameterValues = new Object[parameters.length]; + Map, Object> libAlreadyInstanciatedLocal = libAlreadyInstanciated != null ? libAlreadyInstanciated + : new HashMap<>(); + for (int i = 0; i < parameters.length; i++) { + parameterValues[i] = extractBindingValueForElement(sourceScriptClass, bindings, parameters[i], + libAlreadyInstanciatedLocal); + } + return parameterValues; + } + + private static Field getFieldDeep(Class _clazz, String fieldName) throws NoSuchFieldException { + Class clazz = _clazz; + while (clazz != null && clazz != Object.class) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException | SecurityException e) { + } + clazz = clazz.getSuperclass(); + } + throw new NoSuchFieldException(); + } + + private static Set getAllFields(Class type) { + Set fields = new HashSet(); + for (Class c = type; c != null; c = c.getSuperclass()) { + fields.addAll(Arrays.asList(c.getDeclaredFields())); + } + return fields; + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/InjectBinding.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/InjectBinding.java new file mode 100644 index 0000000000000..15d0d17acce66 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/InjectBinding.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.common; + +import static org.openhab.automation.java223.common.Java223Constants.ANNOTATION_DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This annotation tags fields with an injection intent and related details. + * + * @author Gwendal Roulleau - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE }) +@NonNullByDefault +public @interface InjectBinding { + + /** + * Prevent injection. Useful if you want to do it yourself. + * + * @return + */ + public boolean enable() default true; + + /** + * If set, use this name. Else try to get the name from the code. + * + * @return + */ + public String named() default ANNOTATION_DEFAULT; + + /** + * If set, instructs the framework to get value inside a preset + * + * @return + */ + public String preset() default ANNOTATION_DEFAULT; + + /** + * If true and no value to inject is found, the binding process will fail with an exception. + * If false, null value are allowed. + * + * @return + */ + public boolean mandatory() default true; +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/Java223Constants.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/Java223Constants.java new file mode 100644 index 0000000000000..34aab07092307 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/Java223Constants.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.common; + +import java.nio.file.Path; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.OpenHAB; + +/** + * Constants. + * + * @author Gwendal Roulleau - initial contribution + * + */ +@NonNullByDefault +public class Java223Constants { + + public static final String JAVA_FILE_TYPE = "java"; + public static final String JAR_FILE_TYPE = "jar"; + public static final String METADATA_REGISTRY = "metadataRegistry"; + public static final String RULE_MANAGER = "ruleManager"; + public static final String THING_MANAGER = "thingManager"; + + public static final String ANNOTATION_DEFAULT = "\u0002\u0003"; + + public static final Path LIB_DIR = Path.of(OpenHAB.getConfigFolder(), "automation", "lib", JAVA_FILE_TYPE); +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/Java223Exception.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/Java223Exception.java new file mode 100644 index 0000000000000..524150ae5779d --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/Java223Exception.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.common; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Unchecked exception wrapper + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class Java223Exception extends RuntimeException { + + private static final long serialVersionUID = 7608058563887813804L; + + public Java223Exception(String message, Exception source) { + super(message, source); + } + + public Java223Exception(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ReuseScriptInstance.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ReuseScriptInstance.java new file mode 100644 index 0000000000000..0d04dfad941d9 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ReuseScriptInstance.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.common; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Mark a class candidate for a singleton instantiation. + * The java223 module will try to use an already created instance + * instead of creating one on each execution. + * The instance will be searched in the cache. + * + * @author Gwendal Roulleau - Initial contribution + */ +@Retention(RUNTIME) +@Target({ ElementType.TYPE }) +@NonNullByDefault +public @interface ReuseScriptInstance { + boolean value() default true; +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/RunScript.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/RunScript.java new file mode 100644 index 0000000000000..ec4136dffe329 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/RunScript.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.common; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Mark method to be run by the java223 script engine + * + * @author Gwendal Roulleau - Initial contribution + */ +@Retention(RUNTIME) +@Target({ ElementType.METHOD }) +@NonNullByDefault +public @interface RunScript { +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ScriptLoadedTrigger.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ScriptLoadedTrigger.java new file mode 100644 index 0000000000000..807eec5dbfd44 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ScriptLoadedTrigger.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A method annotated with this will be executed when the script is loaded + * + * @author Gwendal Roulleau - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +@NonNullByDefault +public @interface ScriptLoadedTrigger { +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ScriptUnloadedTrigger.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ScriptUnloadedTrigger.java new file mode 100644 index 0000000000000..4409212fd04a2 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/common/ScriptUnloadedTrigger.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A method annotated with this will be executed when the script is unloaded + * + * @author Gwendal Roulleau - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +@NonNullByDefault +public @interface ScriptUnloadedTrigger { +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223CompiledScriptCache.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223CompiledScriptCache.java new file mode 100644 index 0000000000000..1562a9a289a0c --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223CompiledScriptCache.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal; + +import java.nio.file.Path; + +import javax.script.ScriptException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.service.WatchService; +import org.openhab.core.service.WatchService.Kind; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import ch.obermuhlner.scriptengine.java.JavaCompiledScript; + +/** + * This class caches compiled scripts + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class Java223CompiledScriptCache implements WatchService.WatchEventListener { + + private int cacheSize; + + private Cache cache; + + public Java223CompiledScriptCache(int cacheSize) { + super(); + this.cacheSize = cacheSize; + cache = Caffeine.newBuilder().maximumSize(cacheSize).build(); + } + + /** + * Recreate cache with a new size + * + * @param scriptCacheSize + */ + public void setCacheSize(Integer newCacheSize) { + if (!newCacheSize.equals(cacheSize)) { + this.cacheSize = newCacheSize; + cache = Caffeine.newBuilder().maximumSize(cacheSize).build(); + } + } + + @SuppressWarnings({ "null", "unused" }) // yes it can be null, no there is no dead code + public Java223CompiledScriptInstanceWrapper getOrCompile(String script, Compiler compiler) throws ScriptException { + if (cacheSize <= 0) { // no cache + // Create our wrapper directly + return new Java223CompiledScriptInstanceWrapper(compiler.compile(script).getCompiledClass()); + } + Java223CompiledScriptInstanceWrapper wrapper = cache.getIfPresent(script); + if (wrapper == null) { + wrapper = new Java223CompiledScriptInstanceWrapper(compiler.compile(script).getCompiledClass()); + cache.put(script, wrapper); + } + return wrapper; + } + + public static interface Compiler { + public JavaCompiledScript compile(String script) throws ScriptException; + } + + /** + * If a change is detected somewhere in the libraries, + * then we invalidate all cache + */ + @Override + public void processWatchEvent(Kind kind, Path path) { + cache.invalidateAll(); + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223CompiledScriptInstanceWrapper.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223CompiledScriptInstanceWrapper.java new file mode 100644 index 0000000000000..398198d0714b3 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223CompiledScriptInstanceWrapper.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.java223.common.Java223Exception; + +/** + * Custom java compiled script instance wrapping additional informations + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class Java223CompiledScriptInstanceWrapper { + + @Nullable + private Map bindings; + + @Nullable + private Object wrappedScriptInstance; + + private Class wrappedScriptClass; + + public Java223CompiledScriptInstanceWrapper(Class wrappedScriptClass) { + this.wrappedScriptClass = wrappedScriptClass; + } + + public Map getBindings() { + var localBindings = bindings; + if (localBindings != null) { + return localBindings; + } else { + throw new Java223Exception("Getting bindings before being set ! Should not happened"); + } + } + + public void setBindings(@Nullable Map bindings) { + this.bindings = bindings; + } + + @Nullable + public Object getWrappedScriptInstance() { + return wrappedScriptInstance; + } + + public void setWrappedScriptInstance(Object wrappedScriptInstance) { + this.wrappedScriptInstance = wrappedScriptInstance; + } + + public Class getCompiledClass() { + return wrappedScriptClass; + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223ScriptEngine.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223ScriptEngine.java new file mode 100644 index 0000000000000..19f93f91888e5 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223ScriptEngine.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import javax.script.Invocable; +import javax.script.ScriptException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.java223.common.ScriptLoadedTrigger; +import org.openhab.automation.java223.common.ScriptUnloadedTrigger; +import org.openhab.automation.java223.internal.strategy.Java223Strategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.obermuhlner.scriptengine.java.JavaCompiledScript; +import ch.obermuhlner.scriptengine.java.JavaScriptEngine; + +/** + * This class adds a cache for compiled script to Obermuhlner's base class. + * This class also adds the Invocable aspect to the JavaScriptEngine. The Invocable aspect adds the ability to be called + * when loaded and unloaded script event are triggered. + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class Java223ScriptEngine extends JavaScriptEngine implements Invocable { + private final Logger logger = LoggerFactory.getLogger(Java223ScriptEngine.class); + + private @Nullable JavaCompiledScript lastCompiledScript; + + private Java223CompiledScriptCache cache; + + private Java223Strategy java223Strategy; + + public Java223ScriptEngine(Java223CompiledScriptCache cache, Java223Strategy java223Strategy) { + super(); + this.cache = cache; + this.java223Strategy = java223Strategy; + setExecutionStrategyFactory(java223Strategy); + setBindingStrategy(java223Strategy); + setCompilationStrategy(java223Strategy); + setConstructorStrategy(java223Strategy); + } + + @Override + public JavaCompiledScript compile(@Nullable String script) throws ScriptException { + try { + if (script == null) { + throw new ScriptException("script cannot be null"); + } + // reuse the original compilation class, potentially cached + Java223CompiledScriptInstanceWrapper compiledScriptInstanceWrapper = cache.getOrCompile(script, + super::compile); + // then rebuild a new JavaCompiled object + JavaCompiledScript localCompiledScript = new JavaCompiledScript(this, + compiledScriptInstanceWrapper.getCompiledClass(), compiledScriptInstanceWrapper, java223Strategy, + java223Strategy); + lastCompiledScript = localCompiledScript; + return localCompiledScript; + + } catch (NoClassDefFoundError e) { + throw new ScriptException("NoClassDefFoundError: " + e.getMessage()); + } + } + + @Override + public @Nullable Object invokeMethod(@Nullable Object o, @Nullable String name, Object @Nullable... args) + throws NoSuchMethodException { + throw new NoSuchMethodException("not implemented"); + } + + @Override + public @Nullable Object invokeFunction(@Nullable String name, Object @Nullable... args) throws ScriptException { + + // here we assume (from OpenHAB usual behavior) that the script engine served only once and so the wanted + // compiled script is the last (and only) one + JavaCompiledScript compiledScript = this.lastCompiledScript; + if (compiledScript == null || name == null) { + return null; + } + + Class annotation; + switch (name) { + case "scriptLoaded": + annotation = ScriptLoadedTrigger.class; + break; + case "scriptUnloaded": + annotation = ScriptUnloadedTrigger.class; + break; + default: + throw new ScriptException(name + " is not an allowed method in java223"); + } + + Java223CompiledScriptInstanceWrapper wrapperInstance = (Java223CompiledScriptInstanceWrapper) compiledScript + .getCompiledInstance(); + Object compiledInstance = wrapperInstance.getWrappedScriptInstance(); + for (Method method : wrapperInstance.getCompiledClass().getMethods()) { + Annotation scriptLoadedOrUnloadedAnnotation = method.getAnnotation(annotation); + if (scriptLoadedOrUnloadedAnnotation != null) { + if (method.getParameters().length != 0) { + throw new ScriptException("Method " + method.getName() + + " called by ScriptLoaded/ScriptUnloaded trigger should not have any argument"); + } else { + try { + if (Modifier.isStatic(method.getModifiers())) { + method.invoke(null); + } else { + if (compiledInstance == null) { + logger.debug( + "Calling ScriptLoaded/ScriptUnloaded {} method from a script not yet instanciated is ignored. Use a static modifier", + method.getName()); + } else { + method.invoke(compiledInstance); + } + } + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + logger.warn("Method {} cannot be called by ScriptLoaded/ScriptUnloaded trigger", + method.getName()); + throw new ScriptException(e); + } + } + } + } + return null; + } + + @Override + @SuppressWarnings("null") + public T getInterface(@Nullable Class clazz) { + return null; + } + + @Override + @SuppressWarnings("null") + public T getInterface(@Nullable Object o, @Nullable Class clazz) { + return null; + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223ScriptEngineFactory.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223ScriptEngineFactory.java new file mode 100644 index 0000000000000..8608992e14b54 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/Java223ScriptEngineFactory.java @@ -0,0 +1,321 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal; + +import static org.openhab.automation.java223.common.Java223Constants.LIB_DIR; + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.script.ScriptEngine; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.java223.common.Java223Constants; +import org.openhab.automation.java223.common.Java223Exception; +import org.openhab.automation.java223.internal.codegeneration.DependencyGenerator; +import org.openhab.automation.java223.internal.codegeneration.SourceGenerator; +import org.openhab.automation.java223.internal.codegeneration.SourceWriter; +import org.openhab.automation.java223.internal.strategy.Java223Strategy; +import org.openhab.automation.java223.internal.strategy.ScriptWrappingStrategy; +import org.openhab.core.automation.RuleManager; +import org.openhab.core.automation.module.script.ScriptEngineFactory; +import org.openhab.core.config.core.ConfigParser; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventSubscriber; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.MetadataRegistry; +import org.openhab.core.items.events.ItemAddedEvent; +import org.openhab.core.items.events.ItemRemovedEvent; +import org.openhab.core.service.WatchService; +import org.openhab.core.thing.ThingManager; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.events.ThingAddedEvent; +import org.openhab.core.thing.events.ThingRemovedEvent; +import org.openhab.core.thing.events.ThingStatusInfoChangedEvent; +import org.osgi.framework.BundleContext; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.obermuhlner.scriptengine.java.JavaScriptEngine; +import ch.obermuhlner.scriptengine.java.JavaScriptEngineFactory; +import ch.obermuhlner.scriptengine.java.compilation.ScriptInterceptorStrategy; +import ch.obermuhlner.scriptengine.java.packagelisting.PackageResourceListingStrategy; + +/** + * This is an implementation of a {@link ScriptEngineFactory} for Java + * + * @author Gwendal Roulleau - Initial contribution + */ +@Component(service = { ScriptEngineFactory.class, Java223ScriptEngineFactory.class, + EventSubscriber.class }, configurationPid = "automation.java223") +@NonNullByDefault +public class Java223ScriptEngineFactory extends JavaScriptEngineFactory + implements ScriptEngineFactory, EventSubscriber { + + private static final Logger logger = LoggerFactory.getLogger(Java223ScriptEngineFactory.class); + + private final BundleWiring bundleWiring; + private final BundleContext bundleContext; + + private final PackageResourceListingStrategy osgiPackageResourceListingStrategy; + private final Java223Strategy java223Strategy; + private final ScriptInterceptorStrategy scriptWrappingStrategy; + private Java223CompiledScriptCache compiledScriptCache; + + private final WatchService watchService; + + private final SourceGenerator sourceGenerator; + private final SourceWriter classWriter; + private final DependencyGenerator dependencyGenerator; + + private static final Set INITIALIZED = Set.of(ThingStatus.ONLINE, ThingStatus.OFFLINE, + ThingStatus.UNKNOWN); + private static final Set ACTION_EVENTS = Set.of(ThingStatusInfoChangedEvent.TYPE); + private static final Set ITEM_EVENTS = Set.of(ItemAddedEvent.TYPE, ItemRemovedEvent.TYPE); + private static final Set THING_EVENTS = Set.of(ThingAddedEvent.TYPE, ThingRemovedEvent.TYPE); + private static final Set EVENTS = Stream.of(ACTION_EVENTS, ITEM_EVENTS, THING_EVENTS).flatMap(Set::stream) + .collect(Collectors.toSet()); + + @Activate + public Java223ScriptEngineFactory(BundleContext bundleContext, Map properties, + @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService, + @Reference ItemRegistry itemRegistry, @Reference ThingRegistry thingRegistry) { + + try { + Files.createDirectories(LIB_DIR); + } catch (IOException e) { + logger.warn("Failed to create directory '{}': {}", LIB_DIR, e.getMessage()); + throw new IllegalStateException("Failed to initialize lib folder."); + } + + this.bundleContext = bundleContext; + this.bundleWiring = bundleContext.getBundle().adapt(BundleWiring.class); + + String additionalBundlesConfig = ConfigParser.valueAsOrElse(properties.get("additionalBundles"), String.class, + ""); + String additionalClassesConfig = ConfigParser.valueAsOrElse(properties.get("additionalClasses"), String.class, + ""); + Integer initializationWaitTime = ConfigParser.valueAsOrElse(properties.get("stabilityGenerationWaitTime"), + Integer.class, 10000); + Integer scriptCacheSize = ConfigParser.valueAsOrElse(properties.get("scriptCacheSize"), Integer.class, 50); + Boolean allowInstanceReuse = ConfigParser.valueAsOrElse(properties.get("allowInstanceReuse"), Boolean.class, + false); + + osgiPackageResourceListingStrategy = this::listClassResources; + java223Strategy = new Java223Strategy(getAdditionalBindings(), + bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader()); + java223Strategy.setAllowInstanceReuse(allowInstanceReuse); + scriptWrappingStrategy = new ScriptWrappingStrategy(); + compiledScriptCache = new Java223CompiledScriptCache(scriptCacheSize); + + try { + copyHelperLibJar(); + + dependencyGenerator = new DependencyGenerator(LIB_DIR, additionalBundlesConfig, additionalClassesConfig, + bundleContext); + classWriter = new SourceWriter(LIB_DIR); + this.sourceGenerator = new SourceGenerator(classWriter, dependencyGenerator, itemRegistry, thingRegistry, + bundleContext, initializationWaitTime); + sourceGenerator.generateThings(); + sourceGenerator.generateActions(); + sourceGenerator.generateItems(); + sourceGenerator.generateJava223Script(); + dependencyGenerator.createCoreDependencies(); + // When a lib is removed, SourceWriter should now because it may have to regenerate it + watchService.registerListener(classWriter, LIB_DIR); + } catch (IOException e) { + throw new Java223Exception("Cannot create helper library / class files in lib directory", e); + } + + this.watchService = watchService; + // first building of internal in memory lib representation + java223Strategy.scanLibDirectory(); + // When a lib change, update internal lib storage + watchService.registerListener(java223Strategy, LIB_DIR); + // When a lib change, invalidate cache of compiled script + watchService.registerListener(compiledScriptCache, LIB_DIR); + + logger.info("Bundle activated"); + } + + private void copyHelperLibJar() throws Java223Exception, IOException { + // get old file : + Path dest = LIB_DIR.resolve("helper-lib.jar"); + byte[] oldHelperLibAsByteArray = new byte[0]; + if (dest.toFile().exists()) { + oldHelperLibAsByteArray = Files.readAllBytes(dest); + } + + // get new file : + byte[] newHelperLibAsByteArray; + try (InputStream source = getClass().getResourceAsStream("/helper-lib.jar")) { + if (source != null) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; // Buffer size + int bytesRead; + while ((bytesRead = source.read(buffer)) != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead); + } + newHelperLibAsByteArray = byteArrayOutputStream.toByteArray(); + } else { + throw new Java223Exception("Cannot read helper lib in java223. Should not happened"); + } + } catch (IOException e) { + throw new Java223Exception("Cannot read helper file in classpath", e); + + } + + // compare and write only if different + if (!Arrays.equals(oldHelperLibAsByteArray, newHelperLibAsByteArray)) { + try (FileOutputStream fileOutputStream = new FileOutputStream(dest.toFile())) { + ; + fileOutputStream.write(newHelperLibAsByteArray); + } catch (IOException e) { + throw new Java223Exception("Cannot write helper file", e); + } + } + } + + @Modified + protected void modified(Map properties) { + String additionalBundlesConfig = ConfigParser.valueAsOrElse(properties.get("additionalBundles"), String.class, + ""); + String additionalClassesConfig = ConfigParser.valueAsOrElse(properties.get("additionalClasses"), String.class, + ""); + Integer scriptCacheSize = ConfigParser.valueAsOrElse(properties.get("scriptCacheSize"), Integer.class, 50); + Integer stabilityGenerationWaitTime = ConfigParser.valueAsOrElse(properties.get("stabilityGenerationWaitTime"), + Integer.class, 10000); + Boolean allowInstanceReuse = ConfigParser.valueAsOrElse(properties.get("allowInstanceReuse"), Boolean.class, + false); + + compiledScriptCache.setCacheSize(scriptCacheSize); + sourceGenerator.setStabilityGenerationWaitTime(stabilityGenerationWaitTime); + java223Strategy.setAllowInstanceReuse(allowInstanceReuse); + dependencyGenerator.setAdditionalConfig(additionalBundlesConfig, additionalClassesConfig); + dependencyGenerator.createCoreDependencies(); + logger.debug("java223 configuration update received ({})", properties); + } + + @Deactivate + public void deactivate() { + watchService.unregisterListener(java223Strategy); + watchService.unregisterListener(classWriter); + watchService.unregisterListener(compiledScriptCache); + } + + @Override + public List getScriptTypes() { + String[] types = { Java223Constants.JAVA_FILE_TYPE }; + return Arrays.asList(types); + } + + @Override + public void scopeValues(ScriptEngine scriptEngine, Map scopeValues) { + for (Entry entry : scopeValues.entrySet()) { + scriptEngine.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public @Nullable ScriptEngine createScriptEngine(String scriptType) { + if (getScriptTypes().contains(scriptType)) { + JavaScriptEngine engine = new Java223ScriptEngine(compiledScriptCache, java223Strategy); + engine.setPackageResourceListingStrategy(osgiPackageResourceListingStrategy); + engine.setScriptInterceptorStrategy(scriptWrappingStrategy); + engine.setCompilationOptions(Arrays.asList("-g", "-parameters")); + + return engine; + } + return null; + } + + @Override + public ScriptEngine getScriptEngine() { + ScriptEngine scriptEngine = createScriptEngine(Java223Constants.JAVA_FILE_TYPE); + if (scriptEngine == null) { + throw new Java223Exception("Null script engine returned. Should not happened"); + } + return scriptEngine; + } + + /** + * Additional data to put into bindings so the scripts could use them. + * + * @return + */ + private Map getAdditionalBindings() { + RuleManager ruleManager = bundleContext.getService(bundleContext.getServiceReference(RuleManager.class)); + ThingManager thingManager = bundleContext.getService(bundleContext.getServiceReference(ThingManager.class)); + MetadataRegistry metadataRegistry = bundleContext + .getService(bundleContext.getServiceReference(MetadataRegistry.class)); + return Map.of(Java223Constants.RULE_MANAGER, ruleManager, // + Java223Constants.METADATA_REGISTRY, metadataRegistry, // + Java223Constants.THING_MANAGER, thingManager); + } + + private Collection listClassResources(String packageName) { + String path = packageName.replace(".", "/"); + path = "/" + path; + + Collection resources = bundleWiring.listResources(path, "*.class", 0); + + return resources; + } + + @Override + public Set getSubscribedEventTypes() { + return EVENTS; + } + + @Override + public void receive(Event event) { + String eventType = event.getType(); + + if (ACTION_EVENTS.contains(eventType)) { + ThingStatusInfoChangedEvent eventStatusInfoChange = (ThingStatusInfoChangedEvent) event; + if ((ThingStatus.INITIALIZING.equals(eventStatusInfoChange.getOldStatusInfo().getStatus()) + && INITIALIZED.contains(eventStatusInfoChange.getStatusInfo().getStatus())) + || (ThingStatus.UNINITIALIZED.equals(eventStatusInfoChange.getStatusInfo().getStatus()) + && INITIALIZED.contains(eventStatusInfoChange.getOldStatusInfo().getStatus()))) { + sourceGenerator.generateActions(); + } + } else if (ITEM_EVENTS.contains(eventType)) { + logger.debug("Added/updated item: {}", event); + sourceGenerator.generateItems(); + } else if (THING_EVENTS.contains(eventType)) { + logger.debug("Added/updated thing: {}", event); + sourceGenerator.generateThings(); + sourceGenerator.generateActions(); + } + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/DependencyGenerator.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/DependencyGenerator.java new file mode 100644 index 0000000000000..1c8da6e69505a --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/DependencyGenerator.java @@ -0,0 +1,292 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.codegeneration; + +import static org.osgi.framework.wiring.BundleWiring.*; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.wiring.BundleWiring; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Create a JAR with some dependencies inside, useful to code from an external IDE. + * This is purely a convenience JAR + * + * @author Gwendal Roulleau - Initial contribution, based on work from Jan N. Klug + */ +@NonNullByDefault +public class DependencyGenerator { + + public static final String CONVENIENCE_DEPENDENCIES_JAR = "convenience-dependencies.jar"; + + private static final Logger logger = LoggerFactory.getLogger(DependencyGenerator.class); + + private static final Set DEFAULT_DEPENDENCIES = Set.of("org.openhab.automation.java223.common", + "org.openhab.core.audio", "org.openhab.core.automation", "org.openhab.core.automation.events", + "org.openhab.core.automation.util", "org.openhab.core.automation.module.script", + "org.openhab.core.automation.module.script.defaultscope", + "org.openhab.core.automation.module.script.rulesupport.shared", + "org.openhab.core.automation.module.script.rulesupport.shared.simple", "org.openhab.core.common", + "org.openhab.core.common.registry", "org.openhab.core.config.core", "org.openhab.core.events", + "org.openhab.core.items", "org.openhab.core.items.events", "org.openhab.core.library.types", + "org.openhab.core.library.items", "org.openhab.core.library.dimension", "org.openhab.core.model.script", + "org.openhab.core.model.script.actions", "org.openhab.core.model.rule", "org.openhab.core.persistence", + "org.openhab.core.persistence.extensions", "org.openhab.core.thing", "org.openhab.core.thing.events", + "org.openhab.core.thing.binding", "org.openhab.core.transform", "org.openhab.core.transform.actions", + "org.openhab.core.types", "org.openhab.core.voice", "com.google.gson"); + + private static final Set DEFAULT_CLASSES_DEPENDENCIES = Set.of("org.eclipse.jdt.annotation.NonNull", + "org.eclipse.jdt.annotation.NonNullByDefault", "org.eclipse.jdt.annotation.Nullable", + "org.eclipse.jdt.annotation.DefaultLocation", "org.slf4j.LoggerFactory", "org.slf4j.Logger", + "org.slf4j.Marker"); + + private Path libDir; + // bundle + private String additionalBundlesConfig; + // individual classes + private String additionalClassesConfig; + private BundleContext bundleContext; + + private Set additionalClassesToExport = new HashSet<>(); + + /** + * + * @param libDir The target library directory + * @param additionalBundlesConfig Bundle to inspect. We will extract classes from it. + * @param additionalClassesConfig Individual classes to add to the exported JAR + * @param bundleContext + */ + public DependencyGenerator(Path libDir, String additionalBundlesConfig, String additionalClassesConfig, + BundleContext bundleContext) { + super(); + this.libDir = libDir; + this.additionalBundlesConfig = additionalBundlesConfig; + this.additionalClassesConfig = additionalClassesConfig; + this.bundleContext = bundleContext; + } + + public void setAdditionalConfig(String additionalBundlesConfig, String additionalClassesConfig) { + this.additionalBundlesConfig = additionalBundlesConfig; + this.additionalClassesConfig = additionalClassesConfig; + } + + /** + * Generate a JAR with useful classes for a client project / IDE + * This JAR is not needed, it's just a convenience for writing script smoothly + */ + public synchronized void createCoreDependencies() { + try (FileOutputStream outFile = new FileOutputStream(libDir.resolve(CONVENIENCE_DEPENDENCIES_JAR).toFile())) { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + JarOutputStream target = new JarOutputStream(outFile, manifest); + + Set dependencies = new HashSet<>(DEFAULT_DEPENDENCIES); + dependencies.addAll(Arrays.asList(additionalBundlesConfig.split(","))); + + Set searchIn = new HashSet<>(); + // search all dependencies + for (String packageName : dependencies) { + // a bundle can have the exact name of the package we search, but also the name of a parent package + // so we add them all in our list of search + String[] packageComponents = packageName.split("\\."); + for (int i = 1; i <= packageComponents.length; i++) { + searchIn.add(String.join(".", Arrays.copyOfRange(packageComponents, 0, i))); + } + } + + // browse all bundle and search for matches with the list established above + Set packagesSuccessfullyExported = new HashSet<>(); + for (Bundle bundle : bundleContext.getBundles()) { + if (searchIn.contains(bundle.getSymbolicName())) { // matches ! + copyExportedPackagesByBundleInspection(dependencies, bundle, target, packagesSuccessfullyExported); + } + } + + // we want to warn about the list of packages we didn't found + if (logger.isWarnEnabled()) { + Set packagesNotFound = new HashSet<>(DEFAULT_DEPENDENCIES); + packagesNotFound + .removeAll(packagesSuccessfullyExported.stream().map(s -> s.replaceAll("/", ".")).toList()); + for (String remainingPackage : packagesNotFound) { + logger.warn("Failed to found classes to export in package {}", remainingPackage); + } + } + + Set classesDependencies = new HashSet<>(DEFAULT_CLASSES_DEPENDENCIES); + classesDependencies.addAll(Arrays.asList(additionalClassesConfig.split(","))); + classesDependencies.addAll(additionalClassesToExport); + copyExportedClassesByClassLoader(classesDependencies, target); + + target.close(); + } catch (IOException e) { + logger.warn("Failed to create dependencies jar in '{}': {}", libDir, e.getMessage()); + } + } + + private static void copyExportedClassesByClassLoader(Set classesToExtract, JarOutputStream target) { + for (String classToExtract : classesToExtract) { + if (classToExtract.isEmpty()) { + continue; + } + String path = classToExtract.replaceAll("\\.", "/") + ".class"; + ClassLoader classLoader = DependencyGenerator.class.getClassLoader(); + if (classLoader == null) { + logger.warn("Failed (no classloader) to copy from classpath : {}", classToExtract); + return; + } + try (InputStream stream = classLoader.getResourceAsStream(path)) { + if (stream != null) { + addEntryToJar(target, path, 0, stream); + } else { + logger.warn("InputStream {} from classpath is null", classToExtract); + } + } catch (IOException e) { + logger.warn("Failed to copy classes '{}' from classpath : {}", classToExtract, e.getMessage()); + } + } + } + + private static void copyExportedPackagesByBundleInspection(Set dependencies, Bundle bundle, + JarOutputStream target, Set classesSuccessfullyExported) { + String exportPackage = bundle.getHeaders().get("Export-Package"); + if (exportPackage == null) { + logger.warn("Bundle '{}' does not export any package!", bundle.getSymbolicName()); + return; + } + List exportedPackages = Arrays.stream(exportPackage // + .split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) // split only on comma not in double quotes + .map(s -> s.split(";")[0]) // get only package name and drop uses, version, etc. + .map(b -> b.replace(".", "/")).collect(Collectors.toList()); + Set dependenciesWithSlash = dependencies.stream().map(b -> b.replace(".", "/")) + .collect(Collectors. toSet()); + + bundle.adapt(BundleWiring.class).listResources("", "*.class", LISTRESOURCES_LOCAL + LISTRESOURCES_RECURSE) + .forEach(classFile -> { + try { + int classNameStart = classFile.lastIndexOf("/"); + if (classNameStart != -1) { + String packageName = classFile.substring(0, classNameStart); + if (classNameStart == -1 || !exportedPackages.contains(packageName) + || !dependenciesWithSlash.contains(packageName)) { + return; + } + + URL urlEntry = bundle.getEntry(classFile); + if (urlEntry == null) { + logger.warn("URL for {} is empty, skipping", classFile); + } else { + try (InputStream stream = urlEntry.openStream()) { + addEntryToJar(target, classFile, 0, stream); + classesSuccessfullyExported.add(packageName); + } + } + } + } catch (IOException e) { + logger.warn("Failed to copy class '{}' from '{}': {}", classFile, bundle.getSymbolicName(), + e.getMessage()); + } + }); + } + + private static void addEntryToJar(JarOutputStream jar, String name, long lastModified, InputStream content) + throws IOException { + JarEntry jarEntry = new JarEntry(name); + if (lastModified != 0) { + jarEntry.setTime(lastModified); + } + jar.putNextEntry(jarEntry); + jar.write(content.readAllBytes()); + jar.closeEntry(); + } + + private boolean excludeFromExport(String classToExport) { + return classToExport.startsWith("java.") || DEFAULT_DEPENDENCIES.stream() // + .filter(packageAlreadyExported -> classToExport.startsWith(packageAlreadyExported)) // + .findFirst() // + .isPresent(); + } + + private static void getAllInterfaces(@Nullable Class cls, final Set interfacesFound) { + @Nullable + Class clsLocal = cls; + while (clsLocal != null) { + final Class[] interfaces = clsLocal.getInterfaces(); + for (final Class i : interfaces) { + if (interfacesFound.add(i.getCanonicalName())) { + getAllInterfaces(i, interfacesFound); + } + } + clsLocal = clsLocal.getSuperclass(); + } + } + + private static List getAllSuperclasses(final Class cls) { + final List classes = new ArrayList<>(); + Class superclass = cls.getSuperclass(); + while (superclass != null) { + classes.add(superclass.getCanonicalName()); + superclass = superclass.getSuperclass(); + } + return classes; + } + + /** + * Add classes to export inside the dependencies JAR + * Purely for convenience + * + * @param allClassesToExport + */ + public void setClassesToAddToDependenciesLib(Set allClassesToExport) { + + Set newAdditionalClassesToExport = new HashSet<>(); + for (String clazzAsString : allClassesToExport) { + Class clazz; + try { + clazz = Class.forName(clazzAsString); + newAdditionalClassesToExport.add(clazzAsString); + newAdditionalClassesToExport.addAll(getAllSuperclasses(clazz)); + getAllInterfaces(clazz, newAdditionalClassesToExport); + } catch (ClassNotFoundException e) { + logger.warn("Cannot inspect class {} to add it as a dependancy", clazzAsString); + } + } + + newAdditionalClassesToExport.removeIf(this::excludeFromExport); + // check if there is new classes : + newAdditionalClassesToExport.removeAll(additionalClassesToExport); + if (!newAdditionalClassesToExport.isEmpty()) { + additionalClassesToExport.addAll(newAdditionalClassesToExport); + createCoreDependencies(); + } + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/SourceGenerator.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/SourceGenerator.java new file mode 100644 index 0000000000000..acccaa2d9fcd2 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/SourceGenerator.java @@ -0,0 +1,359 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.codegeneration; + +import java.io.IOException; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import freemarker.template.TemplateMethodModelEx; + +/** + * The SourceGenerator is responsible for generating the additional classes + * helping for rule development. It uses freemarker as a template engine. + * Include a delayed mechanism to prevent creating file multiple time when there is many + * modifications in the registry (especially useful at startup) + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class SourceGenerator { + + private static final String GENERATED = "generated"; + + private final Logger logger = LoggerFactory.getLogger(SourceGenerator.class); + + private final ItemRegistry itemRegistry; + private final ThingRegistry thingRegistry; + private final BundleContext bundleContext; + + private SourceWriter sourceWriter; + private DependencyGenerator dependencyGenerator; + + private static final String TPL_LOCATION = "/generated/"; + + Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); + + private Integer stabilityGenerationWaitTime; + + // keep a reference to generation method, to use as a key in the delayed map + private InternalGenerator actionGeneration = this::internalGenerateActions; + private InternalGenerator itemGeneration = this::internalGenerateItems; + private InternalGenerator thingGeneration = this::internalGenerateThings; + private Map> futureGeneration = new HashMap<>(); + + private ScheduledExecutorService scheduledPool = ThreadPoolManager + .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); + + /** + * + * @param sourceWriter The real writer, with a cache to avoid writing something already existing + * @param dependencyGenerator We will add to the dependency generator a list of package we thing would be + * interesting to export + * @param itemRegistry Lookup inside the itemRegistry to generate a list of item + * @param thingRegistry Lookup inside the thingRegistry to generate a list of thing + * @param bundleContext We will search for class action inside + * @param stabilityGenerationWaitTime + */ + public SourceGenerator(SourceWriter sourceWriter, DependencyGenerator dependencyGenerator, + ItemRegistry itemRegistry, ThingRegistry thingRegistry, BundleContext bundleContext, + Integer stabilityGenerationWaitTime) { + this.sourceWriter = sourceWriter; + this.itemRegistry = itemRegistry; + this.thingRegistry = thingRegistry; + this.bundleContext = bundleContext; + this.dependencyGenerator = dependencyGenerator; + this.stabilityGenerationWaitTime = stabilityGenerationWaitTime; + + cfg.setClassForTemplateLoading(SourceGenerator.class, "/"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + } + + /** + * Delaying generation is especially useful during startup, when item and thing are not all properly initialized. + * To avoid overwriting file with incomplete list of things/items/actions, we must avoid writing to the file if the + * registries are not completely ready. + * Until there is no more item/thing/action activating, this code will delay code generation. + * + * @param generator + */ + protected synchronized void delayWhenStable(InternalGenerator generator) { + ScheduledFuture scheduledFuture = futureGeneration.get(generator); + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + } + Runnable command = () -> { + try { + generator.generate(); + futureGeneration.remove(generator); + } catch (IOException | TemplateException e) { + logger.warn("Cannot create helper class file in library directory", e); + } + }; + futureGeneration.put(generator, + scheduledPool.schedule(command, stabilityGenerationWaitTime, TimeUnit.MILLISECONDS)); + } + + public void generateActions() { + delayWhenStable(actionGeneration); + } + + public void generateItems() { + delayWhenStable(itemGeneration); + } + + public void generateThings() { + delayWhenStable(thingGeneration); + } + + public void generateJava223Script() { + String packageName = sourceWriter.getPackageName(GENERATED); + + try { + Template template = cfg.getTemplate(TPL_LOCATION + "Java223Script.ftl"); + Map context = new HashMap<>(); + context.put("packageName", packageName); + + StringWriter writer = new StringWriter(); + template.process(context, writer); + + sourceWriter.replaceHelperFileIfNotEqual(packageName, "Java223Script", writer.toString()); + } catch (IOException | TemplateException e) { + logger.warn("Cannot create helper class file in library directory", e); + } + } + + @SuppressWarnings({ "unused", "null" }) + private void internalGenerateActions() throws IOException, TemplateException { + List thingActions; + try { + Set> classes = new HashSet<>(); + thingActions = bundleContext.getServiceReferences(ThingActions.class, null).stream() + .map(bundleContext::getService).filter(sr -> classes.add(sr.getClass())) + .collect(Collectors.toList()); + } catch (InvalidSyntaxException e) { + logger.warn("Failed to get thing actions: {}", e.getMessage()); + return; + } + + Template templateAction = cfg.getTemplate(TPL_LOCATION + "ThingAction.ftl"); + Map> actionsByScope = new HashMap<>(); + Set allClassesToImport = new HashSet<>(); + + for (ThingActions thingAction : thingActions) { + Class clazz = thingAction.getClass(); + + ThingActionsScope scopeAnnotation = clazz.getAnnotation(ThingActionsScope.class); + if (scopeAnnotation == null) { + continue; + } + String scope = scopeAnnotation.name().toString(); + String simpleClassName = clazz.getSimpleName(); + String packageName = sourceWriter.getPackageName(GENERATED, scope); + actionsByScope.computeIfAbsent(scope, (key -> new HashSet())) + .add(packageName + "." + simpleClassName); + + logger.trace("Processing class '{}' in package '{}'", simpleClassName, clazz.getPackageName()); + + List methods = Arrays.stream(clazz.getDeclaredMethods()) + .filter(method -> method.getDeclaredAnnotation(RuleAction.class) != null) + .collect(Collectors.toList()); + + Set classesToImport = new HashSet<>(); + List methodsDTO = new ArrayList<>(); + + for (Method method : methods) { + String name = method.getName(); + String returnValue = parseArgumentType(method.getGenericReturnType(), classesToImport); + + List parametersType = Arrays.stream(method.getGenericParameterTypes()) + .map(pt -> parseArgumentType(pt, classesToImport)).toList(); + + MethodDTO methodDTO = new MethodDTO(returnValue, name, parametersType); + logger.trace("Found method '{}' with parameters '{}' and return value '{}'.", name, parametersType, + returnValue); + + methodsDTO.add(methodDTO); + } + + allClassesToImport.addAll(classesToImport); + + Map context = new HashMap<>(); + context.put("packageName", packageName); + context.put("scope", scope); + context.put("classesToImport", classesToImport); + context.put("simpleClassName", simpleClassName); + context.put("methods", methodsDTO); + + StringWriter writer = new StringWriter(); + templateAction.process(context, writer); + + sourceWriter.replaceHelperFileIfNotEqual(packageName, simpleClassName, writer.toString()); + } + + // adding classes to the lib of exported dependencies + dependencyGenerator.setClassesToAddToDependenciesLib(allClassesToImport); + + // now generate action factory : + Template templateActionFactory = cfg.getTemplate(TPL_LOCATION + "Actions.ftl"); + Map context = new HashMap<>(); + context.put("packageName", sourceWriter.getPackageName(GENERATED)); + context.put("classesToImport", actionsByScope.values().stream().flatMap(s -> s.stream()).toList()); + @SuppressWarnings("unchecked") + TemplateMethodModelEx tmmLastName = ( + args) -> className(((List) args).get(0).getAsString()).get(); + context.put("lastName", tmmLastName); + @SuppressWarnings("unchecked") + TemplateMethodModelEx tmmCapitalize = (args) -> capitalize( + ((List) args).get(0).getAsString()); + context.put("camelCase", tmmCapitalize); + context.put("actionsByScope", actionsByScope); + StringWriter writer = new StringWriter(); + templateActionFactory.process(context, writer); + + sourceWriter.replaceHelperFileIfNotEqual(sourceWriter.getPackageName(GENERATED), "Actions", writer.toString()); + } + + /** + * Return a user friendly short readable name (without package details) and add the relevant full class name to the + * imports list + * + * @param type + * @param imports A set to add imports into + * @return a user friendly printable name (without package) + */ + protected static String parseArgumentType(Type type, Set imports) { + String typeName = type.getTypeName(); + String currentName = ""; + String friendlyFullType = ""; + for (int i = 0; i < typeName.length(); i++) { + char ch = typeName.charAt(i); + boolean isAOKClassCharacter = Character.isLetter(ch) || Character.isDigit(ch) + || Character.valueOf(ch).equals('_') || Character.valueOf(ch).equals('.'); + if (isAOKClassCharacter) { + currentName += ch; + } + if (!isAOKClassCharacter || i == typeName.length() - 1) { + if (!currentName.isEmpty()) { + Optional classNameOfAPackage = className(currentName); + if (classNameOfAPackage.isPresent()) { + imports.add(currentName); + friendlyFullType += classNameOfAPackage.get(); + } else { + friendlyFullType += currentName; + } + } + if (!isAOKClassCharacter) { + friendlyFullType += ch; + } + currentName = ""; + } + } + return friendlyFullType; + } + + private void internalGenerateItems() throws IOException, TemplateException { + Collection items = itemRegistry.getItems(); + String packageName = sourceWriter.getPackageName(GENERATED); + + Template template = cfg.getTemplate(TPL_LOCATION + "Items.ftl"); + Map context = new HashMap<>(); + context.put("packageName", packageName); + context.put("items", items); + context.put("itemImports", + items.stream().map(item -> item.getClass().getCanonicalName()).collect(Collectors.toSet())); + + StringWriter writer = new StringWriter(); + template.process(context, writer); + + sourceWriter.replaceHelperFileIfNotEqual(packageName, "Items", writer.toString()); + } + + private void internalGenerateThings() throws IOException, TemplateException { + Collection things = thingRegistry.getAll(); + String packageName = sourceWriter.getPackageName(GENERATED); + + Template template = cfg.getTemplate(TPL_LOCATION + "Things.ftl"); + Map context = new HashMap<>(); + context.put("packageName", packageName); + + context.put("things", things); + + @SuppressWarnings("unchecked") + TemplateMethodModelEx tmm = (args) -> escapeName( + ((List) args).get(0).getWrappedObject().toString()); + context.put("escapeName", tmm); + + StringWriter writer = new StringWriter(); + template.process(context, writer); + + sourceWriter.replaceHelperFileIfNotEqual(packageName, "Things", writer.toString()); + } + + private static String capitalize(String minusString) { + return minusString.substring(0, 1).toUpperCase() + minusString.substring(1); + } + + private static String escapeName(String textToEscape) { + return textToEscape.replace(":", "_").replace("-", "_"); + } + + private static Optional className(String fullName) { + int lastDotIndex = fullName.lastIndexOf('.'); + return lastDotIndex == -1 ? Optional.empty() : Optional.of(fullName.substring(lastDotIndex + 1)); + } + + public static record MethodDTO(String returnValueType, String name, List parameterTypes) { + } + + private static interface InternalGenerator { + public void generate() throws IOException, TemplateException; + } + + public void setStabilityGenerationWaitTime(Integer stabilityGenerationWaitTime) { + this.stabilityGenerationWaitTime = stabilityGenerationWaitTime; + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/SourceWriter.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/SourceWriter.java new file mode 100644 index 0000000000000..410053f4546d6 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/codegeneration/SourceWriter.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.codegeneration; + +import static org.openhab.automation.java223.common.Java223Constants.LIB_DIR; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.java223.common.Java223Constants; +import org.openhab.core.service.WatchService; +import org.openhab.core.service.WatchService.Kind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Write java file in lib directory. + * Do not write if already the same + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class SourceWriter implements WatchService.WatchEventListener { + + public static final String HELPER_PACKAGE = "helper"; + + private final Logger logger = LoggerFactory.getLogger(SourceWriter.class); + + // There is no memory issue in storing whole source code here. The JVM deduplicate Strings + // and sources are already stored in memory by our implementation of MemoryJavaFileObject + protected final Map generatedClassesSources = new HashMap<>(); + + private final Path folder; + + public SourceWriter(Path folder) throws IOException { + this.folder = folder; + + String helperPackageFolder = HELPER_PACKAGE.replaceAll("\\.", "/"); + Files.createDirectories(folder.resolve(helperPackageFolder)); + } + + @Override + public void processWatchEvent(Kind kind, Path path) { + // Because we write only if we found differences with the existent, + // we have to maintain a consistent view of the file system. + // And thus we need to be notified when a file is deleted or modified + Path fullPath = LIB_DIR.resolve(path); + if (fullPath.getFileName().toString().endsWith("." + Java223Constants.JAVA_FILE_TYPE) + && (kind == Kind.DELETE || kind == Kind.MODIFY)) { + // by intercepting delete or modify signal, we ensure that we remove java file from our internal database to + // regenerate them thereafter + String key = fullPath.toString().replace(File.separator, ".").substring(0, fullPath.toString().length()); + generatedClassesSources.remove(key); + } else { + logger.trace("Received '{}' for path '{}' - ignoring (wrong extension)", kind, fullPath); + } + } + + protected synchronized boolean replaceHelperFileIfNotEqual(String packageName, String className, + String generatedClass) throws IOException { + String key = packageName + "." + className; + + if (sourceHasChange(key, generatedClass)) { + String packageFolder = packageName.replaceAll("\\.", "/"); + Path javaFile = folder.resolve(packageFolder + "/" + className + "." + Java223Constants.JAVA_FILE_TYPE); + + Files.createDirectories(javaFile.getParent()); + try (FileOutputStream outFile = new FileOutputStream(javaFile.toFile())) { + outFile.write(generatedClass.getBytes(StandardCharsets.UTF_8)); + logger.debug("Wrote generated class: {}", javaFile.toAbsolutePath()); + } + return true; + } else { + logger.debug("{} has not changed.", key); + return false; + } + } + + protected boolean sourceHasChange(String key, String newSource) { + String previousSource = generatedClassesSources.put(key, newSource); + return !newSource.equals(previousSource); + } + + protected String getPackageName(String... packagePath) { + return HELPER_PACKAGE + "." + String.join(".", packagePath); + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/Java223Strategy.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/Java223Strategy.java new file mode 100644 index 0000000000000..446217590a720 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/Java223Strategy.java @@ -0,0 +1,310 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.strategy; + +import static org.openhab.automation.java223.common.Java223Constants.LIB_DIR; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.script.ScriptException; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.java223.common.BindingInjector; +import org.openhab.automation.java223.common.Java223Constants; +import org.openhab.automation.java223.common.Java223Exception; +import org.openhab.automation.java223.common.ReuseScriptInstance; +import org.openhab.automation.java223.common.RunScript; +import org.openhab.automation.java223.internal.Java223CompiledScriptInstanceWrapper; +import org.openhab.automation.java223.internal.codegeneration.DependencyGenerator; +import org.openhab.automation.java223.internal.strategy.jarloader.JarFileManager; +import org.openhab.automation.java223.internal.strategy.jarloader.JarFileManager.JarFileManagerFactory; +import org.openhab.core.service.WatchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.obermuhlner.scriptengine.java.MemoryFileManager; +import ch.obermuhlner.scriptengine.java.bindings.BindingStrategy; +import ch.obermuhlner.scriptengine.java.compilation.CompilationStrategy; +import ch.obermuhlner.scriptengine.java.construct.ConstructorStrategy; +import ch.obermuhlner.scriptengine.java.execution.ExecutionStrategy; +import ch.obermuhlner.scriptengine.java.execution.ExecutionStrategyFactory; +import ch.obermuhlner.scriptengine.java.name.DefaultNameStrategy; +import ch.obermuhlner.scriptengine.java.name.NameStrategy; + +/** + * A one-for-all strategy for a goal : providing binding / execution / library compilation and management to java223 + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class Java223Strategy implements ExecutionStrategyFactory, ExecutionStrategy, BindingStrategy, + CompilationStrategy, ConstructorStrategy, WatchService.WatchEventListener { + + private static Logger logger = LoggerFactory.getLogger(Java223Strategy.class); + + private static final List METHOD_NAMES_TO_EXECUTE = Arrays.asList("eval", "main", "run", "exec"); + + // Additional bindings, not in the openhab JSR 223 specification + private Map additionalBindings; + + // Keeping a list of library .java file in the lib directory + private static Map librariesByPath = new HashMap<>(); + + NameStrategy nameStrategy = new DefaultNameStrategy(); + JarFileManager.JarFileManagerFactory jarFileManagerfactory; + + private boolean allowInstanceReuseDefaultProperty; + + public Java223Strategy(Map additionalBindings, ClassLoader classLoader) { + super(); + this.additionalBindings = additionalBindings; + this.allowInstanceReuseDefaultProperty = false; + jarFileManagerfactory = new JarFileManagerFactory(LIB_DIR, classLoader); + } + + @Override + public ExecutionStrategy create(@Nullable Class clazz) throws ScriptException { + return this; + } + + @Override + public void associateBindings(Class compiledClass, Object compiledInstance, Map bindings) { + // adding a special self reference to bindings : "bindings", to receive a map with all bindings + bindings.put("bindings", bindings); + + // adding some custom additional fields + bindings.putAll(additionalBindings); + + // store bindings because of deferred instantiation + Java223CompiledScriptInstanceWrapper compiledInstanceWrapper = (Java223CompiledScriptInstanceWrapper) compiledInstance; + compiledInstanceWrapper.setBindings(bindings); + } + + @Override + public @Nullable Object execute(@Nullable Object instance) throws ScriptException { + + Java223CompiledScriptInstanceWrapper compiledInstanceWrapper = (Java223CompiledScriptInstanceWrapper) instance; + if (compiledInstanceWrapper == null) { + throw new ScriptException("Cannot run null class/instance"); + } + + Class compiledClass = compiledInstanceWrapper.getCompiledClass(); + Map bindings = compiledInstanceWrapper.getBindings(); + // empty bindings (this instance may be used in the cache, but its bindings won't be useful anymore) + compiledInstanceWrapper.setBindings(null); + + // instantiate the script + Object compiledInstance = instanciate(compiledInstanceWrapper, bindings); + + // inject bindings data in the script + BindingInjector.injectBindingsInto(compiledClass, bindings, compiledInstance); + + // find methods to execute + Optional returned = null; + for (Method method : compiledInstance.getClass().getMethods()) { + // methods with a special name, or methods with a special annotation + if (METHOD_NAMES_TO_EXECUTE.contains(method.getName()) || method.getAnnotation(RunScript.class) != null) { + try { + Object[] parameterValues = BindingInjector.getParameterValuesFor(compiledClass, method, bindings, + null); + var returnedLocal = method.invoke(compiledInstance, parameterValues); + // keep arbitrarily only the first returned value + if (returned == null || returned.isEmpty()) { + if (returnedLocal != null) { + returned = Optional.of(returnedLocal); + } else { + returned = Optional.empty(); + } + } + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException + | InstantiationException e) { + String simpleName = compiledInstance.getClass().getSimpleName(); + logger.error("Error executing entry point {} in {}", method.getName(), simpleName, e); + throw new ScriptException( + String.format("Error executing entry point %s in %s", method.getName(), simpleName, e)); + } + } + } + + // return if there was at least one execution + if (returned != null) { + return returned.orElse(null); + } + + throw new ScriptException(String.format( + "cannot execute: %s doesn't have a method named eval/main/run, or a RunScript annotated method", + compiledClass.getSimpleName())); + } + + @SuppressWarnings("null") + private Object instanciate(Java223CompiledScriptInstanceWrapper compiledInstanceWrapper, + Map bindings) { + + // default re-instantiation option overwritten by annotation if present + boolean instanceReuse = allowInstanceReuseDefaultProperty; + ReuseScriptInstance reuseAnnotation = compiledInstanceWrapper.getCompiledClass() + .getAnnotation(ReuseScriptInstance.class); + if (reuseAnnotation != null) { + instanceReuse = reuseAnnotation.value(); + } + + // if allowed, get from cache and return + var alreadyExistingWrappedScriptInstance = compiledInstanceWrapper.getWrappedScriptInstance(); + if (instanceReuse && alreadyExistingWrappedScriptInstance != null) { + return alreadyExistingWrappedScriptInstance; + } + + // create real instance from compiled class + // use the empty constructor if available, or the first one otherwise + Constructor[] constructors = compiledInstanceWrapper.getCompiledClass().getDeclaredConstructors(); + Constructor constructor = Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0).findFirst() + .orElseGet(() -> constructors[0]); + + try { + Object[] parameterValues = BindingInjector.getParameterValuesFor(compiledInstanceWrapper.getCompiledClass(), + constructor, bindings, null); + Object compiledInstance = constructor.newInstance(parameterValues); + if (compiledInstance == null) { + throw new Java223Exception("Instanciation of compiledInstance failed. Should not happened"); + } + compiledInstanceWrapper.setWrappedScriptInstance(compiledInstance); + return compiledInstance; + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new Java223Exception("Cannot instantiate the script", e); + } + } + + @Override + public Map retrieveBindings(Class compiledClass, Object compiledInstance) { + // not needed ? What is the use case ? + return new HashMap(); + } + + @Override + public List getJavaFileObjectsToCompile(@Nullable String simpleClassName, + @Nullable String currentSource) { + // the script + JavaFileObject currentJavaFileObject = MemoryFileManager.createSourceFileObject(null, simpleClassName, + currentSource); + // and we add all the .java libraries + List sumFileObjects = new ArrayList<>(librariesByPath.values()); + sumFileObjects.add(currentJavaFileObject); + return sumFileObjects; + } + + @Override + public void processWatchEvent(WatchService.Kind kind, Path pathEvent) { + Path fullPath = LIB_DIR.resolve(pathEvent); + + // All new .java file will be kept in memory + if (fullPath.getFileName().toString().endsWith("." + Java223Constants.JAVA_FILE_TYPE)) { + switch (kind) { + case CREATE: + case MODIFY: + addLibrary(fullPath); + break; + case DELETE: + removeLibrary(fullPath); + break; + default: + logger.warn("watch event not implemented {}", kind); + } + } else if (fullPath.getFileName().toString().endsWith("." + Java223Constants.JAR_FILE_TYPE)) { + // jar will be scanned to be added to the JarFileManagerFactory + // exclude convenience jar from processing + if (fullPath.getFileName().toString().equals(DependencyGenerator.CONVENIENCE_DEPENDENCIES_JAR)) { + return; + } + switch (kind) { + case CREATE: + jarFileManagerfactory.addLibPackage(fullPath); + break; + case MODIFY: + case DELETE: + // we cannot remove something from a ClassLoader, so we have to rebuild it + logger.debug("From watch event {} {}", kind, pathEvent); + jarFileManagerfactory.rebuildLibPackages(); + break; + case OVERFLOW: + break; + } + } else { + logger.trace( + "Received '{}' for path '{}' - ignoring (wrong extension, only .java and .jar file are supported)", + kind, fullPath); + } + } + + private void addLibrary(Path path) { + try { + String readString = Files.readString(path); + String fullName = nameStrategy.getFullName(readString); + String simpleClassName = NameStrategy.extractSimpleName(fullName); + JavaFileObject javafileObject = MemoryFileManager.createSourceFileObject(null, simpleClassName, readString); + librariesByPath.put(path.toString(), javafileObject); + } catch (ScriptException | IOException e) { + logger.info("Cannot get the file {} as a valid java object. Cause: {} {}", path.toString(), + e.getClass().getName(), e.getMessage()); + } + } + + private void removeLibrary(Path path) { + librariesByPath.remove(path.toString()); + } + + public void scanLibDirectory() { + try { + Files.walk(LIB_DIR).filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith("." + Java223Constants.JAVA_FILE_TYPE)) + .forEach(this::addLibrary); + jarFileManagerfactory.rebuildLibPackages(); + } catch (IOException e) { + logger.error("Cannot use libraries", e); + } + } + + @Override + public JavaFileManager getJavaFileManager(@Nullable JavaFileManager parentJavaFileManager) { + if (parentJavaFileManager == null) { + throw new IllegalArgumentException("Parent JavaFileManager should not be null"); + } + return jarFileManagerfactory.create(parentJavaFileManager); + } + + @Override + @Nullable + public Object construct(@Nullable Class clazz) throws ScriptException { + // in Java223ScriptEngineour strategy, we overwrote the compile method + // to use a cache. So the constructor strategy is useless + return null; + } + + public void setAllowInstanceReuse(boolean allowInstanceReuse) { + this.allowInstanceReuseDefaultProperty = allowInstanceReuse; + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/ScriptWrappingStrategy.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/ScriptWrappingStrategy.java new file mode 100644 index 0000000000000..8160da4bc5e53 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/ScriptWrappingStrategy.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.strategy; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import ch.obermuhlner.scriptengine.java.compilation.ScriptInterceptorStrategy; + +/** + * Wraps a script in boilerplate code if not present. + * Must respect some conditions to be wrapped correctly: + * - must not contains "public class" + * - line containing import must start with "import " + * - you can globally return a value, but take care to put the "return " keyword at the beginning of its own line + * - you cannot declare method (in fact, your script is already wrapped inside a method) + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class ScriptWrappingStrategy implements ScriptInterceptorStrategy { + + private static final Pattern NAME_PATTERN = Pattern.compile("public\\s+class\\s+.*"); + private static final Pattern PACKAGE_PATTERN = Pattern.compile("package\\s+[A-Za-z][A-Za-z0-9_$.]*;\\s*"); + private static final Pattern IMPORT_PATTERN = Pattern.compile("import\\s+[A-Za-z][A-Za-z0-9_$.]*;\\s*"); + + private static String BOILERPLATE_CODE_BEFORE = """ + import helper.generated.Java223Script; + import org.openhab.core.library.items.*; + import org.openhab.core.library.types.*; + import org.openhab.core.library.types.HSBType.*; + import org.openhab.core.library.types.IncreaseDecreaseType.*; + import org.openhab.core.library.types.NextPreviousType.*; + import org.openhab.core.library.types.OnOffType.*; + import org.openhab.core.library.types.OpenClosedType.*; + import org.openhab.core.library.types.PercentType.*; + import org.openhab.core.library.types.PlayPauseType.*; + import org.openhab.core.library.types.PointType.*; + import org.openhab.core.library.types.QuantityType.*; + import org.openhab.core.library.types.RewindFastforwardType.*; + import org.openhab.core.library.types.StopMoveType.*; + import org.openhab.core.library.types.UpDownType.*; + + + public class WrappedJavaScript extends Java223Script { + public Object main() { + """; + + private static String BOILERPLATE_CODE_AFTER = """ + } + } + """; + + @Override + public @Nullable String intercept(@Nullable String script) { + + if (script == null) { + return ""; + } + List lines = script.lines().toList(); + + String packageDeclarationLine = ""; + List importLines = new ArrayList<>(lines.size()); + List scriptLines = new ArrayList<>(lines.size()); + boolean returnIsPresent = false; + + // parse the file and sort lines in different categories + for (String line : lines) { + line = line.trim(); + if (NAME_PATTERN.matcher(line).matches()) { // a class declaration is found. No need to wrap + return script; + } + if (PACKAGE_PATTERN.matcher(line).matches()) { + packageDeclarationLine = line; + } else if (IMPORT_PATTERN.matcher(line).matches()) { + importLines.add(line); + } else { + if (line.startsWith("return")) { + returnIsPresent = true; + } + scriptLines.add(line); + } + } + + // recompose a complete script with the different parts + StringBuilder modifiedScript = new StringBuilder(); + modifiedScript.append(packageDeclarationLine + "\n"); + modifiedScript.append(String.join("\n", importLines)); + modifiedScript.append("\n"); + modifiedScript.append(BOILERPLATE_CODE_BEFORE); + modifiedScript.append(String.join("\n", scriptLines)); + modifiedScript.append("\n"); + if (!returnIsPresent) { + modifiedScript.append("return null;"); + } + modifiedScript.append(BOILERPLATE_CODE_AFTER); + + return modifiedScript.toString(); + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarClassLoader.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarClassLoader.java new file mode 100644 index 0000000000000..3bf3841a69d87 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarClassLoader.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.strategy.jarloader; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an implementation of JavaFileManager with extensions for JAR files + * This ClassLoader will be the parent of the MemoryClassLoader (holding the + * script and the .java library files) + * This ClassLoader will hold all classes in JAR. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class JarClassLoader extends ClassLoader { + private final Logger logger = LoggerFactory.getLogger(JarClassLoader.class); + + public static final String CLASS_FILE_TYPE = ".class"; + + private final Map availableClasses = new HashMap<>(); + + public JarClassLoader(@Nullable ClassLoader parent) { + super(parent); + } + + public void addJar(Path path) { + try (JarFile jarFile = new JarFile(path.toFile())) { + jarFile.stream().map(JarEntry::getName).filter(p -> p.endsWith(CLASS_FILE_TYPE)) + .forEach(className -> availableClasses.put(className, path)); + } catch (IOException e) { + logger.warn("Failed to process '{}': {}", path, e.getMessage()); + } + } + + /** + * Has this ClassLoader loaded this class in its memory + * + * @param name The name of the Class to test + * @return true if this ClassLoader already load the class + */ + public boolean isLoadedClass(String name) { + String path = name.replace('.', '/').concat(".class"); + return availableClasses.containsKey(path); + } + + @Override + protected Class findClass(@Nullable String name) throws ClassNotFoundException { + if (name == null) { + throw new ClassNotFoundException(); + } + String path = name.replace('.', '/').concat(".class"); + Path jarPath = availableClasses.get(path); + if (jarPath == null) { + throw new ClassNotFoundException(); + } + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + JarEntry jarEntry = (JarEntry) jarFile.getEntry(path); + if (jarEntry == null) { + throw new FileNotFoundException(); + } + byte[] clazzBytes = jarFile.getInputStream(jarEntry).readAllBytes(); + return defineClass(name, clazzBytes, 0, clazzBytes.length); + } catch (IOException e) { + logger.warn("Failed to load class '{}' from the stored location '{}': {}", name, jarPath, e.getMessage()); + throw new ClassNotFoundException(); + } + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarFileManager.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarFileManager.java new file mode 100644 index 0000000000000..1e2caa7426c14 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarFileManager.java @@ -0,0 +1,258 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.strategy.jarloader; + +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardLocation; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.java223.common.Java223Exception; +import org.openhab.automation.java223.internal.codegeneration.DependencyGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link JarFileManager} is an implementation of {@link JavaFileManager} with extensions for JAR files + * + * @author Jan N. Klug - Initial contribution + * @author Gwendal Roulleau - Use of a factory + */ +@NonNullByDefault +public class JarFileManager extends ForwardingJavaFileManager { + private static final Logger logger = LoggerFactory.getLogger(JarFileManager.class); + + private final Map> additionalPackages; + @Nullable + private final ClassLoader classLoader; + + public JarFileManager(M fileManager, ClassLoader classLoader, + Map> additionalPackages) { + super(fileManager); + this.classLoader = classLoader; + this.additionalPackages = additionalPackages; + } + + @Override + public @Nullable ClassLoader getClassLoader(@Nullable Location location) { + return classLoader; + } + + @SuppressWarnings({ "null", "unused" }) + @Override + public @NonNullByDefault({}) Iterable list(@Nullable Location location, + @Nullable String packageName, Set kinds, boolean recurse) throws IOException { + var localFileManager = fileManager; + if (localFileManager == null) { + throw new Java223Exception("FileManager cannot not be null"); + } + Iterable stdResult = localFileManager.list(location, packageName, kinds, recurse); + + if (location != StandardLocation.CLASS_PATH || !kinds.contains(JavaFileObject.Kind.CLASS)) { + return stdResult; + } + + List mergedFileObjects = new ArrayList<>(); + stdResult.forEach(mergedFileObjects::add); + mergedFileObjects.addAll(additionalPackages.getOrDefault(packageName, List.of())); + + return mergedFileObjects; + } + + @SuppressWarnings({ "unused", "null" }) + @Override + public JavaFileObject getJavaFileForOutput(@Nullable Location location, @Nullable String className, + @Nullable Kind kind, @Nullable FileObject sibling) throws IOException { + if (sibling instanceof JarFileObject) { + URI outFile = URI.create(removeExtension(sibling.toUri().toString()) + ".class"); + return JarFileObject.classFileObject(outFile); + } + var localFileManager = fileManager; + if (localFileManager == null) { + throw new Java223Exception("FileManager cannot not be null"); + } + return localFileManager.getJavaFileForOutput(location, className, kind, sibling); + } + + @SuppressWarnings({ "unused", "null" }) + @Override + public String inferBinaryName(@Nullable Location location, @Nullable JavaFileObject file) { + if (file instanceof JarFileObject) { + return removeExtension(getPath(file.toUri()).replace("/", ".").substring(1)); + } + var localFileManager = fileManager; + if (localFileManager == null) { + throw new Java223Exception("FileManager cannot not be null"); + } + return localFileManager.inferBinaryName(location, file); + } + + private static String removeExtension(String name) { + return name.substring(0, name.lastIndexOf(".")); + } + + private static String getPath(URI uri) { + if ("jar".equals(uri.getScheme())) { + String uriString = uri.toString(); + return uriString.substring(uriString.lastIndexOf("!")); + } else { + return uri.getPath(); + } + } + + /** + * Maintain an internal state to instantiate JarFileManager easily + * + * @author Gwendal Roulleau - Use of a factory + * + */ + public static class JarFileManagerFactory { + + private static final Lock FILEMANAGER_LOCK = new ReentrantLock(); + private static final Predicate JAR_FILTER = p -> p.toString().endsWith(".jar"); + + private Map> upToDateAdditionalPackages = Map.of(); + private ClassLoader upToDateClassLoader; + private ClassLoader parentClassLoader; + + MessageDigest md5Digest; + byte[] md5LibSum = new byte[0]; + + Path libDirectory; + + public JarFileManagerFactory(Path libDirectory, ClassLoader parentClassLoader) { + super(); + this.parentClassLoader = parentClassLoader; + this.libDirectory = libDirectory; + // temporary/default use of the parent : + this.upToDateClassLoader = parentClassLoader; + try { + this.md5Digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new Java223Exception("Cannot instanciate md5 digest. Should not happen"); + } + } + + public JarFileManager create(JavaFileManager fileManager) { + return new JarFileManager(fileManager, upToDateClassLoader, upToDateAdditionalPackages); + } + + public void rebuildLibPackages() { + FILEMANAGER_LOCK.lock(); + + try (Stream libFileStream = Files.list(libDirectory)) { + List libFiles = libFileStream.filter(JAR_FILTER) // + .filter((path) -> !path.getFileName().toString() // exclude convenience lib + .equals(DependencyGenerator.CONVENIENCE_DEPENDENCIES_JAR)) // + .collect(Collectors.toList()); + + // first check if it's really needed, in case we overwrite a file with the same content + for (Path path : libFiles) { + md5Digest.update(Files.readAllBytes(path)); + } + byte[] newMd5LibSum = md5Digest.digest(); + if (Arrays.equals(newMd5LibSum, md5LibSum)) { + logger.debug("No change and no need to rebuild lib package classloader"); + return; + } + + logger.info("Full rebuild of java223 classpath"); + logger.debug("Libraries to load from '{}' to memory: {}", libDirectory, libFiles); + + JarClassLoader newClassLoader = new JarClassLoader(parentClassLoader); + Map> additionalPackages = new HashMap<>(); + libFiles.forEach(libFile -> processLibrary(libFile, newClassLoader, additionalPackages)); + + upToDateClassLoader = newClassLoader; + upToDateAdditionalPackages = additionalPackages; + + md5LibSum = newMd5LibSum; + + } catch (IOException e) { + logger.warn("Could not load libraries: {}", e.getMessage()); + } finally { + FILEMANAGER_LOCK.unlock(); + } + } + + public void addLibPackage(Path newLib) { + if (newLib.getFileName().toString() // + .equals(DependencyGenerator.CONVENIENCE_DEPENDENCIES_JAR)) { + return; + } + try { + FILEMANAGER_LOCK.lock(); + logger.debug("Library to load to memory: {}", newLib.toString()); + if (upToDateClassLoader instanceof JarClassLoader upToDateJarClassLoader) { + processLibrary(newLib, upToDateJarClassLoader, upToDateAdditionalPackages); + } else { + throw new Java223Exception("Initialization error. The class loader should have been initialized"); + } + } finally { + FILEMANAGER_LOCK.unlock(); + } + } + + private void processLibrary(Path jarFile, JarClassLoader jarClassLoader, + Map> additionalPackages) { + try (JarInputStream jis = new JarInputStream(new FileInputStream(jarFile.toFile()))) { + JarEntry entry; + while ((entry = jis.getNextJarEntry()) != null) { + if (!entry.getName().endsWith(JarClassLoader.CLASS_FILE_TYPE)) { + continue; + } + String entryName = entry.getName(); + int fileNameStart = entryName.lastIndexOf("/"); + String packageName = fileNameStart == -1 ? "" + : entry.getName().substring(0, fileNameStart).replace("/", "."); + URI classUri = URI.create("jar:" + jarFile.toUri() + "!/" + entryName); + + Objects.requireNonNull(additionalPackages.computeIfAbsent(packageName, k -> new ArrayList<>())) + .add(JarFileObject.classFileObject(classUri)); + logger.trace("Added entry {} to additional libraries with package {}.", entry, packageName); + } + } catch (IOException e) { + logger.warn("Failed to process {}: {}", jarFile, e.getMessage()); + } + + jarClassLoader.addJar(jarFile); + logger.info("JAR loaded in the java223 script classpath: {}", jarFile.toString()); + } + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarFileObject.java b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarFileObject.java new file mode 100644 index 0000000000000..11c4ac067c5ec --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/java/org/openhab/automation/java223/internal/strategy/jarloader/JarFileObject.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.strategy.jarloader; + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.tools.JavaFileObject; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link JarFileObject} is a custom {@link JavaFileObject} implementation + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class JarFileObject implements JavaFileObject { + private final URI uri; + private final Kind kind; + + private JarFileObject(URI uri, Kind kind) { + this.uri = uri; + this.kind = kind; + } + + @Override + public URI toUri() { + return uri; + } + + @Override + public String getName() { + return uri.toString(); + } + + @Override + public InputStream openInputStream() throws IOException { + return uri.toURL().openStream(); + } + + @Override + public OutputStream openOutputStream() throws IOException { + return new FileOutputStream(Path.of(uri).toFile()); + } + + @Override + public Reader openReader(boolean ignoreEncodingErrors) throws IOException { + return new StringReader(getCharContent(ignoreEncodingErrors).toString()); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + try (ByteArrayOutputStream result = new ByteArrayOutputStream()) { + result.write(openInputStream().readAllBytes()); + return result.toString(StandardCharsets.UTF_8); + } + } + + @Override + public Writer openWriter() throws IOException { + return new OutputStreamWriter(openOutputStream()); + } + + @Override + public long getLastModified() { + return 0; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public Kind getKind() { + return kind; + } + + @Override + public boolean isNameCompatible(@Nullable String simpleName, @Nullable Kind kind) { + if (simpleName == null || kind == null) { + return false; + } + String baseName = simpleName + kind.extension; + return kind.equals(getKind()) && toUri().getPath().endsWith(baseName); + } + + @Override + public @Nullable NestingKind getNestingKind() { + return null; + } + + @Override + public @Nullable Modifier getAccessLevel() { + return null; + } + + @Override + public String toString() { + return uri.toString(); + } + + /** + * create a new {@link JavaFileObject} of kind SOURCE + * + * @param uri the URI of the file object + * @return the corresponding {@link JavaFileObject} + */ + public static JavaFileObject sourceFileObject(URI uri) { + return new JarFileObject(uri, Kind.SOURCE); + } + + /** + * create a new {@link JavaFileObject} of kind CLASS + * + * @param uri the URI of the file object + * @return the corresponding {@link JavaFileObject} + */ + public static JavaFileObject classFileObject(URI uri) { + return new JarFileObject(uri, Kind.CLASS); + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.automation.java223/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..75bb55fac8870 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,37 @@ + + + + automation + Java223 Scripting Automation + This bundle allows using Java in JSR223 scripts. + + + + + Script compilation cache size. 0 to disable. A positive number allows keeping compilation result. + + + + Reuse an instance if found in the cache. Allow sharing data between subsequent executions. Note: Beware + of concurrency issues + + + + Additional bundles exported for developing, concatenated by ",". + + + + Additional classes exported for developing, concatenated by ",". + + + true + + Delay (in ms) before writing generated classes. Each new generation triggering event further delays the + generation. Especially useful at startup. + 10000 + + + + diff --git a/bundles/org.openhab.automation.java223/src/main/resources/generated/Actions.ftl b/bundles/org.openhab.automation.java223/src/main/resources/generated/Actions.ftl new file mode 100644 index 0000000000000..3d8012f9f2ead --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/resources/generated/Actions.ftl @@ -0,0 +1,53 @@ +package ${packageName}; + +import java.lang.reflect.Method; + +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.automation.java223.common.Java223Exception; +import org.openhab.core.automation.module.script.defaultscope.ScriptThingActions; +import org.openhab.core.thing.binding.ThingActions; + +<#list classesToImport as classToImport> +<#if classToImport?has_content> +import ${classToImport}; + + + +public class Actions { + + @InjectBinding + private ScriptThingActions actions; + + public Actions(ScriptThingActions actions) { + this.actions = actions; + } + + /** + * Helper method to call arbitrary action. You should use the dedicated generated static actions class if possible + * + * @param thingActions + * @param method + * @param params + */ + public static void invokeAction(ThingActions thingActions, String method, Object... params) { + Class[] paramClasses = new Class[params.length]; + + for (int i = 0; i < params.length; i++) { + paramClasses[i] = params[i].getClass(); + } + try { + Method m = thingActions.getClass().getMethod(method, paramClasses); + m.invoke(thingActions, params); + } catch (Exception e) { + throw new Java223Exception("Cannot invoke action for " + method); + } + } + +<#list actionsByScope as scope, actions> +<#list actions as action> + public ${lastName(action)} get${camelCase(scope)}_${lastName(action)}(String thingUID) { + return new ${lastName(action)}(actions, thingUID); + } + + +} diff --git a/bundles/org.openhab.automation.java223/src/main/resources/generated/Items.ftl b/bundles/org.openhab.automation.java223/src/main/resources/generated/Items.ftl new file mode 100644 index 0000000000000..e675c372b4bdb --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/resources/generated/Items.ftl @@ -0,0 +1,40 @@ +package ${packageName}; + +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.automation.java223.common.Java223Exception; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemRegistry; + +<#list itemImports as itemImport> +import ${itemImport}; + + +public class Items { + + @InjectBinding + private ItemRegistry itemRegistry; + +<#list items as item> +<#if item.getLabel()??> + /** ${item.getLabel()} */ + + public static final String ${item.getName()} = "${item.getName()}"; + + + +<#list items as item> +<#if item.getLabel()??> + /** ${item.getLabel()} */ + + public ${item.getClass().getSimpleName()} ${item.getName()}() { + return (${item.getClass().getSimpleName()}) getItem(${item.getName()}); + } + + + protected Item getItem(String itemId) { + if (itemRegistry != null) { + return itemRegistry.get(itemId); + } + throw new Java223Exception("Items class not properly initialized. Use automatic instanciation by injection."); + } +} \ No newline at end of file diff --git a/bundles/org.openhab.automation.java223/src/main/resources/generated/Java223Script.ftl b/bundles/org.openhab.automation.java223/src/main/resources/generated/Java223Script.ftl new file mode 100644 index 0000000000000..df50b4dff998a --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/resources/generated/Java223Script.ftl @@ -0,0 +1,98 @@ +package ${packageName}; + +import java.util.Map; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.java223.common.BindingInjector; +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.automation.java223.common.RunScript; +import org.openhab.core.audio.AudioManager; +import org.openhab.core.automation.RuleManager; +import org.openhab.core.automation.RuleRegistry; +import org.openhab.core.automation.module.script.ScriptExtensionManagerWrapper; +import org.openhab.core.automation.module.script.defaultscope.ScriptBusEvent; +import org.openhab.core.automation.module.script.defaultscope.ScriptThingActions; +import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager; +import org.openhab.core.automation.module.script.rulesupport.shared.ValueCache; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.MetadataRegistry; +import org.openhab.core.thing.ThingManager; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.types.State; +import org.openhab.core.voice.VoiceManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import helper.rules.RuleAnnotationParser; +import helper.rules.RuleParserException; + +/** + * Base helper class for all Java223 Scripts. + * This class needs the helper-lib.jar + * Features : + * - Standard JSR223 OpenHAB bindings already declared as fields for immediate access + * - Auto execution of a parsing rule method (internalParseRules calling RuleAnnotationParser) + * - Additional shortcut to useful services (automationManager, sharedCache, ruleManager, metadataRegistry) + * - Include other generated helper classes (_items, _actions, _things) + * + * @author Gwendal Roulleau - Initial contribution + */ +public abstract class Java223Script { + + // warning : default openhab logger level is error + protected Logger logger = LoggerFactory.getLogger(this.getClass()); + + // all OpenHAB input as a convenience object : + protected @InjectBinding Map bindings; + + // default preset + protected @InjectBinding Map items; + protected @InjectBinding ItemRegistry ir; + protected @InjectBinding ItemRegistry itemRegistry; + protected @InjectBinding ThingRegistry things; + protected @InjectBinding RuleRegistry rules; + protected @InjectBinding ScriptBusEvent events; + protected @InjectBinding ScriptThingActions actions; + protected @InjectBinding ScriptExtensionManagerWrapper scriptExtension; + protected @InjectBinding ScriptExtensionManagerWrapper se; + protected @InjectBinding VoiceManager voice; + protected @InjectBinding AudioManager audio; + + // from ruleSupport preset + protected @InjectBinding(preset = "RuleSupport", named = "automationManager") ScriptedAutomationManager automationManager; + protected @InjectBinding(preset = "cache", named = "sharedCache") ValueCache sharedCache; + protected @InjectBinding(preset = "cache", named = "privateCache") ValueCache privateCache; + + // for transformation support + protected @Nullable Object input; + + // additional useful classes : + protected @InjectBinding RuleManager ruleManager; + protected @InjectBinding ThingManager thingManager; + protected @InjectBinding MetadataRegistry metadataRegistry; + + // generated classes + protected @InjectBinding Items _items; + protected @InjectBinding Actions _actions; + protected @InjectBinding Things _things; + + /** + * Parse all method/field rules in this script + */ + @RunScript + public void internalParseRules() { + try { + RuleAnnotationParser.parse(this, automationManager); + } catch (IllegalArgumentException | IllegalAccessException | RuleParserException e) { + logger.error("Cannot parse rules", e); + } + } + + /** + * Use this method to manually inject bindings value in an object of your choice. + * You probably don't need this (you should use your object as a library and let this helper framework injects it) + */ + public void injectBindings(Object objectToInjectInto) { + BindingInjector.injectBindingsInto(this.getClass(), bindings, objectToInjectInto); + } +} diff --git a/bundles/org.openhab.automation.java223/src/main/resources/generated/ThingAction.ftl b/bundles/org.openhab.automation.java223/src/main/resources/generated/ThingAction.ftl new file mode 100644 index 0000000000000..4888884f4d4cd --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/resources/generated/ThingAction.ftl @@ -0,0 +1,55 @@ +package ${packageName}; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.openhab.core.automation.module.script.defaultscope.ScriptThingActions; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.automation.java223.common.Java223Exception; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +<#list classesToImport as classToImport> +<#if classToImport?has_content> +import ${classToImport}; + + + +@InjectBinding(enable = false) +public class ${simpleClassName} { + + protected static Logger logger = LoggerFactory.getLogger(${simpleClassName}.class); + + public static final String SCOPE = "${scope}"; + + ThingActions thingActions; + + public ${simpleClassName}(ScriptThingActions actions, String thingUID) { + thingActions = actions.get(SCOPE, thingUID); + } + +<#list methods as method><#if (method.returnValueType() != "void")> + @SuppressWarnings("unchecked") + public ${method.returnValueType()} ${method.name()}(<#list method.parameterTypes() as parameter>${parameter} p${parameter?counter}<#sep>, ) { + try { + Class thingActionClass = thingActions.getClass(); +<#if (method.parameterTypes()?size > 0)> + Method method = thingActionClass.getMethod("${method.name()}", <#list method.parameterTypes() as parameter>${parameter}.class<#sep>, ); + <#if (method.returnValueType() != "void")> + Object returnValue = method.invoke(thingActions, <#list 1..method.parameterTypes()?size as i>p${i}<#sep>, ); +<#else> + Method method = thingActionClass.getMethod("${method.name()}"); + <#if (method.returnValueType() != "void")>@SuppressWarnings("unused") + Object returnValue = method.invoke(thingActions); + +<#if (method.returnValueType() != "void")> + return (${method.returnValueType()}) returnValue; + + } catch (NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + throw new Java223Exception("Error running action ${method.name()}", e); + } + } + + +} \ No newline at end of file diff --git a/bundles/org.openhab.automation.java223/src/main/resources/generated/Things.ftl b/bundles/org.openhab.automation.java223/src/main/resources/generated/Things.ftl new file mode 100644 index 0000000000000..6dc8ece67311d --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/main/resources/generated/Things.ftl @@ -0,0 +1,39 @@ +package ${packageName}; + +import org.openhab.automation.java223.common.InjectBinding; +import org.openhab.automation.java223.common.Java223Exception; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; + +public class Things { + + @InjectBinding + public ThingRegistry things; + +<#list things as thing> +<#if thing.getLabel()??> + /** ${thing.getLabel()} */ + + public static final String ${escapeName(thing.getUID())} = "${thing.getUID()}"; + + + +<#list things as thing> +<#if thing.getLabel()??> + /** ${thing.getLabel()} */ + + public Thing ${escapeName(thing.getUID())}() { + return getThing("${thing.getUID()}"); + } + + + protected Thing getThing(String stringUID) { + if (things != null) { + return things.get(new ThingUID(stringUID)); + } else { + throw new Java223Exception("Things class not properly initialized. Use automatic instanciation by injection"); + } + } + +} \ No newline at end of file diff --git a/bundles/org.openhab.automation.java223/src/test/java/org/openhab/automation/java223/internal/codegeneration/ClassGeneratorTest.java b/bundles/org.openhab.automation.java223/src/test/java/org/openhab/automation/java223/internal/codegeneration/ClassGeneratorTest.java new file mode 100644 index 0000000000000..3e0a6e9c51e52 --- /dev/null +++ b/bundles/org.openhab.automation.java223/src/test/java/org/openhab/automation/java223/internal/codegeneration/ClassGeneratorTest.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.java223.internal.codegeneration; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class ClassGeneratorTest { + + @Test + public void parseArgumentTypeFullClassNameTest() { + Set imports = new HashSet<>(); + String className = SourceGenerator.parseArgumentType(new FakeType("org.junit.jupiter.api.Test"), imports); + assertEquals("Test", className); + assertTrue(imports.contains("org.junit.jupiter.api.Test"), "Missing import statement"); + } + + @Test + public void parseArgumentOneWordTypeTest() { + Set imports = new HashSet<>(); + String className = SourceGenerator.parseArgumentType(new FakeType("oneword"), imports); + assertEquals("oneword", className); + assertTrue(imports.isEmpty(), "Too many import statement"); + } + + @Test + public void parseArgumentTypeFullGenericClassNameTest() { + Set imports = new HashSet<>(); + String className = SourceGenerator.parseArgumentType( + new FakeType("java.fake.Generic"), imports); + assertEquals("Generic", className); + assertTrue(imports.contains("org.openhab.core.automation.annotation.RuleAction"), "Missing import statement"); + assertTrue(imports.contains("java.fake.Generic"), "Missing import statement"); + assertEquals(2, imports.size()); + } + + @Test + public void parseArgumentTypeComplexGenericClassNameTest() { + Set imports = new HashSet<>(); + String className = SourceGenerator.parseArgumentType(new FakeType( + "java.fake.DoubleGeneric>"), + imports); + assertEquals("DoubleGeneric>", className); + assertTrue(imports.contains("java.fake.DoubleGeneric"), "Missing import statement"); + assertTrue(imports.contains("org.fake.First"), "Missing import statement"); + assertTrue(imports.contains("fake.Set"), "Missing import statement"); + assertTrue(imports.contains("org.openhab.core.automation.annotation.RuleAction"), "Missing import statement"); + assertEquals(4, imports.size()); + } + + public static class FakeType implements Type { + + private String type; + + public FakeType(String type) { + this.type = type; + } + + @Override + public String getTypeName() { + return type; + } + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index a473e96de60e5..d5a6fad24c692 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -19,6 +19,7 @@ org.openhab.automation.groovyscripting + org.openhab.automation.java223 org.openhab.automation.jrubyscripting org.openhab.automation.jsscripting org.openhab.automation.jsscriptingnashorn