├── src ├── main │ ├── resources │ │ ├── bootstrap.yml │ │ ├── rules │ │ │ ├── rules_catalog.json │ │ │ └── WhatToDo.js │ │ ├── log4j2.yml │ │ └── application.yml │ └── java │ │ └── com │ │ └── sakx │ │ └── developer │ │ └── rulesengine │ │ ├── RulesengineApplication.java │ │ ├── config │ │ ├── SpringFoxConfig.java │ │ └── WebConfig.java │ │ ├── rest │ │ └── RulesResource.java │ │ └── service │ │ └── RulesCatalog.java └── test │ └── java │ └── com │ └── sakx │ └── developer │ └── rulesengine │ ├── RulesengineApplicationTests.java │ └── WhatToDoRuleTest.java ├── settings.gradle ├── manifest-docker.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── manifest.yml ├── ci ├── credentials-local.yml ├── 1.sh ├── tasks │ └── build-service │ │ ├── task.yml │ │ └── task.sh ├── README.md └── pipeline.yml ├── Dockerfile ├── .gitignore ├── gradlew.bat ├── README.md └── gradlew /src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | debug: false -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'rulesengine' 2 | -------------------------------------------------------------------------------- /src/main/resources/rules/rules_catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "WhatToDo": "/rules/WhatToDo.js" 3 | } 4 | -------------------------------------------------------------------------------- /manifest-docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: simplerules 4 | memory: 1G 5 | instances: 1 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akoranne/rulesengine/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: simple-rules 4 | path: build/libs/rulesengine-0.4.0-SNAPSHOT.jar 5 | buildpack: java_buildpack_offline 6 | memory: 768M 7 | instances: 1 8 | -------------------------------------------------------------------------------- /ci/credentials-local.yml: -------------------------------------------------------------------------------- 1 | deploy-username: user 2 | deploy-password: pass 3 | pws-organization: pcfdev-org 4 | pws-staging-space: pcfdev-space 5 | pws-production-space: pcfdev-space 6 | pws-api: http://api.local.pcfdev.io 7 | skip-cert: true 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM openjdk:8 2 | FROM marceldekoster/alpine-oracle-jdk-8 3 | COPY build/libs/rulesengine-0.3.0-SNAPSHOT.jar /app/rulesengine.jar 4 | EXPOSE 8080 5 | ENV CLASSPATH /app/rulesengine.jar 6 | ENTRYPOINT ["java", "-jar", "/app/rulesengine.jar"] 7 | -------------------------------------------------------------------------------- /ci/1.sh: -------------------------------------------------------------------------------- 1 | # fly -t local pipelines 2 | # fly -t local destroy-pipeline -p rulesengine 3 | # fly -t local pipelines 4 | fly -t local set-pipeline -p rulesengine -c pipeline.yml -l credentials-ecslab.yml 5 | fly -t local unpause-pipeline --pipeline rulesengine 6 | fly -t local pipelines 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu May 10 21:57:44 CDT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-all.zip 7 | -------------------------------------------------------------------------------- /src/main/java/com/sakx/developer/rulesengine/RulesengineApplication.java: -------------------------------------------------------------------------------- 1 | package com.sakx.developer.rulesengine; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class RulesengineApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(RulesengineApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/sakx/developer/rulesengine/RulesengineApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.sakx.developer.rulesengine; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class RulesengineApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ci/tasks/build-service/task.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: docker-image 6 | source: 7 | repository: java 8 | tag: latest 9 | 10 | inputs: 11 | - name: service-repo 12 | 13 | outputs: 14 | - name: build-output 15 | 16 | params: 17 | TERM: -dumb 18 | GRADLE_OPTS: -Dorg.gradle.native=false 19 | 20 | #run: 21 | # path: "service-repo/gradlew" 22 | # args: ["--build-file", "source-code/build.gradle", "build"] 23 | 24 | run: 25 | path: service-repo/ci/tasks/build-service/task.sh 26 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.yml: -------------------------------------------------------------------------------- 1 | Configuration: 2 | status: warn 3 | 4 | Appenders: 5 | Console: 6 | name: Console 7 | target: SYSTEM_OUT 8 | PatternLayout: 9 | Pattern: "%d [trace=%X{X-Trace-Id:-},span=%X{X-Span-Id:-},%t] %highlight{%-5level} -- %c{1.}: %msg%n" 10 | Loggers: 11 | Root: 12 | level: info 13 | AppenderRef: 14 | ref: Console 15 | Logger: 16 | name: com.sax 17 | additivity: false 18 | level: debug 19 | AppenderRef: 20 | ref: Console -------------------------------------------------------------------------------- /ci/tasks/build-service/task.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # fail fast 4 | # set -x # print commands 5 | 6 | echo "" 7 | echo " .. Running build" 8 | echo "" 9 | 10 | cd service-repo 11 | 12 | # gradle build 13 | export GRADLE_OPTS="-Dorg.gradle.native=false" 14 | ./gradlew clean test assemble 15 | 16 | # create target folder 17 | # mkdir -f ../build-output 18 | 19 | # move all manifests file to target 20 | cp manifest.yml ../build-output/ 21 | 22 | cp build/libs/*.jar ../build-output/ 23 | 24 | echo "" 25 | echo " Build completed!!!" 26 | echo "" 27 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: rulesengine 4 | 5 | # show all actuator endpoints (only for POC) 6 | endpoints: 7 | enabled-by-default: true 8 | web: 9 | exposure: 10 | include: '*' 11 | server: 12 | port: 8080 13 | 14 | 15 | # spring boot 1.x 16 | security: 17 | basic: 18 | enabled: false 19 | ignored: /swagger-resources/** 20 | 21 | endpoints: 22 | restart: 23 | enabled: true 24 | 25 | # allow access to all actuator endpoints 26 | management: 27 | security: 28 | enabled: false 29 | 30 | -------------------------------------------------------------------------------- /ci/README.md: -------------------------------------------------------------------------------- 1 | # CI pipeline for Simple Rules Engine 2 | 3 | Update the credentials as needed or make a copy of it. 4 | 5 | To setup the pipeline, follow the following steps 6 | 7 | Login to Concourse 8 | ``` 9 | $> fly -t concourse login -c 10 | $> fly -t concourse login -c http://192.168.100.4:8080 11 | ``` 12 | 13 | Add the pipeline and unpause it. 14 | ``` 15 | $> fly -t concourse set-pipeline -p rulesengine -c pipeline.yml -l credentials-local.yml 16 | $> fly -t concourse unpause-pipeline --pipeline rulesengine 17 | $> fly -t concourse pipelines 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/sakx/developer/rulesengine/config/SpringFoxConfig.java: -------------------------------------------------------------------------------- 1 | package com.sakx.developer.rulesengine.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Profile; 6 | import springfox.documentation.builders.ApiInfoBuilder; 7 | import springfox.documentation.builders.PathSelectors; 8 | import springfox.documentation.builders.RequestHandlerSelectors; 9 | import springfox.documentation.service.ApiInfo; 10 | import springfox.documentation.service.Contact; 11 | import springfox.documentation.spi.DocumentationType; 12 | import springfox.documentation.spring.web.plugins.Docket; 13 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 14 | 15 | @Configuration 16 | @EnableSwagger2 17 | @Profile({"default", "cloud"}) 18 | public class SpringFoxConfig { 19 | @Bean 20 | public Docket apiDocket() { 21 | return new Docket(DocumentationType.SWAGGER_2) 22 | .select() 23 | .apis(RequestHandlerSelectors.basePackage("com.sarkkom")) 24 | .paths(PathSelectors.any()) 25 | .build(); 26 | } 27 | 28 | private ApiInfo apiInfo() { 29 | return new ApiInfoBuilder() 30 | .title("simple-rules") 31 | .description("Simple Rules Engine") 32 | .termsOfServiceUrl("http://localhost:8080/") 33 | .contact(new Contact("Ajay S. Koranne", "", "akoranne")) 34 | .version("1.0") 35 | .build(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/sakx/developer/rulesengine/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.sakx.developer.rulesengine.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 9 | 10 | @Configuration 11 | public class WebConfig extends WebSecurityConfigurerAdapter { 12 | 13 | @Bean 14 | public WebMvcConfigurerAdapter adapter() { 15 | return new WebMvcConfigurerAdapter() { 16 | @Override 17 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 18 | registry.addResourceHandler("swagger-ui.html") 19 | .addResourceLocations("classpath:/META-INF/resources/swagger-ui.html"); 20 | registry.addResourceHandler("/webjars/**") 21 | .addResourceLocations("classpath:/META-INF/resources/webjars/"); 22 | 23 | super.addResourceHandlers(registry); 24 | } 25 | }; 26 | } 27 | 28 | 29 | protected void configure(HttpSecurity http) throws Exception { 30 | http.authorizeRequests() 31 | .antMatchers( 32 | "/v2/api-docs", 33 | "/swagger-resources", 34 | "/swagger-resources/configuration/ui", 35 | "/swagger-resources/configuration/security") 36 | .permitAll(); 37 | http.csrf().disable(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/rules/WhatToDo.js: -------------------------------------------------------------------------------- 1 | // 2 | // Implementing 3 | // http://study.com/academy/lesson/what-is-a-decision-tree-examples-advantages-role-in-management.html 4 | 5 | var family_visiting; 6 | var weather; 7 | var money; 8 | var known_weathers = [ "sunny", "rainy", "windy" ]; 9 | var rich_money = [ "rich", "wealthy" ]; 10 | 11 | // function(s) 12 | var contains = function(needle) { 13 | // Per spec, the way to identify NaN is that it is not equal to itself 14 | var findNaN = needle !== needle; 15 | var indexOf; 16 | 17 | if(!findNaN && typeof Array.prototype.indexOf === 'function') { 18 | indexOf = Array.prototype.indexOf; 19 | } else { 20 | indexOf = function(needle) { 21 | var i = -1, index = -1; 22 | 23 | for(i = 0; i < this.length; i++) { 24 | var item = this[i]; 25 | 26 | if((findNaN && item !== item) || item === needle) { 27 | index = i; 28 | break; 29 | } 30 | } 31 | 32 | return index; 33 | }; 34 | } 35 | 36 | return indexOf.call(this, needle) > -1; 37 | }; 38 | 39 | 40 | // -------- output variable(s) --------- 41 | var todo; 42 | 43 | // --- starts here 44 | 45 | var arrValues = ["Sam","Great", "Sample", "High"]; 46 | // print (arrValues.indexOf("Sam")); 47 | // print(" jjs args: ---> ", family_visiting, weather, money); 48 | 49 | if (family_visiting == true) { 50 | todo = "Cinema"; 51 | } else { 52 | if (!known_weathers.indexOf(weather) == -1) { 53 | todo = null; 54 | } else if (weather == "sunny" ) { 55 | todo = "Play Tennis"; 56 | } else if (weather == "rainy") { 57 | todo = "Stay In"; 58 | } else { 59 | // weather is cold / windy, we need an indoor activity 60 | todo = rich_money.indexOf(money) > -1 ? "Shopping" : (money=="poor" ? "Cinema" : null) ; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###################### 2 | # Project Specific 3 | ###################### 4 | /build/www/** 5 | 6 | ###################### 7 | # Eclipse 8 | ###################### 9 | *.pydevproject 10 | .project 11 | .metadata 12 | /bin/** 13 | /tmp/** 14 | /tmp/**/* 15 | *.tmp 16 | *.bak 17 | *.swp 18 | *~.nib 19 | local.properties 20 | .classpath 21 | .settings/** 22 | .loadpath 23 | /src/main/resources/rebel.xml 24 | 25 | # External tool builders 26 | .externalToolBuilders/** 27 | 28 | # Locally stored "Eclipse launch configurations" 29 | *.launch 30 | 31 | # CDT-specific 32 | .cproject 33 | 34 | # PDT-specific 35 | .buildpath 36 | 37 | ###################### 38 | # Intellij 39 | ###################### 40 | .idea/** 41 | *.iml 42 | *.iws 43 | *.ipr 44 | *.ids 45 | *.orig 46 | 47 | ###################### 48 | # Maven 49 | ###################### 50 | /log/** 51 | /target/** 52 | 53 | ###################### 54 | # Package Files 55 | ###################### 56 | *.jar 57 | *.war 58 | *.ear 59 | *.db 60 | 61 | ###################### 62 | # Windows 63 | ###################### 64 | # Windows image file caches 65 | Thumbs.db 66 | 67 | # Folder config file 68 | Desktop.ini 69 | 70 | ###################### 71 | # Mac OSX 72 | ###################### 73 | .DS_Store 74 | .svn 75 | 76 | # Thumbnails 77 | ._* 78 | 79 | # Files that might appear on external disk 80 | .Spotlight-V100 81 | .Trashes 82 | 83 | ###################### 84 | # Directories 85 | ###################### 86 | /build/** 87 | /bin/** 88 | /spring_loaded/** 89 | /deploy/** 90 | 91 | ###################### 92 | # Logs 93 | ###################### 94 | *.log 95 | 96 | ###################### 97 | # Others 98 | ###################### 99 | *.class 100 | *.*~ 101 | *~ 102 | .merge_file* 103 | 104 | ###################### 105 | # Gradle 106 | ###################### 107 | !gradle/wrapper/gradle-wrapper.jar 108 | .gradle/ 109 | .gradle/** 110 | out 111 | 112 | ###################### 113 | # Maven Wrapper 114 | ###################### 115 | !.mvn/wrapper/maven-wrapper.jar 116 | .idea/ 117 | build/ 118 | ci/assets/ 119 | ci/credentials-ecslab.yml 120 | ci/scripts/ 121 | -------------------------------------------------------------------------------- /ci/pipeline.yml: -------------------------------------------------------------------------------- 1 | # pipeline for rulesengine 2 | 3 | groups: 4 | - name: Rulesengine-Service 5 | jobs: 6 | - service-unit 7 | 8 | resource_types: 9 | - name: slack-notification 10 | type: docker-image 11 | source: 12 | repository: cfcommunity/slack-notification-resource 13 | tag: latest 14 | 15 | resources: 16 | - name: service-repo 17 | type: git 18 | source: 19 | uri: https://github.com/akoranne/rulesengine.git 20 | branch: develop 21 | - name: deploy-dev-env 22 | type: cf 23 | source: 24 | api: {{pws-api}} 25 | username: {{deploy-username}} 26 | password: {{deploy-password}} 27 | skip_cert_check: true 28 | organization: {{pws-organization}} 29 | space: {{pws-staging-space}} 30 | 31 | jobs: 32 | - name: service-unit 33 | public: true 34 | serial: true 35 | plan: 36 | - get: service-repo 37 | trigger: true 38 | - task: CreateArchive 39 | privileged: true 40 | file: service-repo/ci/tasks/build-service/task.yml 41 | # on_failure: 42 | # put: slack-alert 43 | # params: 44 | # channels: ci-pipeline 45 | # text: | 46 | # The $BUILD_PIPELINE_NAME build failed! 47 | # https://channel.slack.com/archives/ci-pipeline/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME 48 | # username: concourse_user 49 | # icon_url: http://downloadicons.net/sites/default/files/error-icons-44565.png 50 | - put: deploy-dev-env 51 | params: 52 | manifest: build-output/manifest.yml 53 | path: build-output/*.jar 54 | # on_failure: 55 | # put: slack-alert 56 | # params: 57 | # channels: ci-pipeline 58 | # text: | 59 | # The $BUILD_PIPELINE_NAME failed deployment to dev space. 60 | # https://channel.slack.com/archives/ci-pipeline/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME 61 | # username: concourse_user 62 | # icon_url: http://downloadicons.net/sites/default/files/error-icons-44565.png 63 | # on_success: 64 | # put: slack-alert 65 | # params: 66 | # channels: ci-pipeline 67 | # text: | 68 | # The $BUILD_PIPELINE_NAME successfully deployed to dev space. 69 | # https://channel.slack.com/archives/ci-pipeline/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME 70 | # username: concourse_user 71 | # icon_url: http://vignette2.wikia.nocookie.net/legouniverse/images/f/f5/Jay_render.PNG/revision/latest?cb=20120406164257 72 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/com/sakx/developer/rulesengine/rest/RulesResource.java: -------------------------------------------------------------------------------- 1 | package com.sakx.developer.rulesengine.rest; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import javax.script.ScriptEngine; 7 | import javax.script.ScriptEngineManager; 8 | 9 | import org.json.JSONException; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.PathVariable; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestMethod; 17 | import org.springframework.web.bind.annotation.RequestParam; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import com.sakx.developer.rulesengine.service.RulesCatalog; 22 | 23 | /** 24 | * Rest Controller for get rules for the given ruleName at runtime. 25 | */ 26 | @RestController 27 | @RequestMapping("/") 28 | public class RulesResource { 29 | @Autowired 30 | private RulesCatalog catalogService; 31 | 32 | static ScriptEngine engine = null; 33 | // private String defaultRuleType = "WhatToDo"; 34 | // private String rule = null; 35 | 36 | @RequestMapping("/") 37 | public String home() { 38 | return "\n\n *** Rules Engine *** "; 39 | } 40 | 41 | @RequestMapping(value = "/api/rules/{name}", 42 | method = RequestMethod.GET, 43 | produces = MediaType.APPLICATION_JSON_VALUE) 44 | public ResponseEntity evaluateRule( 45 | @PathVariable String name, 46 | @RequestParam(value = "family_visiting", defaultValue = "") String familyVisiting, 47 | @RequestParam(value = "weather", defaultValue = "") String weather, 48 | @RequestParam(value = "money", defaultValue = "") String money) throws JSONException { 49 | String json = null; 50 | try { 51 | engine = new ScriptEngineManager().getEngineByName("Nashorn"); 52 | String rule = catalogService.getNashornRule(name); 53 | engine.put("family_visiting", (familyVisiting != null && familyVisiting.equalsIgnoreCase("yes") ? true : false)); 54 | engine.put("weather", weather); 55 | engine.put("money", money); 56 | engine.eval(rule); 57 | 58 | // JSONObject results = new JSONObject(); 59 | Map results = new HashMap(); 60 | results.put("todo", (String) engine.get("todo")); 61 | json = (new ObjectMapper()).writeValueAsString(results); 62 | } catch (Exception e) { 63 | StringBuilder msg = new StringBuilder(); 64 | msg.append("Evaluate rule - ").append(name).append(" failed - "); 65 | msg.append(e.getMessage()); 66 | throw new JSONException(msg.toString()); 67 | } 68 | ResponseEntity responseEntity = new ResponseEntity(json, HttpStatus.OK); 69 | return responseEntity; 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Simple Rules Engine 2 | 3 | For one of my project, I needed a simple rules engine. 4 | 5 | I like the [DecisionDag](https://github.com/mandarjog/decisionDag) that [Mandar Jog](https://github.com/mandarjog) built. 6 | It uses [Commons JEXL](https://commons.apache.org/proper/commons-jexl/reference/syntax.html) and allows rules to be defined in plain english. 7 | 8 | 9 | In my use case, I had following goals. 10 | 11 | * keep it simple (KISS) 12 | * rules engine as micro-service (deployable to cloud). 13 | * a rules catalog of sorts that allowed new versions of the rules to be added and older ones removed. 14 | * ability to calculate within the rule and return results. 15 | 16 | This is a simple rules engine built with spring-boot in java. 17 | 18 | The rules are in plain javascript. 19 | 20 | Nashorn script engine allows runtime loading and evaluation of rules. 21 | 22 | 23 | ## Instructions 24 | 25 | * Install the app by cloning the repository [rulesengine](https://github.com/akoranne/rulesengine.git) 26 | 27 | * Build and run the app 28 | 29 | ``` 30 | $ cd rulesengine 31 | $ gradlew bootRun 32 | ``` 33 | 34 | * Call rest end-points. 35 | 36 | ``` 37 | $ curl -v 'http://localhost:8080/api/rules/WhatToDo?family_visiting=yes' 38 | 39 | $ curl 'http://localhost:8080/api/rules/WhatToDo?family_visiting=no&money=poor&weather=good' 40 | 41 | $ curl 'http://localhost:8080/api/rules/WhatToDo?family_visiting=no&money=poor&weather=cold' 42 | 43 | $ curl 'http://localhost:8080/api/rules/WhatToDo?family_visiting=no&money=rich&weather=cold' 44 | ``` 45 | 46 | 47 | ## PCF Dev 48 | 49 | __[Meet PCF Dev](https://blog.pivotal.io/pivotal-cloud-foundry/products/meet-pcf-dev-your-ticket-to-running-cloud-foundry-locally)__, a simplified, and minimized version of the Pivotal Cloud Foundry intended for your local machine. And [Getting started](https://pivotal.io/platform/pcf-tutorials/getting-started-with-pivotal-cloud-foundry-dev/introduction) is simple. 50 | 51 | ##Deploy to cloud 52 | 53 | * Target the cloud instance 54 | 55 | ``` 56 | $ cf login -a api.local.pcfdev.io --skip-ssl-validation 57 | 58 | API endpoint: api.local.pcfdev.io 59 | Email> admin 60 | Password> admin 61 | ``` 62 | 63 | * Build and deploy to cloud 64 | 65 | ``` 66 | $ cd rulesengine 67 | $ ./gradlew assemble 68 | $ cf push -f manifest.yml 69 | ``` 70 | 71 | * Test the cloud service 72 | 73 | ``` 74 | $ curl -v 'http://simple-rules.local.pcfdev.io/api/rules/WhatToDo?family_visiting=yes' 75 | 76 | $ curl 'http://simple-rules.local.pcfdev.io/api/rules/WhatToDo?family_visiting=no&money=poor&weather=good' 77 | 78 | $ curl 'http://simple-rules.local.pcfdev.io/api/rules/WhatToDo?family_visiting=no&money=poor&weather=cold' 79 | 80 | $ curl 'http://simple-rules.local.pcfdev.io/api/rules/WhatToDo?family_visiting=no&money=rich&weather=cold' 81 | 82 | ``` 83 | 84 | * Concours CI/CD Pipleine 85 | 86 | The CI/CD pipeline will build the and push the app to local pcf-dev instance. Setup the concourse pipeline as follows 87 | 88 | ``` 89 | $ fly -t local set-pipeline -p rulesengine -c ci/rulesengine-pipeline.yml 90 | 91 | $ fly -t local unpause-pipeline -p rulesengine 92 | 93 | ``` 94 | 95 | Please post your comments for me, or if you have any questions. 96 | -------------------------------------------------------------------------------- /src/main/java/com/sakx/developer/rulesengine/service/RulesCatalog.java: -------------------------------------------------------------------------------- 1 | package com.sakx.developer.rulesengine.service; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.google.common.io.CharStreams; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.util.StringUtils; 11 | 12 | import java.io.*; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.stream.Collectors; 16 | 17 | /** 18 | * Created by ajaykoranne on 4/26/16. 19 | */ 20 | @Service 21 | public class RulesCatalog { 22 | private final Logger log = LoggerFactory.getLogger(RulesCatalog.class); 23 | 24 | // The fileCatalog file defining rules 25 | public final String defaultCatalogFile = "/rules/rules_catalog.json"; 26 | 27 | private Map fileCatalog = new HashMap(); 28 | private Map jsCatalogMap = new HashMap(); 29 | 30 | /** 31 | * Load the fileCatalog 32 | * 33 | * @throws Exception 34 | */ 35 | public void loadCatalog() throws IOException, UnsupportedEncodingException { 36 | loadCatalog(defaultCatalogFile, false); 37 | } 38 | 39 | /** 40 | * Load the catalog file, read rules and cache into rules catalog. 41 | * 42 | * @param catalogFile 43 | * @param isRefresh 44 | * @throws IOException 45 | * @throws UnsupportedEncodingException 46 | */ 47 | public void loadCatalog(String catalogFile, boolean isRefresh) throws IOException, UnsupportedEncodingException { 48 | // the products fileCatalog json file must be under resources folder 49 | // in the classpath 50 | 51 | if (fileCatalog == null || fileCatalog.isEmpty() || isRefresh == true) { 52 | log.debug(" ... loading rules fileCatalog from - " + catalogFile); 53 | 54 | InputStream in = this.getClass().getResourceAsStream(catalogFile); 55 | String json = CharStreams.toString(new InputStreamReader(in, "UTF-8")); 56 | 57 | ObjectMapper mapper = new ObjectMapper(); 58 | fileCatalog = mapper.readValue(json, new TypeReference>() { 59 | }); 60 | 61 | log.info(" ... the rules fileCatalog - " + fileCatalog); 62 | 63 | log.debug(" ... finished loading rules fileCatalog. "); 64 | } 65 | } 66 | 67 | 68 | public Map getFileCatalog() { 69 | return fileCatalog; 70 | } 71 | 72 | // 73 | public String getNashornRule(String typeName) throws IOException, UnsupportedEncodingException { 74 | String rule = null; 75 | 76 | // if the catalog of the rule files is empty, 77 | if (fileCatalog.isEmpty()) { 78 | loadCatalog(); 79 | } 80 | 81 | // is the given type in the decision dag catalogmap? 82 | if (jsCatalogMap.containsKey(typeName)) { 83 | // found it 84 | rule = jsCatalogMap.get(typeName); 85 | } else { 86 | // not found in the weak hash, load the decision dag catalog 87 | // get the dag rule file for the given type 88 | rule = getJSRule(typeName); 89 | } 90 | return rule; 91 | } 92 | 93 | private String getJSRule(String typeName) throws IOException, FileNotFoundException { 94 | String rulesText = null; 95 | // get the name of the rule file for the given type 96 | String ruleFileName = fileCatalog.get(typeName); 97 | if (StringUtils.hasText(ruleFileName)) { 98 | log.debug("reteriving javascript rules - " + ruleFileName); 99 | 100 | InputStream inStr = this.getClass().getResourceAsStream(ruleFileName); 101 | try (BufferedReader in = new BufferedReader(new InputStreamReader(inStr, "UTF-8"))) { 102 | rulesText = in.lines().collect(Collectors.joining("\n")); 103 | } 104 | 105 | // ClassLoader classLoader = RulesCatalog.class.getClassLoader(); 106 | // try (BufferedReader in = new BufferedReader(new FileReader(classLoader.getResource(ruleFileName).getFile()))) { 107 | // rulesText = in.lines().collect(Collectors.joining("\n")); 108 | // } 109 | 110 | 111 | if (StringUtils.hasText(rulesText)) { 112 | // found and loaded rules, add to the map 113 | jsCatalogMap.put(typeName, rulesText); 114 | } 115 | } 116 | return rulesText; 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/test/java/com/sakx/developer/rulesengine/WhatToDoRuleTest.java: -------------------------------------------------------------------------------- 1 | package com.sakx.developer.rulesengine; 2 | 3 | import static org.junit.Assert.fail; 4 | 5 | import javax.script.ScriptEngine; 6 | import javax.script.ScriptEngineManager; 7 | 8 | import org.junit.Assert; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.context.SpringBootTest; 16 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 17 | 18 | import com.sakx.developer.rulesengine.service.RulesCatalog; 19 | 20 | /** 21 | * Created by ajaykoranne on 4/21/16. 22 | */ 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | @SpringBootTest(classes = RulesengineApplication.class) 25 | public class WhatToDoRuleTest { 26 | private static final Logger log = LoggerFactory.getLogger(WhatToDoRuleTest.class); 27 | 28 | @Autowired 29 | private RulesCatalog catalogService; 30 | 31 | static ScriptEngine engine = null; 32 | private String ruleType = "WhatToDo"; 33 | private String rule = null; 34 | 35 | @Before 36 | public void setUp() throws Exception { 37 | try { 38 | catalogService.loadCatalog(); 39 | Assert.assertNotNull(" The rules file map is null.. ", catalogService.getFileCatalog()); 40 | Assert.assertNotNull(" rules file not found in rule catalog ", catalogService.getFileCatalog().get(ruleType)); 41 | } catch (Exception e) { 42 | log.error(" Received error while loading rule files json", e); 43 | fail(); 44 | } 45 | 46 | // check nashorn engine instance 47 | engine = new ScriptEngineManager().getEngineByName("Nashorn"); 48 | Assert.assertNotNull(" Nashorn engine failed priming .. ", engine); 49 | 50 | rule = catalogService.getNashornRule(ruleType); 51 | Assert.assertNotNull(" Rule is null ", rule); 52 | } 53 | 54 | @Test 55 | public void test1() throws Exception { 56 | engine.put("family_visiting", true); 57 | engine.eval(rule); 58 | Object results = engine.get("todo"); 59 | Assert.assertEquals("Cinema", results); 60 | System.out.println("Todo: " + results); 61 | } 62 | 63 | @Test 64 | public void test2() throws Exception { 65 | engine.put("family_visiting", false); 66 | engine.put("weather", "cold"); 67 | engine.eval(rule); 68 | Object results = engine.get("todo"); 69 | Assert.assertEquals(null, results); 70 | System.out.println("Todo: " + results); 71 | } 72 | 73 | @Test 74 | public void test3() throws Exception { 75 | engine.put("family_visiting", false); 76 | engine.put("weather", "sunny"); 77 | engine.eval(rule); 78 | Object results = engine.get("todo"); 79 | Assert.assertEquals("Play Tennis", results); 80 | System.out.println("Todo: " + results); 81 | } 82 | 83 | @Test 84 | public void test4() throws Exception { 85 | engine.put("family_visiting", false); 86 | engine.put("weather", "rainy"); 87 | engine.eval(rule); 88 | Object results = engine.get("todo"); 89 | Assert.assertEquals("Stay In", results); 90 | System.out.println("Todo: " + results); 91 | } 92 | 93 | @Test 94 | public void test5() throws Exception { 95 | engine.put("family_visiting", false); 96 | engine.put("weather", "windy"); 97 | engine.eval(rule); 98 | Object results = engine.get("todo"); 99 | Assert.assertEquals(null, results); 100 | System.out.println("Todo: " + results); 101 | } 102 | 103 | @Test 104 | public void test6() throws Exception { 105 | engine.put("family_visiting", false); 106 | engine.put("weather", "windy"); 107 | engine.put("money", "rich"); 108 | engine.eval(rule); 109 | Object results = engine.get("todo"); 110 | Assert.assertEquals("Shopping", results); 111 | System.out.println("Todo: " + results); 112 | } 113 | 114 | @Test 115 | public void test7() throws Exception { 116 | engine.put("family_visiting", false); 117 | engine.put("weather", "windy"); 118 | engine.put("money", "poor"); 119 | engine.eval(rule); 120 | Object results = engine.get("todo"); 121 | Assert.assertEquals("Cinema", results); 122 | System.out.println("Todo: " + results); 123 | } 124 | 125 | @Test 126 | public void test8() throws Exception { 127 | engine.put("family_visiting", false); 128 | engine.put("weather", "cloudy"); 129 | engine.put("money", "poor"); 130 | engine.eval(rule); 131 | Object results = engine.get("todo"); 132 | Assert.assertEquals("Cinema", results); 133 | System.out.println("Todo: " + results); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | --------------------------------------------------------------------------------