From 01dd8525b168dcb17724ef33ca436f1214211833 Mon Sep 17 00:00:00 2001 From: Amer Agovic Date: Tue, 2 Nov 2021 13:38:59 -0500 Subject: [PATCH] inital commit --- .classpath | 19 + .gitignore | 35 ++ .project | 40 ++ .settings/org.eclipse.buildship.core.prefs | 13 + .settings/org.eclipse.core.resources.prefs | 4 + .settings/org.eclipse.jdt.apt.core.prefs | 2 + .settings/org.eclipse.jdt.core.prefs | 9 + .settings/org.eclipse.m2e.core.prefs | 4 + Makefile | 132 +++++ README.md | 15 + build.gradle | 181 +++++++ pom.xml | 145 ++++++ src/main/java/com/reliancy/dbo/Action.java | 164 +++++++ src/main/java/com/reliancy/dbo/Condition.java | 81 ++++ src/main/java/com/reliancy/dbo/DBO.java | 34 ++ src/main/java/com/reliancy/dbo/Entity.java | 41 ++ src/main/java/com/reliancy/dbo/Field.java | 16 + .../java/com/reliancy/dbo/SQLTerminal.java | 36 ++ src/main/java/com/reliancy/dbo/Terminal.java | 48 ++ .../java/com/reliancy/jabba/AppSession.java | 67 +++ .../com/reliancy/jabba/AppSessionFilter.java | 50 ++ .../java/com/reliancy/jabba/CallSession.java | 84 ++++ src/main/java/com/reliancy/jabba/Config.java | 9 + .../java/com/reliancy/jabba/EndPoint.java | 18 + .../java/com/reliancy/jabba/FileServer.java | 117 +++++ src/main/java/com/reliancy/jabba/HTTP.java | 65 +++ .../com/reliancy/jabba/MethodEndPoint.java | 102 ++++ src/main/java/com/reliancy/jabba/Path.java | 86 ++++ .../java/com/reliancy/jabba/Processor.java | 82 ++++ src/main/java/com/reliancy/jabba/Request.java | 79 +++ .../java/com/reliancy/jabba/Response.java | 152 ++++++ .../com/reliancy/jabba/ResponseEncoder.java | 128 +++++ src/main/java/com/reliancy/jabba/Route.java | 11 + src/main/java/com/reliancy/jabba/Router.java | 231 +++++++++ .../com/reliancy/jabba/RouterEndPoint.java | 135 ++++++ src/main/java/com/reliancy/jabba/Session.java | 11 + .../com/reliancy/jabbasec/NotAuthentic.java | 11 + .../com/reliancy/jabbasec/NotPermitted.java | 12 + .../java/com/reliancy/jabbasec/Securable.java | 20 + .../com/reliancy/jabbasec/SecurityActor.java | 9 + .../com/reliancy/jabbasec/SecurityPermit.java | 16 + .../com/reliancy/jabbasec/SecurityPolicy.java | 125 +++++ .../reliancy/jabbasec/SecurityProtocol.java | 42 ++ .../com/reliancy/jabbasec/resources/login.j2 | 4 + .../java/com/reliancy/rec/DecoderSink.java | 14 + src/main/java/com/reliancy/rec/Hdr.java | 126 +++++ src/main/java/com/reliancy/rec/JSON.java | 22 + .../java/com/reliancy/rec/JSONDecoder.java | 331 +++++++++++++ .../java/com/reliancy/rec/JSONEncoder.java | 287 +++++++++++ src/main/java/com/reliancy/rec/Obj.java | 153 ++++++ src/main/java/com/reliancy/rec/Rec.java | 20 + src/main/java/com/reliancy/rec/Slot.java | 60 +++ .../java/com/reliancy/rec/TextDecoder.java | 17 + src/main/java/com/reliancy/rec/Vec.java | 18 + .../com/reliancy/util/CloseableIterator.java | 7 + src/main/java/com/reliancy/util/Handy.java | 458 ++++++++++++++++++ src/main/java/com/reliancy/util/Path.java | 418 ++++++++++++++++ .../java/com/reliancy/util/Resources.java | 144 ++++++ src/main/java/com/reliancy/util/Template.java | 94 ++++ .../java/com/reliancy/util/Tokenizer.java | 260 ++++++++++ .../java/com/reliancy/dbo/TerminalTest.java | 42 ++ .../java/com/reliancy/jabba/RouterTest.java | 47 ++ src/test/java/com/reliancy/rec/ObjTest.java | 37 ++ var/base.j2 | 27 ++ var/favicon.ico | Bin 0 -> 32038 bytes 65 files changed, 5267 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/org.eclipse.buildship.core.prefs create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 .settings/org.eclipse.jdt.apt.core.prefs create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 .settings/org.eclipse.m2e.core.prefs create mode 100644 Makefile create mode 100644 README.md create mode 100644 build.gradle create mode 100644 pom.xml create mode 100644 src/main/java/com/reliancy/dbo/Action.java create mode 100644 src/main/java/com/reliancy/dbo/Condition.java create mode 100644 src/main/java/com/reliancy/dbo/DBO.java create mode 100644 src/main/java/com/reliancy/dbo/Entity.java create mode 100644 src/main/java/com/reliancy/dbo/Field.java create mode 100644 src/main/java/com/reliancy/dbo/SQLTerminal.java create mode 100644 src/main/java/com/reliancy/dbo/Terminal.java create mode 100644 src/main/java/com/reliancy/jabba/AppSession.java create mode 100644 src/main/java/com/reliancy/jabba/AppSessionFilter.java create mode 100644 src/main/java/com/reliancy/jabba/CallSession.java create mode 100644 src/main/java/com/reliancy/jabba/Config.java create mode 100644 src/main/java/com/reliancy/jabba/EndPoint.java create mode 100644 src/main/java/com/reliancy/jabba/FileServer.java create mode 100644 src/main/java/com/reliancy/jabba/HTTP.java create mode 100644 src/main/java/com/reliancy/jabba/MethodEndPoint.java create mode 100644 src/main/java/com/reliancy/jabba/Path.java create mode 100644 src/main/java/com/reliancy/jabba/Processor.java create mode 100644 src/main/java/com/reliancy/jabba/Request.java create mode 100644 src/main/java/com/reliancy/jabba/Response.java create mode 100644 src/main/java/com/reliancy/jabba/ResponseEncoder.java create mode 100644 src/main/java/com/reliancy/jabba/Route.java create mode 100644 src/main/java/com/reliancy/jabba/Router.java create mode 100644 src/main/java/com/reliancy/jabba/RouterEndPoint.java create mode 100644 src/main/java/com/reliancy/jabba/Session.java create mode 100644 src/main/java/com/reliancy/jabbasec/NotAuthentic.java create mode 100644 src/main/java/com/reliancy/jabbasec/NotPermitted.java create mode 100644 src/main/java/com/reliancy/jabbasec/Securable.java create mode 100644 src/main/java/com/reliancy/jabbasec/SecurityActor.java create mode 100644 src/main/java/com/reliancy/jabbasec/SecurityPermit.java create mode 100644 src/main/java/com/reliancy/jabbasec/SecurityPolicy.java create mode 100644 src/main/java/com/reliancy/jabbasec/SecurityProtocol.java create mode 100644 src/main/java/com/reliancy/jabbasec/resources/login.j2 create mode 100644 src/main/java/com/reliancy/rec/DecoderSink.java create mode 100644 src/main/java/com/reliancy/rec/Hdr.java create mode 100644 src/main/java/com/reliancy/rec/JSON.java create mode 100644 src/main/java/com/reliancy/rec/JSONDecoder.java create mode 100644 src/main/java/com/reliancy/rec/JSONEncoder.java create mode 100644 src/main/java/com/reliancy/rec/Obj.java create mode 100644 src/main/java/com/reliancy/rec/Rec.java create mode 100644 src/main/java/com/reliancy/rec/Slot.java create mode 100644 src/main/java/com/reliancy/rec/TextDecoder.java create mode 100644 src/main/java/com/reliancy/rec/Vec.java create mode 100644 src/main/java/com/reliancy/util/CloseableIterator.java create mode 100644 src/main/java/com/reliancy/util/Handy.java create mode 100644 src/main/java/com/reliancy/util/Path.java create mode 100644 src/main/java/com/reliancy/util/Resources.java create mode 100644 src/main/java/com/reliancy/util/Template.java create mode 100644 src/main/java/com/reliancy/util/Tokenizer.java create mode 100644 src/test/java/com/reliancy/dbo/TerminalTest.java create mode 100644 src/test/java/com/reliancy/jabba/RouterTest.java create mode 100644 src/test/java/com/reliancy/rec/ObjTest.java create mode 100644 var/base.j2 create mode 100644 var/favicon.ico diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..4f45250 --- /dev/null +++ b/.classpath @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ba0238 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +.vscode/ +!.vscode/launch.json + +### Java ### +*.class + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +### Gradle ### +.gradle +build/ +dist/ +target/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..698c0c7 --- /dev/null +++ b/.project @@ -0,0 +1,40 @@ + + + jabba + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.buildship.core.gradleprojectnature + + + + 1614986289528 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..9545588 --- /dev/null +++ b/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(LOCAL_INSTALLATION(C\:\\ProgramData\\chocolatey\\lib\\gradle\\tools\\gradle-7.0)) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=C\:/Program Files/AdoptOpenJDK/jdk-15.0.2.7-hotspot +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..4c28b1a --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..ec0c557 --- /dev/null +++ b/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=false diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..3a7f4f9 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.processAnnotations=disabled +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..14b697b --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ab485f7 --- /dev/null +++ b/Makefile @@ -0,0 +1,132 @@ +# Let's try makefiles on Kotlin or JAVA +# we will try to create one sample makefile to compile and create JAR files +# both java and kotlin are jvm - running compiler on single files is bad +# both java and kotlin are diffent from c as the compilers honor package structure not folders +# so we compile in src/main/java but that gets used as base by compiler + +# Project Artifacts - Starts with SRC_FILES +PNAME=jabba +ENTRYPOINT=com.reliancy.jabba.Router +DIR_COMPILE=build +DIR_PACKAGE=dist +DIR_SRC=src +DIR_LIB=target/lib +MANIFEST= +MANIFEST_AUTO=$(DIR_PACKAGE)/manifest.mf + +ASSETS=%.txt %.html %.css %.js %.png %.jsp +# Params +KOTLIN_BASE=$(DIR_SRC)/main/kotlin +KOTLIN_EXT = kt +JAVA_BASE=$(DIR_SRC)/main/java +JAVA_EXT = java +OBJ_EXT = class +JFLAGS=-g +KFLAGS= +KOTLIN_TODO=kotlin_todo.txt +JAVA_TODO=java_todo.txt + +# Tools +PRINT := @printf +RM := @rm -rf +COPY := @cp +MOVE := @mv +JVC := javac +KTC := java -jar $(subst \,/,$(KOTLIN_HOME))/lib/kotlin-compiler.jar +TEST_IF := @test -s +JAR := @jar -cvfm +MAKEDIR := @mkdir -p +CFIND := find +FIND := @$(CFIND) + +# File Sets +ALL_SRC=$(subst \,/,$(shell $(CFIND) $(DIR_SRC) -type f)) +ASSETS_SRC=$(filter $(ASSETS),$(ALL_SRC)) +ASSETS_JAVA:=$(subst $(JAVA_BASE),$(DIR_COMPILE),$(filter $(JAVA_BASE)/%,$(ASSETS_SRC))) +ASSETS_KOTLIN:=$(subst $(KOTLIN_BASE),$(DIR_COMPILE),$(filter $(KOTLIN_BASE)/%,$(ASSETS_SRC))) +SRC_DIRS ?=$(subst \,/,$(shell $(CFIND) $(DIR_SRC) -type d -print)) +KOTLIN_SRC=$(foreach dir,$(SRC_DIRS),$(filter $(KOTLIN_BASE)/%,$(wildcard $(dir)/*.$(KOTLIN_EXT)))) +KOTLIN_OBJ=$(KOTLIN_SRC:$(KOTLIN_BASE)/%.$(KOTLIN_EXT)=$(DIR_COMPILE)/%.$(OBJ_EXT)) +JAVA_SRC=$(foreach dir,$(SRC_DIRS),$(filter $(JAVA_BASE)/%,$(wildcard $(dir)/*.$(JAVA_EXT)))) +JAVA_OBJ=$(JAVA_SRC:$(JAVA_BASE)/%.$(JAVA_EXT)=$(DIR_COMPILE)/%.$(OBJ_EXT)) +LIBS=$(foreach dir,$(DIR_LIB),$(wildcard $(dir)/*.jar)) +SPACE=$() $() +CLASSPATH=$(subst $(SPACE),;,$(LIBS)) +JFLAGS:= -cp '$(CLASSPATH)' +KFLAGS:= -cp '$(CLASSPATH)' + +# Common Targets +.SUFFIXES: + +# rule to build kotlin +$(DIR_COMPILE)/%.$(OBJ_EXT): $(KOTLIN_BASE)/%.$(KOTLIN_EXT) + $(PRINT) '\tCollecting source: %s\n' $< + $(PRINT) "$<\n" >> $(DIR_COMPILE)/$(KOTLIN_TODO) +# rule to build java +$(DIR_COMPILE)/%.$(OBJ_EXT): $(JAVA_BASE)/%.$(JAVA_EXT) + $(PRINT) '\tCollecting source: %s\n' $< + $(PRINT) "$<\n" >> $(DIR_COMPILE)/$(JAVA_TODO) +# rule to move assets - observe static pattern rules +$(ASSETS_KOTLIN): $(DIR_COMPILE)/% : $(KOTLIN_BASE)/% + $(MAKEDIR) $(dir $@) + $(COPY) --update $< $@ && printf "\tCopy asset:$< -> $@\n" +$(ASSETS_JAVA): $(DIR_COMPILE)/% : $(JAVA_BASE)/% + $(MAKEDIR) $(dir $@) + $(COPY) --update $< $@ && printf "\tCopy asset:$< -> $@\n" + +#default: dist +package: compile | $(DIR_PACKAGE) + $(PRINT) "Packaging\n" + $(RM) $(DIR_COMPILE)/$(KOTLIN_TODO) + $(RM) $(DIR_COMPILE)/$(JAVA_TODO) +# $(FIND) $(BUILD_DIR) -name "META-INF" -exec rm -rf {} + +ifeq ($(MANIFEST),) + $(PRINT) "Manifest-Version: 1.0\n" >> $(MANIFEST_AUTO) + $(PRINT) "Created-By: Reliancy Makefile\n" >> $(MANIFEST_AUTO) + $(PRINT) "Class-Path: $(foreach lib,$(LIBS),lib/$(notdir $(lib)))\n" >> $(MANIFEST_AUTO) +ifneq ($(ENTRYPOINT),) + $(PRINT) "Main-Class: $(ENTRYPOINT)\n" >> $(MANIFEST_AUTO) +endif +else + $(COPY) $(MANIFEST) $(MANIFEST_AUTO) +endif + $(JAR) $(DIR_PACKAGE)/$(PNAME).jar $(MANIFEST_AUTO) -C $(DIR_COMPILE) . +compile: compile_kotlin compile_java + $(PRINT) "Compiling done.\n" + +compile_kotlin: $(KOTLIN_OBJ) | $(DIR_COMPILE) $(ASSETS_KOTLIN) +ifneq ($(KOTLIN_OBJ),) +# $(PRINT) "KtSrc:$(KOTLIN_SRC)\n" +# $(PRINT) "KtObj:$(KOTLIN_OBJ)\n" + $(PRINT) 'Building Kotlin\n' + $(KTC) $(KFLAGS) -d $(DIR_COMPILE) @$(DIR_COMPILE)/$(KOTLIN_TODO) +endif + +compile_java: $(JAVA_OBJ) | $(DIR_COMPILE) $(ASSETS_JAVA) +ifneq ($(JAVA_OBJ),) + $(PRINT) "JavaSrc:$(JAVA_SRC)\n" + $(PRINT) "JavaObj:$(JAVA_OBJ)\n" + $(PRINT) "Building Java\n" + @$(JVC) $(JFLAGS) -d $(DIR_COMPILE) @$(DIR_COMPILE)/$(JAVA_TODO) +endif + +$(DIR_COMPILE): + $(PRINT) "prepare dir: $(DIR_COMPILE).\n" + $(MAKEDIR) $(DIR_COMPILE) + $(RM) $(DIR_COMPILE)/$(KOTLIN_TODO) + $(RM) $(DIR_COMPILE)/$(JAVA_TODO) + +$(DIR_PACKAGE): + $(PRINT) "prepare dir: $(DIR_PACKAGE).\n" + $(MAKEDIR) $(DIR_PACKAGE) + $(MAKEDIR) $(DIR_PACKAGE)/lib + $(COPY) $(LIBS) $(DIR_PACKAGE)/lib/ + +run: + $(PRINT) "Running\n" +test: + $(PRINT) "Testing\n" +clean: + $(RM) $(DIR_COMPILE) + $(RM) $(DIR_PACKAGE) +.PHONY: package compile dirs run test clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..40f9451 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Jabba the easy going java web app plumber +Jabba is a java library that gets its inspiration from Pythong flask. It will expose all the elementary features needed for deveopment of web apps and microservices. + +# How to Run Things +* running a build via: gradle jar +* running a test via: gradle test +* running a continouse server via: gradle runServer, then work on code + +## Things Left to Do +* ~~Complete support for demarshalling and marshalling of objects to java methods~~ on 10/4/2021 +* ~~Session middleware~~ +* Auth middleware supporting basic and digest, an security entities +* ~~Static file serving~~ +* ~~Templating like jinja~~ +* Database layer or serial/deserial system like SQL Alchemy \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f537589 --- /dev/null +++ b/build.gradle @@ -0,0 +1,181 @@ +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'application' +mainClassName = 'com.reliancy.jabba.Router' +repositories { + mavenLocal() + mavenCentral() +} +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + implementation "org.eclipse.jetty:jetty-server:11.0.1" + implementation "org.slf4j:slf4j-simple:2.0.0-alpha0" + implementation 'com.hubspot.jinjava:jinjava:2.5.10' + implementation 'com.hubspot.jinjava:jinjava:2.5.10' + implementation 'com.h2database:h2:1.4.200' + // https://mvnrepository.com/artifact/org.postgresql/postgresql + implementation 'org.postgresql:postgresql:42.3.1' + // https://mvnrepository.com/artifact/com.zaxxer/HikariCP + implementation 'com.zaxxer:HikariCP:5.0.0' + + testImplementation "junit:junit:4.12" +} +test { + testLogging { + // Make sure output from + // standard out or error is shown + // in Gradle output. + outputs.upToDateWhen {false} + showStandardStreams = true + exceptionFormat = 'full' + // Or we use events method: + // events 'standard_out', 'standard_error' + + // Or set property events: + // events = ['standard_out', 'standard_error'] + + // Instead of string values we can + // use enum values: + // events org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_OUT, + // org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR, + } +} +jar { + archiveBaseName = 'jabba' + archiveVersion = '0.1' + manifest { + attributes "Main-Class": mainClassName + attributes "Class-Path": configurations.runtimeClasspath.collect { it.getName() }.join(' ') + } +} +task copyToLib(type: Copy) { + into "${buildDir}/libs" from configurations.runtimeClasspath +} +build.dependsOn(copyToLib) +task fat_jar(type: Jar) { + archiveBaseName = 'fat-jabba' + archiveVersion = '0.1' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + /* + manifest { + attributes "Main-Class": mainClassName + attributes "Class-Path": configurations.runtimeClasspath.collect { it.getName() }.join(' ') + } + */ + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + }{ + exclude "META-INF/NOTICE.txt" + exclude "META-INF/LICENSE" + } + with jar +} +/** +We define a singleton pattern using background thread to launch a blocking process. +Later we make sure to terminate previous running driver threads before starting new. +Also we split start, stop because we might not run in continouse mode. + */ +class Server implements Runnable{ + static Server singleton=null; + public static Server getSingleton(){ + if(singleton==null){ + System.out.println("creating new server"); + singleton=new Server(); + } + return singleton; + } + org.slf4j.Logger log=org.slf4j.LoggerFactory.getLogger("server.driver"); + Thread driver=null; + Runnable task=null; + protected Server(){} + public void info(String msg){ + System.out.println(msg); + } + public Server useDriver(boolean f){ + info("using driver:"+f); + if(f){ + driver=new Thread(this); + driver.setName("server.driver"); + }else{ + driver=null; + } + return this; + } + public void run(){ + info("running task"); + try{ + task.run(); + }catch(java.lang.InterruptedException ex){ + info("running task:interrupted"); + }catch(org.gradle.internal.UncheckedException ex2){ + info("running task:interrupted2"); + } + } + public Server start(Runnable c){ + info("starting server"); + this.task=c; + if(driver!=null) + driver.start(); + else + this.run(); + return this; + } + public Server stop(){ + info("stopping server"); + if(driver!=null){ + driver.interrupt(); + driver.join(); + } + for(Thread th:Thread.getAllStackTraces().keySet()){ + if(th.getName().equalsIgnoreCase("executor")){ + info("cleaning up stale driver:"+th.toString()) + th.stop(); + } + if(th.getName().equalsIgnoreCase("server.driver")){ + info("cleaning up stale driver:"+th.toString()) + th.stop(); + } + } + return this; + } +} +task runServer{ + inputs.files 'src' + doFirst { + //println 'This is executed first during the execution phase.' + Server.getSingleton().stop(); + Server.getSingleton().useDriver(project.gradle.startParameter.continuous); + } + doLast { + //println 'This is executed last during the execution phase.' + Server.getSingleton().start({ + project.javaexec { + classpath = project.sourceSets.main.runtimeClasspath + main = mainClassName + } + }); + } + /* + group = 'Run' // <-- change the name as per your need + description = 'execute run but continously' + classpath sourceSets.main.runtimeClasspath // <-- Don't change this + main = mainClassName + //args "arg1", "arg2" + */ +} +// build.gradle +eclipse.classpath { + defaultOutputDir = file("build") ///default + file.whenMerged { cp -> + cp.entries.forEach { cpe -> + if (cpe instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder) { + cpe.output = cpe.output.replace "bin/", "build/classes/java/" + } + if (cpe instanceof org.gradle.plugins.ide.eclipse.model.Output) { + cpe.path = cpe.path.replace "bin/", "build/" + } + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c30db1a --- /dev/null +++ b/pom.xml @@ -0,0 +1,145 @@ + + + + 4.0.0 + + com.reliancy.jabba + jabba + 1.0 + jar + jabba + http://www.reliancy.com + + + UTF-8 + 1.8 + 1.8 + + + + + junit + junit + 4.11 + test + + + org.eclipse.jetty + jetty-server + 11.0.7 + + + org.eclipse.jetty + jetty-servlet + 11.0.1 + + + com.hubspot.jinjava + jinjava + 2.5.10 + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + + copy-dependencies + package + + copy-dependencies + + + + + ${project.build.directory}/lib + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.4.1 + + + + jar-with-dependencies + + + + + com.reliancy.jabba.Router + + + + + + + make-assembly + + package + + single + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + com.reliancy.jabba.Router + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/src/main/java/com/reliancy/dbo/Action.java b/src/main/java/com/reliancy/dbo/Action.java new file mode 100644 index 0000000..b6ea1d9 --- /dev/null +++ b/src/main/java/com/reliancy/dbo/Action.java @@ -0,0 +1,164 @@ +package com.reliancy.dbo; + +import java.io.IOException; +import java.util.Iterator; + +import com.reliancy.util.CloseableIterator; + +/** Description of a terminal operation with a slice of dbo objects as input or output. + * This object is not just for reading but also bulk updating. + * It will be used to describe a multi DBO read or write and to then also track results. + */ +public class Action implements Iterable,CloseableIterator{ + public static enum Type{ + NONE,LOAD,SAVE,DELETE + } + Terminal terminal; + Type type; + Entity entity; + Object[] params; + CloseableIterator items; + int limit,offset; + Condition filter; + + public Action(){ + type=Type.NONE; + } + public Action(Type t){ + type=t; + } + public Action(Terminal t){ + terminal=t; + type=Type.NONE; + } + public Action execute() throws IOException{ + return terminal.execute(this); + } + + public Terminal getTerminal() { + return terminal; + } + public Action setTerminal(Terminal terminal) { + this.terminal = terminal; + return this; + } + public Type getType() { + return type; + } + public Action setType(Type type) { + this.type = type; + return this; + } + public Entity getEntity() { + return entity; + } + public Action setEntity(Entity entity) { + this.entity = entity; + return this; + } + public void clear(){ + terminal=null; + type=Type.NONE; + entity=null; + setItems((DBO)null); + } + public Action load(Entity ent){ + type=Type.LOAD; + entity=ent; + return this; + } + public Action save(Entity ent){ + type=Type.SAVE; + entity=ent; + return this; + } + public Action delete(Entity ent){ + type=Type.DELETE; + entity=ent; + return this; + } + public Action params(Object...p){ + params=p; + return this; + } + public Action setItems(DBO ...itms){ + CloseableIterator it=null; + if(itms!=null){ + it=new CloseableIterator() { + private int index = 0; + @Override + public boolean hasNext() { + return itms.length > index; + } + @Override + public DBO next() { + return itms[index++]; + } + @Override + public void close() throws IOException { + } + }; + } + return setItems(it); + } + public Action setItems(CloseableIterator itms){ + if(items!=null){ + try { + items.close(); + } catch (Exception e) { + } + } + items=itms; + return this; + } + protected CloseableIterator getItems(){ + return items; + } + @Override + public Iterator iterator() { + return this; + } + @Override + public boolean hasNext() { + return items!=null?items.hasNext():false; + } + @Override + public DBO next() { + return items.next(); + } + @Override + public void close() throws IOException { + if(items!=null){ + items.close(); + if(terminal!=null) terminal.end(this); + } + items=null; + } + public Action limit(int max) { + limit=max; + return this; + } + public Action if_filter(Condition... c){ + if(c!=null){ + if(c.length>1) filter=Condition.and(c); + else filter=c[0]; + }else{ + filter=null; + } + return this; + } + public Action if_pk(Object[] id) { + Field pk=entity.getPk(); + return if_filter(Condition.eq(pk,id)); + } + public DBO first() { + try{ + return items!=null?items.next():null; + }finally{ + clear(); + } + } + public boolean isDone(){ + return items==null || items.hasNext()==false; + } +} diff --git a/src/main/java/com/reliancy/dbo/Condition.java b/src/main/java/com/reliancy/dbo/Condition.java new file mode 100644 index 0000000..d527327 --- /dev/null +++ b/src/main/java/com/reliancy/dbo/Condition.java @@ -0,0 +1,81 @@ +package com.reliancy.dbo; + +/** constraint on a field. + * conditions can be leafs or groups such as and,or,not + */ +public class Condition { + public static abstract class Op{ + public abstract boolean met(Condition c); + } + public static Op AND=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op OR=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op NOT=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op EQ=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op NEQ=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op GT=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op GTE=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op LT=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op LTE=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op LIKE=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Op IN=new Op(){ + public boolean met(Condition c){ + return true; + } + }; + public static Condition and(Condition... c) { + return new Condition(AND,c); + } + public static Condition eq(Field pk, Object... id) { + return new Condition(EQ,pk,id); + } + Op code; + Object[] args; + public Condition(Op code,Field f,Object val){ + this.code=code; + args=new Object[]{f,val}; + } + public Condition(Op code,Condition ... sub){ + this.code=code; + args=sub; + } +} diff --git a/src/main/java/com/reliancy/dbo/DBO.java b/src/main/java/com/reliancy/dbo/DBO.java new file mode 100644 index 0000000..3b27a47 --- /dev/null +++ b/src/main/java/com/reliancy/dbo/DBO.java @@ -0,0 +1,34 @@ +package com.reliancy.dbo; + + +/** Instance of an entity, usually a row in a table. + * + */ +public class DBO{ + public static enum Status{ + NEW,USED,DELETED,COMPUTED + } + Terminal terminal; + Entity type; + Status status; + public DBO() { + } + public Terminal getTerminal() { + return terminal; + } + public void setTerminal(Terminal terminal) { + this.terminal = terminal; + } + public Entity getType() { + return type; + } + public void setType(Entity type) { + this.type = type; + } + public Status getStatus(){ + return status; + } + public void setStatus(Status s) { + this.status = s; + } +} diff --git a/src/main/java/com/reliancy/dbo/Entity.java b/src/main/java/com/reliancy/dbo/Entity.java new file mode 100644 index 0000000..60a4b62 --- /dev/null +++ b/src/main/java/com/reliancy/dbo/Entity.java @@ -0,0 +1,41 @@ +package com.reliancy.dbo; + +import java.util.HashMap; + +import com.reliancy.rec.Hdr; + +/** Describes an object structure, usually a table. + * + */ +public class Entity extends Hdr{ + static final HashMap registry=new HashMap<>(); + public static final void publish(Entity ent){ + registry.put(ent.getName(),ent); + } + public static final void retract(Entity ent){ + registry.values().remove(ent); + } + public static final Entity recall(String name){ + return registry.get(name); + } + public static final Entity recall(Class cls){ + return recall(cls.getSimpleName()); + } + /** + * this method will analyze a DBO class and forumate an Entity object out of it. + * @param cls + * @return + */ + public static final Entity publish(Class cls){ + return null; + } + Entity base; + String dbName; + Field pk; + public Entity(String name) { + super(name); + } + public Field getPk(){ + return pk; + } +} diff --git a/src/main/java/com/reliancy/dbo/Field.java b/src/main/java/com/reliancy/dbo/Field.java new file mode 100644 index 0000000..b2a8239 --- /dev/null +++ b/src/main/java/com/reliancy/dbo/Field.java @@ -0,0 +1,16 @@ +package com.reliancy.dbo; + +import com.reliancy.rec.Slot; +/** + * Description of a column or property. + */ +public class Field extends Slot { + + public Field(String name) { + super(name); + } + public Field(String name,Class typ) { + super(name,typ); + } + +} diff --git a/src/main/java/com/reliancy/dbo/SQLTerminal.java b/src/main/java/com/reliancy/dbo/SQLTerminal.java new file mode 100644 index 0000000..a4576b1 --- /dev/null +++ b/src/main/java/com/reliancy/dbo/SQLTerminal.java @@ -0,0 +1,36 @@ +package com.reliancy.dbo; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; + +import com.reliancy.util.Path; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +public class SQLTerminal implements Terminal{ + HikariConfig config = new HikariConfig(); + HikariDataSource ds; + Path url; + public SQLTerminal(String url){ + this.url=new Path(url); + String proto=this.url.getProtocol(); + if(!proto.startsWith("jdbc:")) proto="jdbc:"+proto; + String u=proto+"://"+this.url.getHost()+":"+this.url.getPort()+"/"+this.url.getDatabase(); + config.setJdbcUrl(u); + config.setUsername(this.url.getUserid()); + config.setPassword(this.url.getPassword()); + config.addDataSourceProperty( "cachePrepStmts" , "true" ); + config.addDataSourceProperty( "prepStmtCacheSize" , "250" ); + //config.addDataSourceProperty( "prepStmtCacheSqlLimit" , "2048" ); + ds = new HikariDataSource( config ); + } + public Connection getConnection() throws SQLException{ + return ds.getConnection(); + } + @Override + public Action execute(Action q) throws IOException { + return q; + } + +} diff --git a/src/main/java/com/reliancy/dbo/Terminal.java b/src/main/java/com/reliancy/dbo/Terminal.java new file mode 100644 index 0000000..8353c67 --- /dev/null +++ b/src/main/java/com/reliancy/dbo/Terminal.java @@ -0,0 +1,48 @@ +package com.reliancy.dbo; + +import java.io.IOException; + +/** Endpoint for dbo objects. + * this interface will implemet CRUD plus control over a databse or folder. + * control will be implemented via meta terminal which will return specialized terminals for each entity and running actions on it + * will modify the entity structure. + * + * the core of the temrminal will be the Action object. The others will just be wrappers for since item actions. + * the action will be a read or write query with session management. + */ +public interface Terminal { + public Action execute(Action q) throws IOException; + + public default Action begin(){ + return begin(null); + } + public default Action begin(String sig){ + return new Action(this); + } + public default void end(Action act){ + act.clear(); + } + public default Terminal meta(Entity ent){ + return null; + } + public default DBO load(Entity ent,Object...id) throws IOException { + String sig="/"+ent.getName()+"/load"; + try(Action act=begin(sig).load(ent).limit(1).if_pk(id).execute()){ + return act.first(); + } + } + public default boolean save(DBO rec) throws IOException{ + Entity ent=rec.getType(); + String sig="/"+ent.getName()+"/save"; + try(Action act=begin(sig).save(ent).setItems(rec).execute()){ + return act.isDone(); + } + } + public default boolean delete(DBO rec) throws IOException { + Entity ent=rec.getType(); + String sig="/"+ent.getName()+"/delete"; + try(Action act=begin(sig).delete(ent).setItems(rec).execute()){ + return act.isDone(); + } + } +} diff --git a/src/main/java/com/reliancy/jabba/AppSession.java b/src/main/java/com/reliancy/jabba/AppSession.java new file mode 100644 index 0000000..8d86930 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/AppSession.java @@ -0,0 +1,67 @@ +package com.reliancy.jabba; + +import java.util.HashMap; + +import com.reliancy.jabbasec.SecurityActor; + +public class AppSession implements Session{ + final String id; + final HashMap values; + long timeCreated; + long lastActive; + long maxAge; + SecurityActor user; + + public AppSession(String id){ + this.id=id; + values=new HashMap<>(); + lastActive=timeCreated=System.currentTimeMillis(); + maxAge=1000*60*15; + } + @Override + public void setValue(String key, Object val) { + if(val!=null) values.put(key,val); + else values.remove(key); + } + @Override + public Object getValue(String key) { + return values.get(key); + } + public long getTimeInactive(){ + return System.currentTimeMillis()-lastActive; + } + public void setLastActive(){ + lastActive=System.currentTimeMillis(); + } + public boolean isExpired(){ + return getTimeInactive()>maxAge; + } + /** allows specialized appsessions to register with more ids. */ + protected void onPublish(HashMap directory){ + } + /** allows specialized appsessions to deregister with more ids. */ + protected void onRetract(HashMap directory){ + } + static HashMap instances=new HashMap<>(); + public static AppSession getInstance(String id){ + return instances.get(id); + } + public static void setInstance(String id,AppSession ss){ + AppSession old=getInstance(id); + if(ss!=null){ + if(ss==old) return; // already published + instances.put(id,ss); + ss.onPublish(instances); + }else{ + if(ss==old) return; // already retracted + old.onRetract(instances); + instances.remove(id); + } + } + public SecurityActor getUser() { + return user; + } + public void setUser(SecurityActor user){ + this.user=user; + } +} diff --git a/src/main/java/com/reliancy/jabba/AppSessionFilter.java b/src/main/java/com/reliancy/jabba/AppSessionFilter.java new file mode 100644 index 0000000..279bfa4 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/AppSessionFilter.java @@ -0,0 +1,50 @@ +package com.reliancy.jabba; + +import java.io.IOException; +import java.util.UUID; + +/** AppSession middleware will inject an appsession object into callsession. + * During each request,response we will if not alrady present extract a cookie or param + * and based on it install an app wide sesson dictionary. + */ +public class AppSessionFilter extends Processor{ + public static final String KEY_NAME="jbssid"; + public AppSessionFilter() { + super(AppSessionFilter.class.getSimpleName().toLowerCase()); + } + @Override + public void before(Request request, Response response) throws IOException { + String ssid=(String)request.getParam(KEY_NAME,null); + if(ssid==null){ + UUID uuid = UUID.randomUUID(); + ssid=uuid.toString(); + } + AppSession ss=AppSession.getInstance(ssid); + if(ss!=null){ + if(ss.isExpired()){ + // this app sessin expired - create a new one + ss=new AppSession(ssid); + AppSession.setInstance(ssid, ss); + }else{ + // this session is good + ss.setLastActive(); + } + }else{ + // no session available + ss=new AppSession(ssid); + AppSession.setInstance(ssid, ss); + } + CallSession css=CallSession.getInstance(); + css.setAppSession(ss); + } + @Override + public void after(Request request, Response response) throws IOException { + CallSession css=CallSession.getInstance(); + AppSession ss=(AppSession) css.getAppSession(); + response.setCookie(KEY_NAME,ss.id,15*60,false); + } + @Override + public void serve(Request request, Response response) throws IOException{ + + } +} diff --git a/src/main/java/com/reliancy/jabba/CallSession.java b/src/main/java/com/reliancy/jabba/CallSession.java new file mode 100644 index 0000000..80eb980 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/CallSession.java @@ -0,0 +1,84 @@ +package com.reliancy.jabba; + +import java.util.ArrayList; + +/** + * Thread local object that lets us access some variables in specialized handler methods. + * For example request and response objects are accessible. + * The session is updated at process phase of each processor. + */ +public class CallSession implements Session{ + ArrayList callers=new ArrayList<>(); + Session appSession; + Request request; + Response response; + + public CallSession(){ + } + protected void end(){ + appSession=null; + request=null; + response=null; + callers.clear(); + } + protected void begin(Session ss,Request req,Response resp){ + appSession=ss; + request=req; + response=resp; + callers.clear(); + } + protected void enter(Processor c){callers.add(c);} + protected void leave(Processor c){ + int len=callers.size(); + int at=len-1; + Processor last=len>0?callers.get(at):null; + if(c!=null && c==last){ + callers.remove(len-1); + }else if(len>0 && (at=callers.indexOf(c))!=-1){ + // bad last is not same c, some processors have not left properly + do{ + last=callers.remove(callers.size()-1); + }while(last!=c); + } + } + @Override + public void setValue(String key, Object val) { + if(appSession!=null) appSession.setValue(key, val); + } + @Override + public Object getValue(String key) { + return appSession!=null?appSession.getValue(key):null; + } + public void setAppSession(Session ss) { + appSession=ss; + } + public Session getAppSession() { + return appSession; + } + public Request getRequest() { + return request; + } + public void setRequest(Request request) { + this.request = request; + } + public Response getResponse() { + return response; + } + public void setResponse(Response response) { + this.response = response; + } + public Processor getCaller() { + int len=callers.size(); + return len>0?callers.get(len-1):null; + } + /** + * Will return current session given the call stack. + * @return + */ + public static ThreadLocal instance=new ThreadLocal<>(); + public static CallSession getInstance(){ + CallSession ret=instance.get(); + if(ret==null) instance.set(ret=new CallSession()); + return ret; + } +} diff --git a/src/main/java/com/reliancy/jabba/Config.java b/src/main/java/com/reliancy/jabba/Config.java new file mode 100644 index 0000000..48aef79 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/Config.java @@ -0,0 +1,9 @@ +package com.reliancy.jabba; + +public interface Config { + public void load(); + public void save(); + public String getId(); + public Object getProperty(String key,Object def); + public Config setProperty(String key,Object val); +} diff --git a/src/main/java/com/reliancy/jabba/EndPoint.java b/src/main/java/com/reliancy/jabba/EndPoint.java new file mode 100644 index 0000000..8e87665 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/EndPoint.java @@ -0,0 +1,18 @@ +package com.reliancy.jabba; + +import java.io.IOException; + +public abstract class EndPoint extends Processor{ + + public EndPoint(String id) { + super(id); + } + @Override + public void before(Request request, Response response) throws IOException { + } + @Override + public void after(Request request, Response response) throws IOException { + } + public abstract void serve(Request request, Response response) throws IOException; + +} diff --git a/src/main/java/com/reliancy/jabba/FileServer.java b/src/main/java/com/reliancy/jabba/FileServer.java new file mode 100644 index 0000000..3d4d852 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/FileServer.java @@ -0,0 +1,117 @@ +package com.reliancy.jabba; +import com.reliancy.util.Resources; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Iterator; +import java.util.stream.Stream; + +public class FileServer extends EndPoint implements Resources.PathRewrite{ + public static interface Filter{ + boolean isAcceptable(String path); + } + public static class ExtFilter implements Filter{ + final String[] allowed; + public ExtFilter(String ...ext){allowed=ext;} + @Override + public boolean isAcceptable(String path) { + if(allowed.length==0) return true; + for(String ext:allowed) if(path.endsWith(ext)) return true; + return false; + } + } + public static Filter NOFILTER=new ExtFilter(); + String diskPrefix; + String classPrefix; + final HashMap map; + final HashMap filt; + public FileServer(String url_path,Filter f,Object ... disk_path){ + super("fileserver"); + diskPrefix=classPrefix=null; + filt=new HashMap<>(); + map=new HashMap<>(); + addRoute(url_path,f, disk_path); + } + public FileServer(String url_path,Object ... disk_path){ + this(url_path,NOFILTER,disk_path); + } + @Override + public void serve(Request request, Response response) throws IOException { + String path=request.getPath(); + log().info("serving:"+path); + for(String prefix:map.keySet()){ + boolean match=path.startsWith(prefix); + if(match){ + Object[] sp=getSearchPath(prefix); + String rpath=path.replace(prefix,""); + if(!filt.get(prefix).isAcceptable(rpath)) continue; // not acceptable to filter + URL f=Resources.findFirst(this, rpath, sp); + if(f==null) continue; // skip if rpath not located + System.out.println("RES:"+f); + writeResource(f,response); + return; + } + } + response.setStatus(Response.HTTP_NOT_FOUND); + response.getEncoder().writeln("missing file:{0}",path); + } + /** + * we prefix our path for disk and class contexts. + */ + @Override + public String rewritePath(String path, Object context) { + if(diskPrefix!=null && context instanceof String) return this.diskPrefix+path; + if(diskPrefix!=null && context instanceof File) return this.diskPrefix+path; + if(classPrefix!=null && context instanceof Class) return this.classPrefix+path; + return path; + } + public FileServer setDiskPrefix(String prefix){ + diskPrefix=prefix; + return this; + } + public FileServer setClassPrefix(String prefix){ + classPrefix=prefix; + return this; + } + /** + * Will render a file to response. + * @param f + * @param response + */ + protected void writeResource(URL f, Response response) throws IOException{ + //log().info("writing:"+f); + ResponseEncoder enc=response.getEncoder(); + try(InputStream is=f.openStream()){ + String ctype=HTTP.guess_mime(f); + response.setStatus(Response.HTTP_OK); + response.setContentType(ctype); + enc.writeStream(is); + } + } + public final void addRoute(String url_path,Filter f,Object... disk_path){ + if(disk_path!=null){ + map.put(url_path,disk_path); + filt.put(url_path,f!=null?f:NOFILTER); + }else{ + map.remove(url_path); + filt.remove(url_path); + } + } + public Object[] getSearchPath(String url_path){ + return map.get(url_path); + } + public Filter getFilter(String url_path){ + return filt.get(url_path); + } + public Stream streamRoutes() { + return map.keySet().stream(); + } + public Iterator enumRoutes(){ + return map.keySet().iterator(); + } + public void exportRoutes(RouterEndPoint rep) { + streamRoutes().forEach(up->rep.addRoute("GET",up+".*",this)); + } +} diff --git a/src/main/java/com/reliancy/jabba/HTTP.java b/src/main/java/com/reliancy/jabba/HTTP.java new file mode 100644 index 0000000..e0bfab3 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/HTTP.java @@ -0,0 +1,65 @@ +package com.reliancy.jabba; + +import java.io.File; +import java.net.URL; +import java.util.HashMap; + +/** HTTP related methods and classes. */ +public final class HTTP { + public static HashMap MIME_MAP=new HashMap<>(); + public static class Header{ + public String key; + public String value; + public Header(String k, String v){ + key=k;value=v; + } + } + public static class Cookie{ + public String key; + public String value; + public int maxAge; + public boolean secure; + public Cookie(String k,String v, int maxAge, boolean sec){ + key=k;value=v;this.maxAge=maxAge;secure=sec; + } + } + public static String ext2mime(String ext){ + if(MIME_MAP.isEmpty()){ + MIME_MAP.put("ico","image/x-icon"); + MIME_MAP.put("js","application/javascript"); + MIME_MAP.put("doc","application/msword"); + MIME_MAP.put("pdf","application/pdf"); + MIME_MAP.put("zip","application/zip"); + MIME_MAP.put("gif","image/gif"); + MIME_MAP.put("jpg","image/jpeg"); + MIME_MAP.put("jpeg","image/jpeg"); + MIME_MAP.put("png","image/png"); + MIME_MAP.put("webp","image/webp"); + MIME_MAP.put("txt","text/plain"); + MIME_MAP.put("css","text/css"); + MIME_MAP.put("csv","text/csv"); + MIME_MAP.put("html","text/html"); + MIME_MAP.put("htm","text/html"); + MIME_MAP.put("xml","text/xml"); + } + return MIME_MAP.get(ext); + } + public static String guess_mime(Object ret) { + if(ret instanceof CharSequence){ + CharSequence retstr=(CharSequence)ret; + if(retstr.length()>0 && retstr.charAt(0)=='<') return "text/html"; + if(retstr.length()>0 && "{[".indexOf(retstr.charAt(0))!=-1) return "application/json"; + return "text/plain"; + } + if(ret instanceof byte[]){ + return "application/octet-stream"; + } + if(ret instanceof File || ret instanceof URL){ + String path=String.valueOf(ret); + String ext=path.substring(path.lastIndexOf(".")+1).toLowerCase(); + String mime=ext2mime(ext); + return mime!=null?mime:"application/octet-stream"; + } + return null; + } +} diff --git a/src/main/java/com/reliancy/jabba/MethodEndPoint.java b/src/main/java/com/reliancy/jabba/MethodEndPoint.java new file mode 100644 index 0000000..72a48b9 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/MethodEndPoint.java @@ -0,0 +1,102 @@ +package com.reliancy.jabba; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import com.reliancy.util.Handy; + +public class MethodEndPoint extends EndPoint{ + enum InvokeProfile{ + PLAIN, // no return, request, response as argument + NOARG, // no arguments, possible return + FULL, // one or more arguments need to do casting + + } + Route route; + Object target; + Method method; + Parameter[] params; + Class retType; + InvokeProfile invokeType; + + public MethodEndPoint(Object target,Method m,Route r) { + super(target.getClass().getSimpleName()+"."+m.getName()); + this.route=r; + this.target=target; + this.method=m; + this.params=m.getParameters(); + this.retType=m.getReturnType(); + this.invokeType=InvokeProfile.FULL; + if(params.length==2 && params[0].getType()==Request.class && params[1].getType()==Response.class){ + invokeType=InvokeProfile.PLAIN; + } + if(params.length==0){ + invokeType=InvokeProfile.NOARG; + } + } + + @Override + public void serve(Request request, Response response) throws IOException{ + log().info("Serving method....{}",invokeType); + try{ + Object ret=null; + switch(invokeType){ + case PLAIN:{ // plain profile just passes req,resp + method.invoke(target,request,response); + break; + } + case NOARG:{ // no args will not pass any arguments, will deal with return (marshalling) + ret=method.invoke(target); + encodeResponse(ret,response); + break; + } + default:{ + // here we do full unmarshalling, marshalling + Object[] argVals=decodeRequest(request); + ret=method.invoke(target,argVals); + encodeResponse(ret,response); + } + } + }catch(Exception ex2){ + if(ex2 instanceof IOException) throw ((IOException)ex2); + else throw new IOException(ex2); + } + } + public String getPath() { + String ret=route.path(); + if(!ret.startsWith("/")) ret="/"+ret; + ret=ret.replace("{method}",method.getName()); + return ret; + } + protected Object[] decodeRequest(Request request){ + Object[] argVals=new Object[params.length]; + for(int i=0;i cls=p.getType(); + String byName=p.getName(); + String byPos="_arg"+i; + Object val=request.getParam(byName,request.getParam(byPos,null)); // get by name or pos + argVals[i]=Handy.normalize(cls,val); + } + return argVals; + } + protected void encodeResponse(Object ret, Response response) throws IOException{ + if(ret instanceof Response){ + // we have a response return - take its status and content type + Response resp=(Response)ret; + if(resp!=response){ + response.setStatus(resp.getStatus()); + response.setContentType(resp.getContentType()); + resp.exportContent(response.getEncoder()); + } + }else{ + // we do not have a response but must set status, content type + String ctype=route.return_mime(); + if(Handy.isBlank(ctype)) ctype=HTTP.guess_mime(ret); + response.setContentType(ctype); + if(ret!=null){ + response.getEncoder().writeObject(ret); + } + } + } +} diff --git a/src/main/java/com/reliancy/jabba/Path.java b/src/main/java/com/reliancy/jabba/Path.java new file mode 100644 index 0000000..f460e80 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/Path.java @@ -0,0 +1,86 @@ +package com.reliancy.jabba; + +/** uri path decoded into tokens. */ +public class Path { + String url; + String schema; + String host; + String db; + String query; + String[] db_parts; + public Path(String url) { + this.url = url.trim(); + db=this.url; + // extract schema + int schi=db.indexOf("://"); + if(schi!=-1){ + schema=db.substring(0,schi); + db=db.substring(schi+3); + } + //extract host + schi=db.indexOf("/"); + if(schi>0){ + host=db.substring(0,schi); + db=db.substring(schi); + } + // extract query + schi=db.indexOf("?"); + if(schi>0){ + query=db.substring(schi+1); + db=db.substring(0,schi); + } + db_parts=db.split("/"); + } + public String toString(){ + return getURL(); + } + public String getURL() { + if(url==null){ + StringBuilder buf=new StringBuilder(); + if(schema!=null) buf.append(schema).append("://"); + if(host!=null) buf.append(host); + String db=getDB(); + if(db!=null) buf.append(db); + String q=getQuery(); + if(q!=null) buf.append("?").append(q); + url=buf.toString(); + } + return url; + } + public void setURL(String url) { + this.url = url; + } + public String getSchema() { + return schema; + } + public void setSchema(String schema) { + this.schema = schema; + } + public String getHost() { + return host; + } + public void setHost(String host) { + this.host = host; + } + public String getDB() { + if(db==null && db_parts!=null) db="/"+String.join("/",db_parts); + return db; + } + public void setDB(String db) { + this.db = db; + } + public String[] getDBParts() { + return db_parts; + } + public void setDBParts(String...parts) { + db_parts=parts; + db=null; + } + public String getQuery() { + return query; + } + public void setQuery(String query) { + this.query = query; + } + +} diff --git a/src/main/java/com/reliancy/jabba/Processor.java b/src/main/java/com/reliancy/jabba/Processor.java new file mode 100644 index 0000000..b4f8521 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/Processor.java @@ -0,0 +1,82 @@ +package com.reliancy.jabba; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class Processor { + protected Processor next; + protected String id; + protected boolean active; + protected Config config; + protected Logger logger; + + public Processor(String id){ + next=null; + this.id=id!=null?id:this.getClass().getSimpleName().toLowerCase(); + active=true; + } + public String getId(){ + return id; + } + + public Processor getNext() { + return next; + } + public void setNext(Processor next) { + this.next = next; + } + public boolean isActive() { + return active; + } + public void setActive(boolean active) { + this.active = active; + } + + public Config getConfig() { + return config; + } + /* + public void setConfig(Config config) { + this.config = config; + } + */ + /** + * Main event processing chain. + * Will go down the chain until result code is set. + * @param request + * @param response + * @throws IOException + */ + public void process(Request request,Response response) throws IOException { + CallSession ss=CallSession.getInstance(); + try{ + ss.enter(this); + if(!active){ + if(next!=null) next.process(request, response); + }else{ + before(request, response); + if(response.getStatus()==null) serve(request, response); + if(next!=null && response.getStatus()==null) next.process(request, response); + after(request, response); + } + }finally{ + ss.leave(this); + } + } + public void begin(Config conf){ + this.config=conf; + }; + public void end(){ + this.config=null; + }; + protected Logger log(){ + // prefer local over central one + Logger ret=logger!=null?logger:(config!=null?(Logger)config.getProperty("logger",null):null); + // if none provided install a fresh one locally + if(ret==null) ret=logger=LoggerFactory.getLogger(this.getId()); + return ret; + } + public abstract void before(Request request,Response response) throws IOException; + public abstract void after(Request request,Response response) throws IOException; + public abstract void serve(Request request,Response response) throws IOException; +} diff --git a/src/main/java/com/reliancy/jabba/Request.java b/src/main/java/com/reliancy/jabba/Request.java new file mode 100644 index 0000000..28bdd28 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/Request.java @@ -0,0 +1,79 @@ +package com.reliancy.jabba; + +import java.util.HashMap; +import java.util.Map; + +import com.reliancy.util.Handy; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; + +public class Request { + final HttpServletRequest http_request; + final HashMap pathParams=new HashMap<>(); + public Request(HttpServletRequest http_request) { + this.http_request = http_request; + } + public Map getPathParams(){ + return pathParams; + } + public String getPath() { + return http_request.getPathInfo(); + } + + public String getVerb() { + return http_request.getMethod(); + } + /** + * Look for this parameter in pathParan, queryParams and forms. + * @param pname + * @return + */ + public Object getParam(String pname,Object def){ + if(pathParams.containsKey(pname)) return pathParams.get(pname); + String[] vals=http_request.getParameterValues(pname); + if(vals!=null) return vals.length==1?vals[0]:vals; + String hdr=getHeader(pname); + if(hdr!=null) return hdr; + String cook=getCookie(pname,null); + if(cook!=null) return cook; + return def; + } + public Request setParam(String pname,Object val){ + if(pathParams.containsKey(pname)){ + pathParams.put(pname,String.valueOf(Handy.nz(val,""))); + }else{ + throw new IllegalArgumentException("invalid param name:"+pname); + } + return this; + } + public String getHeader(String key){ + return http_request.getHeader(key); + } + public String getCookie(String name,String def){ + for(Cookie c:http_request.getCookies()){ + if(name.equalsIgnoreCase(c.getName())) return c.getValue(); + } + return def; + } + private static final String[] HEADERS4IP = { + "X-Forwarded-For", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "HTTP_VIA", + "REMOTE_ADDR" }; + public String getRemoteAddress() { + for (String header : HEADERS4IP) { + String ip = getHeader(header); + if(ip==null || ip.length()==0 || "unknown".equalsIgnoreCase(ip)) continue; + return ip.contains(",")?ip.split(",",2)[0]:ip; + } + return http_request.getRemoteAddr(); + } +} diff --git a/src/main/java/com/reliancy/jabba/Response.java b/src/main/java/com/reliancy/jabba/Response.java new file mode 100644 index 0000000..c705dff --- /dev/null +++ b/src/main/java/com/reliancy/jabba/Response.java @@ -0,0 +1,152 @@ +package com.reliancy.jabba; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +/** + * Our representation of the response. + * We usually wrap servlet response with this object and use in write mode. + * But we can also create it with no servletresponse then it represents delayed response to be + * read out later and written somewhere. + */ +public class Response { + // status codes + public static final int HTTP_OK=HttpServletResponse.SC_OK; + public static final int HTTP_BAD_REQUEST=HttpServletResponse.SC_BAD_REQUEST; + public static final int HTTP_NOT_FOUND=HttpServletResponse.SC_NOT_FOUND; + public static final int HTTP_UNAUTHORIZED=HttpServletResponse.SC_UNAUTHORIZED; + public static final int HTTP_FORBIDDEN=HttpServletResponse.SC_FORBIDDEN; + + final protected HttpServletResponse http_response; + final protected Writer char_response; + final protected OutputStream byte_response; + protected ResponseEncoder encoder; + protected String content_type; + protected Integer status; + protected final ArrayList headers=new ArrayList<>(); + protected final ArrayList cookies=new ArrayList<>(); + + public Response(HttpServletResponse http_response) { + this.http_response = http_response; + this.char_response=null; + this.byte_response=null; + } + public Response(Writer w) { + this.http_response = null; + this.char_response=w; + this.byte_response=null; + } + public Response(OutputStream w) { + this.http_response = null; + this.char_response=null; + this.byte_response=w; + } + public Response() { + this.http_response = null; + this.char_response=new StringWriter(); + this.byte_response=null; + } + public ResponseEncoder getEncoder(){ + if(encoder==null) encoder=new ResponseEncoder(this); + return encoder; + } + /**returns accumulated string body content if in stringwriter mode or possibly bytearray*/ + public Object getContent(){ + if(char_response instanceof StringWriter){ + return ((StringWriter)char_response).toString(); + }else if( byte_response instanceof ByteArrayOutputStream){ + return ((ByteArrayOutputStream)byte_response).toByteArray(); + }else return null; + } + /** similar to get content only sends own content to external encoder. + * @throws IOException + **/ + public void exportContent(ResponseEncoder ext) throws IOException { + if(char_response instanceof StringWriter){ + ext.writeString(((StringWriter)char_response).toString()); + }else if( byte_response instanceof ByteArrayOutputStream){ + byte[] buf=((ByteArrayOutputStream)byte_response).toByteArray(); + ext.writeBytes(buf,0,buf.length); + } + } + + public void setContentType(String ctype) { + content_type=ctype; + if(http_response!=null) http_response.setContentType(ctype); + } + public String getContentType(){ + return content_type; + } + public void setStatus(int status) { + this.status=status; + if(http_response!=null) http_response.setStatus(status); + } + public Integer getStatus(){ + return status; + } + public String getHeader(String key){ + for(HTTP.Header hdr:headers){ + if(key.equalsIgnoreCase(key)) return hdr.value; + } + if(http_response!=null){ + return http_response.getHeader(key); + }else{ + return null; + } + } + public Response setHeader(String key,String val){ + HTTP.Header sel=null; + for(HTTP.Header hdr:headers){ + if(key.equalsIgnoreCase(key)){ + sel=hdr; + break; + } + } + if(sel!=null) sel.value=val; else headers.add(new HTTP.Header(key,val)); + if(http_response!=null) http_response.setHeader(key,val); + return this; + } + public List getHeaders(){ + return headers; + } + public String getCookie(String key){ + for(HTTP.Cookie c:cookies){ + if(key.equalsIgnoreCase(key)) return c.value; + } + return null; + } + public Response setCookie(String key,String val,int maxAge,boolean secure){ + HTTP.Cookie sel=null; + for(HTTP.Cookie hdr:cookies){ + if(key.equalsIgnoreCase(key)){ + sel=hdr; + break; + } + } + if(sel!=null){ + sel.value=val; + sel.maxAge=maxAge; + sel.secure=secure; + } else{ + cookies.add(new HTTP.Cookie(key,val,maxAge,secure)); + } + if(http_response!=null){ + Cookie c=new Cookie(key,val); + c.setMaxAge(maxAge); + c.setSecure(secure); + http_response.addCookie(c); + } + return this; + } + public List getCookies(){ + return cookies; + } +} diff --git a/src/main/java/com/reliancy/jabba/ResponseEncoder.java b/src/main/java/com/reliancy/jabba/ResponseEncoder.java new file mode 100644 index 0000000..17b9b7f --- /dev/null +++ b/src/main/java/com/reliancy/jabba/ResponseEncoder.java @@ -0,0 +1,128 @@ +package com.reliancy.jabba; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.Iterator; +import java.util.Locale; + +/** + * This class will replace the Java writer. + * It will have chainable calls. It will inherit lower level calls + * and then extend with higher level. For example write, writeln but then writeJson etc. + */ +public class ResponseEncoder { + protected final Response response; + protected final Locale locale; + protected Writer writer; + protected OutputStream out; + + public ResponseEncoder(Response r){ + response=r; + locale=Locale.getDefault(); + } + public ResponseEncoder(Response r,Locale loc){ + response=r; + locale=loc; + } + protected OutputStream getOutputStream() throws IOException{ + if(out!=null) return out; + if(response.getStatus()==null) response.setStatus(Response.HTTP_OK); + if(response.getContentType()==null) response.setContentType("application/octet-stream"); + if(response.http_response!=null){ + out=response.http_response.getOutputStream(); + }else if(response.byte_response!=null){ + out=response.byte_response; + }else{ + out=new ByteArrayOutputStream(); + } + writer=new OutputStreamWriter(out,StandardCharsets.UTF_8); + return out; + } + protected Writer getWriter() throws IOException{ + if(writer!=null) return writer; + if(response.getStatus()==null) response.setStatus(Response.HTTP_OK); + if(response.getContentType()==null) response.setContentType("text/plain;charset=utf-8"); + if(response.http_response!=null){ + writer=response.http_response.getWriter(); + }else if(response.char_response!=null){ + writer=response.char_response; + }else if(response.byte_response!=null){ + out=response.byte_response; + writer=new OutputStreamWriter(out,StandardCharsets.UTF_8); + }else{ + writer=new StringWriter(); + } + return writer; + } + public ResponseEncoder writeBytes(byte[] buf,int offset,int len) throws IOException{ + getOutputStream().write(buf,offset, len); + return this; + } + public ResponseEncoder writeString(String str) throws IOException{ + getWriter().append(str); + return this; + } + public ResponseEncoder writeStream(InputStream is) throws IOException{ + byte[] buf=new byte[2*4096]; + int bytesRead=-1; + while((bytesRead=is.read(buf))!=-1){ + writeBytes(buf,0,bytesRead); + } + return this; + } + public ResponseEncoder writeln(String msg,Object ... args) throws IOException{ + if(args.length==0){ + getWriter().append(msg).append("\n"); + }else{ + String str=MessageFormat.format(msg,args); + getWriter().append(str).append("\n"); + } + return this; + } + public ResponseEncoder writeIterator(Iterator it) throws IOException{ + Writer wr=getWriter(); + while(it.hasNext()) wr.append(it.next()); + return this; + } + public ResponseEncoder writeReader(Reader rd) throws IOException{ + char[] buffer = new char[2*4096]; + int n = 0; + Writer wr=this.getWriter(); + while (-1 != (n = rd.read(buffer))) { + wr.write(buffer, 0, n); + } + return this; + } + public ResponseEncoder writeObject(Object ret) throws IOException{ + if(ret==null) return this; + Writer wr=getWriter(); + if(ret instanceof Iterator){ + Iterator it=(Iterator)ret; + while(it.hasNext()){ + Object obj=it.next(); + writeObject(obj); + } + }else if(ret instanceof Collection){ + Collection cret=(Collection) ret; + for(Object o:cret) writeObject(o); + }else if(ret instanceof Reader){ + writeReader((Reader)ret); + }else if(ret instanceof byte[]){ + byte[] bret=(byte[])ret; + writeBytes(bret,0,bret.length); + }else{ + wr.append(ret.toString()); + } + //wr.append("\n"); + return this; + } +} diff --git a/src/main/java/com/reliancy/jabba/Route.java b/src/main/java/com/reliancy/jabba/Route.java new file mode 100644 index 0000000..f2ebc6a --- /dev/null +++ b/src/main/java/com/reliancy/jabba/Route.java @@ -0,0 +1,11 @@ +package com.reliancy.jabba; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Route { + String path() default "{method}"; + String verb() default "GET|POST|DELETE"; + String return_mime() default ""; +} diff --git a/src/main/java/com/reliancy/jabba/Router.java b/src/main/java/com/reliancy/jabba/Router.java new file mode 100644 index 0000000..a0d7f4a --- /dev/null +++ b/src/main/java/com/reliancy/jabba/Router.java @@ -0,0 +1,231 @@ +package com.reliancy.jabba; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +import com.reliancy.jabbasec.SecurityPolicy; +import com.reliancy.util.Template; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Router is entry point and servlet implementation that dispatches messages to our endpoints. + * It will launch an embedded jetty server. + * It will provide facilities to register endpoints. + */ +public class Router extends AbstractHandler{ + protected Connector[] connectors; + protected Server jetty; + protected Processor first=null; + protected Processor last=null; + protected RouterEndPoint main=null; + protected transient Config config=null; + protected Logger logger=LoggerFactory.getLogger(Router.class); + + public Router() { + jetty = new Server(); + jetty.setHandler(this); + } + public Config getConfig(){ + return config; + } + public void addProcessor(Processor m){ + if(first==null){ + last=first=m; + }else{ + last.next=m; + } + while(last.next!=null) last=last.next; + } + public void removeProcessor(Processor m){ + if(first==m){ + if(first==last) last=null; + first=first.next; + while(last!=null && last.next!=null) last=last.next; + }else{ + for(Processor prev=first;prev!=null;prev=prev.next){ + if(prev.next==m){ + if(last==m) last=prev; + prev.next=m.next; + break; + } + } + } + m.next=null; + } + public Processor getProcessor(String id){ + for(Processor c=first;c!=null;c=c.next){ + if(c.getId().equalsIgnoreCase(id)) return c; + } + return null; + } + + public RouterEndPoint getMain() { + return main; + } + public void setMain(RouterEndPoint resolver) { + this.main = resolver; + } + public RouterEndPoint importEndPoints(Object target){ + RouterEndPoint ret=new RouterEndPoint(); + LinkedList routes=new LinkedList<>(); + Class type=target.getClass(); + while (type != null) { + for(Method m : type.getDeclaredMethods()){ + //System.out.println("Method:"+m.toString()); + if(m.getAnnotation(Route.class)!=null){ + routes.add(0,m); + } + } + type = type.getSuperclass(); + } + for(Method m:routes){ + //System.out.println("M:"+m); + Route r=m.getAnnotation(Route.class); + MethodEndPoint mm=new MethodEndPoint(target,m,r); + ret.addRoute(r.verb(),mm.getPath(),mm); + } + return ret; + } + + public void handle(String target, + Request baseRequest, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + baseRequest.setHandled(true); + com.reliancy.jabba.Request req=new com.reliancy.jabba.Request(request); + Response resp=new Response(response); + CallSession ss=CallSession.getInstance(); + try{ + ss.begin(null, req, resp); + if(first!=null) first.process(req, resp); + if(main!=null) main.process(req,resp); + }finally{ + ss.end(); + } + } + + public Connector[] getConnectors(){ + if(connectors!=null) return connectors; + ServerConnector connector = new ServerConnector(jetty); + connector.setReuseAddress(false); + connector.setPort(8090); + connectors=new Connector[] {connector}; + return connectors; + } + public void begin(Config conf) throws Exception{ + if(config!=null) throw new RuntimeException("Router running already"); + config=conf; + for(Processor p=first;p!=null;p=p.getNext()){ + p.begin(config); + } + if(main!=null) main.begin(config); + jetty.setConnectors(getConnectors()); + try{ + jetty.start(); + }catch(Exception ex){ + if(ex.getCause() instanceof java.net.BindException){ + logger.error("Bind issue",ex); + Thread.sleep(3000); + } + } + } + public void end() throws Exception{ + if(main!=null) main.end(); + for(Processor p=first;p!=null;p=p.getNext()){ + p.end(); + } + config=null; + logger.info("stopiing jetty"); + Connector[] connectors=jetty.getConnectors(); + System.out.println(connectors); + if(connectors!=null) for(Connector c:connectors){ + ServerConnector cc=(ServerConnector) c; + //System.out.println("stopping connecor:"+cc); + try{ + cc.stop(); + cc.getConnectedEndPoints().forEach((endpoint)-> { + //System.out.println("closing endpoint:"+endpoint); + endpoint.close(); + }); + }finally{ + cc.close(); + //System.out.println("closing connecor:"+cc.getState()); + } + } + //System.out.println("signaling..."); + jetty.stop(); + //System.out.println("cleanup..."); + System.gc(); + //System.out.println("return..."); + } + public void run(Config conf) throws Exception { + try{ + begin(conf); + //System.out.println("Entering server loop..."); + jetty.join(); + }finally{ + //System.out.println("Exiting server loop..."); + end(); + //System.out.println("Exiting server loop...done"); + } + } + public static void main( String[] args ) throws Exception + { + //System.out.println("Hello World!"); + Router app=new Router(); + app.addProcessor(new AppSessionFilter()); + app.addProcessor(new SecurityPolicy()); + app.setMain(app.importEndPoints(app)); + FileServer fs=new FileServer("/static","./var"); + fs.exportRoutes(app.getMain()); + app.run(null); + //System.out.println("Goodbye World!"); + } + + @Route() + public String hello(){ + Map context = new HashMap<>(); + context.put("name", "Jared"); + String ret=""; + try { + Template.search_path("./var",SecurityPolicy.class); + Template t=Template.find("resources/login.j2"); + System.out.println("Template:"+t); + ret = t.render(context).toString(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return ret; + //#return "Hello World"; + } + @Route( + path="/helloPlain" + ) + public void hello2(com.reliancy.jabba.Request req,Response resp) throws IOException{ + resp.getEncoder().writeln("Hi There"); + } + @Route( + path="/hello3/{idd:int}" + ) + public String hello3(int id){ + return "Hello3:"+id; + } +} diff --git a/src/main/java/com/reliancy/jabba/RouterEndPoint.java b/src/main/java/com/reliancy/jabba/RouterEndPoint.java new file mode 100644 index 0000000..c582623 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/RouterEndPoint.java @@ -0,0 +1,135 @@ +package com.reliancy.jabba; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.reliancy.util.Handy; + +public class RouterEndPoint extends EndPoint{ + HashMap routes=new HashMap<>(); + HashMap> routeParams=new HashMap<>(); + ArrayList patterns=new ArrayList<>(); // route patterns ordered + int[] indexes; // indexes for each route within regex + Pattern regex; + + public RouterEndPoint() { + super(null); + } + + @Override + public void serve(Request req, Response resp) throws IOException { + //System.out.println(req.http_request); + String verb=req.getVerb(); + String path=req.getPath(); + log().info("serving:{}",path); + Matcher m=match(verb,path); + //Matcher m=rep.match("GET","/helloP"); + if(m!=null){ + //HashMap pms=new HashMap<>(); + String rt=evalMatcher(m,req.getPathParams()); + //System.out.println(rt); + //System.out.println(req.getPathParams()); + EndPoint ep=getRoute(rt); + if(ep!=null){ + ep.process(req, resp); + }else{ + log().error("no endpoint for:{}",rt); + resp.setContentType("text/plain;charset=utf-8"); + resp.setStatus(Response.HTTP_NOT_FOUND); + resp.getEncoder().writeln("no endpoint for :"+rt); + } + }else{ + log().error("could not resolve path:{}",path); + resp.setContentType("text/plain;charset=utf-8"); + resp.setStatus(Response.HTTP_NOT_FOUND); + resp.getEncoder().writeln("could not resolve path:"+path); + } + } + public EndPoint getRoute(String r){ + return routes.get(r); + } + public void addRoute(String verb,String path, EndPoint mm) { + if(verb==null) verb="GET|POST|DELETE"; + String pathPat=path.replaceAll("\\{(.+)\\}","(.+)"); + String routePat=Handy.wrap(verb,"(",")")+" "+pathPat; + if(!routePat.endsWith("/") && !routePat.endsWith("$")) routePat+="$"; + routes.put(routePat,mm); + //System.out.println("Adding route:"+routePat); + ArrayList params=new ArrayList(); + Pattern p=Pattern.compile("\\{(.+)\\}"); + Matcher m=p.matcher(path); + while(m.find()){ + String g=m.group(); + params.add(Handy.unwrap(g,"{","}")); + } + if(params.isEmpty()==false) routeParams.put(routePat,params); + } + + public void compile() { + patterns.clear(); + for(String r:routes.keySet()){ + patterns.add(r); + } + // sort with longest first + Collections.sort(patterns,Comparator.comparing((str)->{return -str.length();})); + String fullPat = "("+String.join(")|(",patterns)+")"; + regex=Pattern.compile(fullPat); + // also recompute indexes + indexes=new int[patterns.size()]; + int index=1; + for (int i = 0; i < indexes.length; i++) { + indexes[i]=index; + String p=patterns.get(i); + index+=2; // this includes the verb group + if(routeParams.containsKey(p)){ // this includes any param groups + index+=routeParams.get(p).size(); + } + } + //Arrays.stream(indexes).forEach(e->System.out.println(e+" ")); + } + public Matcher match(String verb,String path){ + if(regex==null) compile(); + String input=verb+" "+path; + Matcher m=regex.matcher(input); + if(!m.find()) return null; + return m; + } + /** + * Find the route and return also url params. + * url params are saved in two ways by name and by pos. + * @param m + * @param routeParams + * @return + */ + public String evalMatcher(Matcher m,Map p){ + String gstr=null; + int gindex=1; + while(gindex pms=routeParams.get(ret); + for(int i=0;i getOwnedSecurables(); + public List getDirectPermits(); + public SecurityPermit getPermit(Securable sec); + public SecurityPolicy getPolicy(); +} diff --git a/src/main/java/com/reliancy/jabbasec/SecurityActor.java b/src/main/java/com/reliancy/jabbasec/SecurityActor.java new file mode 100644 index 0000000..43f3fbd --- /dev/null +++ b/src/main/java/com/reliancy/jabbasec/SecurityActor.java @@ -0,0 +1,9 @@ +package com.reliancy.jabbasec; + +/** + * Interface that is implemented by any User or Principal entity. + * Often an AppSession will be determined in many ways by the user. + */ +public interface SecurityActor extends Securable{ + +} diff --git a/src/main/java/com/reliancy/jabbasec/SecurityPermit.java b/src/main/java/com/reliancy/jabbasec/SecurityPermit.java new file mode 100644 index 0000000..0cd3210 --- /dev/null +++ b/src/main/java/com/reliancy/jabbasec/SecurityPermit.java @@ -0,0 +1,16 @@ +package com.reliancy.jabbasec; +/** + * An object describing what rights an actor has on a securable. + * This object can be one individual rule or an effective merge of multiple rights. + * In any case constructing this class will not be allowed except by security policy. + * We should start implementing from security policy and maybe we do not even need this class. + */ +public interface SecurityPermit { + public SecurityActor getActor(); + public Securable getSubject(); + public boolean canRead(); + public boolean canWrite(); + public boolean canDelete(); + public boolean canCreate(); + public boolean canSecure(); +} diff --git a/src/main/java/com/reliancy/jabbasec/SecurityPolicy.java b/src/main/java/com/reliancy/jabbasec/SecurityPolicy.java new file mode 100644 index 0000000..d3ef5ae --- /dev/null +++ b/src/main/java/com/reliancy/jabbasec/SecurityPolicy.java @@ -0,0 +1,125 @@ +package com.reliancy.jabbasec; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; + +import com.reliancy.jabba.AppSession; +import com.reliancy.jabba.CallSession; +import com.reliancy.jabba.Processor; +import com.reliancy.jabba.Request; +import com.reliancy.jabba.Response; +import com.reliancy.util.Handy; + +/** + * SecurityPolicy is a filter/processor that implements various auth protocols but also sources users. + * The policy will produce new users once they are authenticated. + * SecurityPolicy will authenticate SecurityActors and possibly authorize them via a permission mechanism + * to access. + * SecurityProtocol is one authenticatio method. We will not adjust response only try to recover user. + * Initialization of auth will occur outside: + * - for gui after login we will set an auth cookie to remember user and password + * - for APIs if user is required will issue error 401 with WWW-Authenticate header + */ +public class SecurityPolicy extends Processor{ + public static final String KEY_NAME="jbauth"; + protected String secret="sdfklgj 7150 9178-54=09"; + protected ArrayList protocols; + protected SecurityActor admin; + protected SecurityActor guest; + + public SecurityPolicy() { + super(SecurityPolicy.class.getSimpleName().toLowerCase()); + protocols=new ArrayList<>(); + protocols.add(new SecurityProtocol.Digest()); + protocols.add(new SecurityProtocol.Basic()); + } + @Override + public void before(Request request, Response response) throws IOException { + // we will recover a user here + CallSession css=CallSession.getInstance(); + AppSession ass=(AppSession) css.getAppSession(); + if(ass==null || ass.getUser()!=null){ + return; // we got a user all good + } + try{ + SecurityActor user=authenticate(request); + if(user!=null) ass.setUser(user); + }catch(NotAuthentic ex){ + // we could not establish user + response.setStatus(Response.HTTP_FORBIDDEN); + response.getEncoder().writeObject(ex); + } + } + @Override + public void after(Request request, Response response) throws IOException { + } + @Override + public void serve(Request request, Response response) throws IOException { + // nothing to do here + } + protected String getSecret(){ + return secret; + } + /** authenticates or establishes user based on request and updates response. + * this method might redirect to a login view. once it is done + * @param req + * @param res + * @return user we could establish + * @throws NotPermitted + * @throws IOException + */ + public SecurityActor authenticate(Request req) throws IOException, NotAuthentic{ + // must recover user from cookies or by redirecting to login + log().info("User is missing."); + String auth=req.getCookie(KEY_NAME,null); + if(auth!=null){ + // we have an auth cookie - encoded user login + Map kv=Handy.decrypt(getSecret(),auth); + String username=(kv.get("n")); + String password=(kv.get("p")); + String address=(kv.get("a")); + if(address!=null && !address.equals(req.getRemoteAddress())){ + return null; // invalid auth cookie + } + try { + SecurityActor user = loadActor(admin,username,password); + if(user!=null) return user; + else throw new NotAuthentic("invalid credentials"); + } catch (NotPermitted e) { + throw new NotAuthentic("not permitted to authenticate",e); + } + } + // try authorization header as fallback - can't clear it always + auth=req.getHeader("Authorization"); + if(auth!=null){ + String[] kv=auth.split(" ",2); + String proto_name=kv[0]; + String proto_args=kv.length>1?kv[1]:""; + for(SecurityProtocol sproto:protocols){ + if(proto_name.equalsIgnoreCase(sproto.getName())){ + return sproto.authenticate(this, req,proto_args); + } + } + throw new NotAuthentic("auth method not supported:"+proto_name); + } + return null; + } + /** will establish what if any rights an actor has on a securable. */ + public SecurityPermit authorize(SecurityActor actor, Securable subject){ + return actor.getPermit(subject); + } + /** loads a securable by id given actor permits. */ + public Securable loadSecurable(SecurityActor actor, Integer id) throws IOException, NotPermitted{ + return null; + } + /** loads an actor given name and password. */ + public SecurityActor loadActor(SecurityActor actor, String name, String pwd) throws IOException, NotPermitted{ + return null; + } + /** will save a securable including a user if permitted via actor. */ + public void saveSecurable(SecurityActor actor, Securable sec) throws IOException{ + + } + +} diff --git a/src/main/java/com/reliancy/jabbasec/SecurityProtocol.java b/src/main/java/com/reliancy/jabbasec/SecurityProtocol.java new file mode 100644 index 0000000..1b57be7 --- /dev/null +++ b/src/main/java/com/reliancy/jabbasec/SecurityProtocol.java @@ -0,0 +1,42 @@ +package com.reliancy.jabbasec; + +import java.io.IOException; + +import com.reliancy.jabba.Request; +import com.reliancy.util.Handy; + +/** + * A SecurityProtocol will be processing HTTP to establish SecurityActor or user. + */ +public abstract class SecurityProtocol { + protected final String name; + public SecurityProtocol(String n){ + name=n; + } + public String getName(){ + return name; + } + public abstract SecurityActor authenticate(SecurityPolicy policy,Request req,String tok) throws NotPermitted, IOException ; + public static class Basic extends SecurityProtocol{ + public Basic(){ + super("Basic"); + } + @Override + public SecurityActor authenticate(SecurityPolicy policy,Request req,String tok) throws NotPermitted, IOException { + tok=String.valueOf(Handy.decodeBase64(tok)) ; + String[] up=tok.split(":",2); + String userid=up[0]; + String pwd=up.length>1?up[1]:""; + return policy.loadActor(policy.admin,userid,pwd); + } + } + public static class Digest extends SecurityProtocol{ + public Digest(){ + super("Digest"); + } + @Override + public SecurityActor authenticate(SecurityPolicy policy,Request req,String tok) { + return null; + } + } +} diff --git a/src/main/java/com/reliancy/jabbasec/resources/login.j2 b/src/main/java/com/reliancy/jabbasec/resources/login.j2 new file mode 100644 index 0000000..c87a8b7 --- /dev/null +++ b/src/main/java/com/reliancy/jabbasec/resources/login.j2 @@ -0,0 +1,4 @@ +{% extends "base.j2" %} +{% block content %} +
Hello from code: {{name}}!
+{% endblock %} diff --git a/src/main/java/com/reliancy/rec/DecoderSink.java b/src/main/java/com/reliancy/rec/DecoderSink.java new file mode 100644 index 0000000..136356a --- /dev/null +++ b/src/main/java/com/reliancy/rec/DecoderSink.java @@ -0,0 +1,14 @@ +package com.reliancy.rec; + +/** Similar to a SAX interface used by parsers for XML and JSON to assemble DOM structures. + * Simply gets notified of events during parsing. + * @author amer + */ +public interface DecoderSink { + void beginDocument(Rec init); + Rec endDocument(); + void beginElement(String name); + void endElement(String name); + void setKey(String name); + void setValue(CharSequence seq); +} diff --git a/src/main/java/com/reliancy/rec/Hdr.java b/src/main/java/com/reliancy/rec/Hdr.java new file mode 100644 index 0000000..603740b --- /dev/null +++ b/src/main/java/com/reliancy/rec/Hdr.java @@ -0,0 +1,126 @@ +package com.reliancy.rec; + +import java.util.ArrayList; +import java.util.ListIterator; + +/** Base class of meta objects. + * We use it to describe certain meta information. We derive from it Slot. + * We define keys list of slots on the header level to describe slots. + */ +public class Hdr { + public static final int FLAG_ARRAY =0x0001; + public static final int FLAG_CHANGED =0x0002; + public static final int FLAG_HIDDEN =0x0004; + public static final int FLAG_LOCKED =0x0008; + int flags; + String name; + String label; + Class type; + final ArrayList keys; + + public Hdr(String name) { + this.name=name; + keys=new ArrayList<>(); + } + public Hdr(String name,Class type) { + this.name=name; + this.type=type; + keys=new ArrayList<>(); + } + @Override + public String toString(){ + StringBuilder ret=new StringBuilder(); + ret.append("{").append("flags:").append(flags).append(",name:").append(name); + ret.append(",dim:").append(keys.size()).append("}"); + return ret.toString(); + } + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getLabel() { + return label!=null?label:name; + } + public void setLabel(String name) { + this.label = name; + } + public Class getType() { + return type; + } + public void setType(Class type) { + this.type = type; + } + public Hdr raiseFlags(int f){ + flags|=f; + return this; + } + public Hdr clearFlags(int f){ + flags&=~f; + return this; + } + public boolean checkFlags(int f){ + return (flags & f)!=0; + } + public T castAs(Class clazz){ + return clazz.cast(this); + } + public int findSlot(String name){ + return findSlot(name,0); + } + public int findSlot(String name,int ofs){ + ListIterator it=keys.listIterator(ofs); + while(it.hasNext()){ + int index=it.nextIndex(); + Slot e=it.next(); + if(e.getName().equalsIgnoreCase(name)) return index; + } + return -1; + } + public int findSlot(Slot s,int ofs){ + ListIterator it=keys.listIterator(ofs); + while(it.hasNext()){ + int index=it.nextIndex(); + Slot e=it.next(); + if(e==s) return index; + } + return -1; + } + /** + * this version will get or create a slot by given name. + * @param name + * @return + */ + public Slot getSlot(String name){ + int index=findSlot(name); + if(index<0){ + return new Slot(name); + }else{ + return getSlot(index); + } + } + public Slot getSlot(int pos){ + return keys.get(pos); + } + public Hdr removeSlot(int pos){ + keys.remove(pos); + return this; + } + public Hdr addSlot(Slot s){ + keys.add(s); + return this; + } + public Hdr setSlot(int index,Slot s){ + keys.set(index,s); + return this; + } + public Slot[] slots(Slot... slots){ + if(slots!=null && slots.length>0){ + keys.clear(); + for(int i=0;i stack=new LinkedList(); + /** will not add white space only nodes. */ + boolean whitespaceIgnored=true; + boolean entitycharsIgnored=false; + + public boolean isWhitespaceIgnored() { + return whitespaceIgnored; + } + + public void setWhitespaceIgnored(boolean whitespaceIgnored) { + this.whitespaceIgnored = whitespaceIgnored; + } + + public boolean isEntitycharsIgnored() { + return entitycharsIgnored; + } + + public void setEntitycharsIgnored(boolean entitycharsIgnored) { + this.entitycharsIgnored = entitycharsIgnored; + } + + public Rec getRoot() { + return stack.getLast(); + } + public Rec getSubject(){ + if(stack.isEmpty()) return null; + return stack.getFirst(); + } + public void pushSubject(Rec n){ + stack.push(n); + } + public Rec popSubject(){ + Rec child=stack.pop(); + Rec parent=getSubject(); + if(parent==null) return child; + if(parent.isArray()){ + parent.add(child); + }else{ + String key=(String) parent.get(KEY,null); + Slot keyslot=parent.getSlot(key); + parent.remove(KEY).set(keyslot,child); + // if array and has key it should bomb + //parent.setArray(false); + } + return child; + } + + public void beginDocument() { + beginDocument(null); + } + @Override + public void beginDocument(Rec init) { + sets=inBody; + out.setLength(0); + lastToken=null; + stack.clear(); + Rec arr=new Obj(true); + stack.push(arr); + //System.out.println("BeginDoc"); + } + + @Override + public Rec endDocument() { + // need to set the actual parent + while(stack.getFirst()!=stack.getLast()){ + popSubject(); + } + // now adjust the root if it is array with only one child - one we added in start document as first element + Rec root=getSubject(); + if(root.isArray() && root.count()==1 && root.get(0) instanceof Rec){ + // ok we collapse our array from above - since we only have one object + Object bb=root.get(0); + Rec b=(Rec)bb ; + popSubject(); + pushSubject(b); + } + //System.out.println("EndDoc"); + return getRoot(); + } + + @Override + public void beginElement(String name) { + Rec element=new Obj("array".equals(name)); + //element.setAttr(0); + pushSubject(element); + //System.out.println("BeginElement:"+name); + } + + @Override + public void endElement(String name) { + // check if the correct end element is sent + Rec sub=this.getSubject(); + if(!sub.isArray()) sub.remove(KEY); + // finally pop the root + popSubject(); + //System.out.println("EndElement:"+name); + } + + @Override + public void setKey(String name) { + Rec sub=this.getSubject(); + String key=(String) sub.get(KEY,null); + if(key!=null){ + // something is wrong - our tokizer might have ignored escape char or input has forgotten a delimiter + // we try to split name because it would contain key and value merged + int split=0; + if(name.startsWith("\"")) split=name.indexOf('\"', 1); + if(name.startsWith("'")) split=name.indexOf('\'', 1); + String val=name.substring(0,split+1); + setValue(val); + name=name.substring(split+1); + } + int start=0;int stop=name.length(); + while(start= 0) { + val = Double.parseDouble(sVal); + } else { + val = Integer.parseInt(sVal); + } + }else if(this.isEntitycharsIgnored()==false && seq!=null && seq.length()>0){ + // maybe it is a string after all + val=unescape(seq); + } + }else if(this.isEntitycharsIgnored()==false && seq!=null && seq.length()>0){ + // we had quotes so lets decode escaed chars + val=unescape(seq); + } + return val; + } + + public static CharSequence unescape(CharSequence str) { + StringBuilder buf = null; + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch == '\\' && i < (str.length() - 1)) { + i = i + 1; + char ch2 = str.charAt(i); + switch (ch2) { + case '"': + if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):""); + buf.append("\""); + break; + case '\\': + if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):""); + buf.append("\\"); + break; + case '/': + if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):""); + buf.append("/"); + break; + case 'b': + if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):""); + buf.append("\b"); + break; + case 'f': + if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):""); + buf.append("\f"); + break; + case 'n': + if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):""); + buf.append("\n"); + break; + case 'r': + if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):""); + buf.append("\r"); + break; + case 't': + if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):""); + buf.append("\t"); + break; + default: + if(buf!=null) buf.append(ch); + } + } else { + if(buf!=null) buf.append(ch); + } + } + return buf!=null?buf.toString():str; + } + +} diff --git a/src/main/java/com/reliancy/rec/JSONEncoder.java b/src/main/java/com/reliancy/rec/JSONEncoder.java new file mode 100644 index 0000000..d42cec5 --- /dev/null +++ b/src/main/java/com/reliancy/rec/JSONEncoder.java @@ -0,0 +1,287 @@ +package com.reliancy.rec; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class JSONEncoder{ + public JSONEncoder(){ + } + /** + * We encode into an appendable various primitives and Rec. + * If appendable null then we just compute expected size. + * keys are not escaped they better not contain any special chars. + * values are quoted and escaped unless we detect a string that looks like a json object those are passed thru. + * in the past we tried to deduce if quoting was needed, but this is not the place to do so because we do not know how many times + * value was escaped so the only thing we can assume is that it needs to be escaped. So feeding a value that is quoted and + * escaped will return back on parse the same and will need to dequoted and descaped once more but that shoudl work fine with + * whoever quoted it in the upstream in the first place. + * @param val property value + * @param o encoding output + * @return length in characters of encoded result + * @throws IOException + */ + public static int encode(Object val,Appendable o) throws IOException { + int len = 0; + /* + // first key + if (key != null) { + if (o != null) { + o.append('"').append(key).append("\":"); + } + len += 3 + key.length(); + } + */ + // now value + if (val instanceof Object[]) { + Object[] valval = (Object[]) val; + if (o != null) { + o.append('['); + } + int index = 0; + for (Object obj : valval) { + if (index++ > 0) { + len += 1; + if (o != null) { + o.append(","); + } + } + len += encode(obj, o); + } + if (o != null) { + o.append(']'); + } + len += 2; + } else if (val instanceof List) { + List valval = (List) val; + if (o != null) { + o.append('['); + } + int index = 0; + for (Object obj : valval) { + if (index++ > 0) { + len += 1; + if (o != null) { + o.append(","); + } + } + len += encode(obj, o); + } + if (o != null) { + o.append(']'); + } + len += 2; + } else if (val instanceof Map) { + len+=encodeMap((Map)val,o); + } else if (val instanceof Rec) { + len += encodeRec((Rec) val, o); + } else if (val instanceof Number || val instanceof Boolean) { + String str = val.toString(); + if (o != null) { + o.append(str); + } + len += str.length(); + }else if(val instanceof int[]){ + int[] valval = (int[]) val; + if (o != null) { + o.append('['); + } + int index = 0; + for (int obj : valval) { + if (index++ > 0) { + len += 1; + if (o != null) { + o.append(","); + } + } + if(o!=null) o.append(String.valueOf(obj)); + len += 1; + } + if (o != null) { + o.append(']'); + } + len += 2; + }else if(val instanceof float[]){ + float[] valval = (float[]) val; + if (o != null) { + o.append('['); + } + int index = 0; + for (float obj : valval) { + if (index++ > 0) { + len += 1; + if (o != null) { + o.append(","); + } + } + if(o!=null) o.append(String.valueOf(obj)); + len += 1; + } + if (o != null) { + o.append(']'); + } + len += 2; + }else if (val instanceof Object) { + String str = val.toString(); + boolean jsontxt = false; + jsontxt |= str.length() > 0 && str.startsWith("{") && str.endsWith("}"); + jsontxt |= str.length() > 0 && str.startsWith("[") && str.endsWith("]"); + //boolean quoted=str.length() > 1 && str.startsWith("\"") && str.endsWith("\""); + // embedded json is not quoted and not escaped + // all other text is quoted otherwise we will prevent quoted quotes (those would be swallowed) + // we will not try to be smart if someone added an item that is quoted already it will be escaped and queotes retained + // we must be consistent so that repeated parse and encode works and not too smart here + // we need to put quotes around unless + if (!jsontxt) { + str = escape(str); + if (o != null) { + o.append('"'); + } + len += 1; + } + if (o != null) { + o.append(str); + } + len += str.length(); + if (!jsontxt) { + if (o != null) { + o.append('"'); + } + len += 1; + } + } else if (val == null) { + String str = "null"; + if (o != null) { + o.append(str); + } + len += str.length(); + } + return len; + } + public static int encodeMap(Map valval,Appendable o) throws IOException{ + int len=0; + if (o != null) { + o.append('{'); + } + int index = 0; + for (Object obj : valval.keySet()) { + if (index++ > 0) { + len += 1; + if (o != null) { + o.append(","); + } + } + String key=obj.toString(); + if (o != null) { + o.append('"').append(key).append("\":"); + } + len += 3 + key.length(); + len += encode(valval.get(obj), o); + } + if (o != null) { + o.append('}'); + } + len += 2; + return len; + } + public static int encodeRec(Rec val,Appendable o) throws IOException{ + int len=0; + if (o != null) { + o.append(val.isArray()?"[":"{"); + } + for (int i=0;i 0) { + len += 1; + if (o != null) { + o.append(","); + } + } + if(k!=null){ + String key=k.getName(); + if (o != null) { + o.append('"').append(key).append("\":"); + } + len += 3 + key.length(); + } + len += encode(v, o); + } + if (o != null) { + o.append(val.isArray()?"]":"}"); + } + len += 2; + return len; + } + /** + * @param str + * @return true if the string includes any of the special chars. + */ + public static boolean needsEscaping(String str) { + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + switch (ch) { + case '"': + case '\\': + case '/': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + return true; + } + } + return false; + } + + /** + * this helper method handle quotes and control chars. + * @param str input string + * @return output after encoding special chars + */ + public static String escape(String str) { + StringBuilder buf = null; + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + switch (ch) { + case '"': + if(buf==null) buf=new StringBuilder(str.substring(0,i)); + buf.append("\\\""); + break; + case '\\': + if(buf==null) buf=new StringBuilder(str.substring(0,i)); + buf.append("\\\\"); + break; + case '/': + if(buf==null) buf=new StringBuilder(str.substring(0,i)); + buf.append("\\/"); + break; + case '\b': + if(buf==null) buf=new StringBuilder(str.substring(0,i)); + buf.append("\\b"); + break; + case '\f': + if(buf==null) buf=new StringBuilder(str.substring(0,i)); + buf.append("\\f"); + break; + case '\n': + if(buf==null) buf=new StringBuilder(str.substring(0,i)); + buf.append("\\n"); + break; + case '\r': + if(buf==null) buf=new StringBuilder(str.substring(0,i)); + buf.append("\\r"); + break; + case '\t': + if(buf==null) buf=new StringBuilder(str.substring(0,i)); + buf.append("\\t"); + break; + default: + if(buf!=null) buf.append(ch); + } + } + return buf!=null?buf.toString():str; + } + +} diff --git a/src/main/java/com/reliancy/rec/Obj.java b/src/main/java/com/reliancy/rec/Obj.java new file mode 100644 index 0000000..e345ade --- /dev/null +++ b/src/main/java/com/reliancy/rec/Obj.java @@ -0,0 +1,153 @@ +package com.reliancy.rec; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation of a Rec. + * We separate keys and values because Obj could just be an array. + * If object is declated an array keys are nonexistant and rec related methods will return null or crash. + * Our setters return this object to main the calls chainable. + * Also positional calls accept negative values which reference from end backward. + */ +public class Obj implements Rec{ + final List values; + final Hdr meta; + + public Obj() { + values=new ArrayList<>(); + meta=new Slot(null); + } + public Obj(boolean is_array) { + values=new ArrayList<>(); + meta=new Slot(null); + if(is_array) meta.raiseFlags(Hdr.FLAG_ARRAY); + } + public Obj(List k,List v) { + values=v; + meta=new Slot(null); + meta.keys.addAll(k); + } + /** + * This ctor is reserved for derivations with fixed slot definitions. + * This constructor will inspect static Slot members and construct keys that way + * if meta named. + * @param def + */ + protected Obj(Hdr def){ + values=new ArrayList<>(); + meta=def; + } + @Override + public String toString(){ + StringBuilder buf=new StringBuilder(); + toString(buf); + return buf.toString(); + } + public int toString(StringBuilder buf){ + boolean is_arr=isArray(); + int length0=buf.length();// length before anything done + //StringBuffer indent=new StringBuffer(); // detect indent + //for(int i=length0;i>0 && Character.isWhitespace(buf.charAt(i));i--){ + // indent.append(buf.codePointAt(i)); + //} + buf.append(is_arr?"[":"{"); + if(is_arr){ + for(int pos=0;pos0) buf.append(","); + Object val=this.get(pos); + if(val instanceof Obj) ((Obj)val).toString(buf); + else if(val!=null) buf.append(val.toString()); + else buf.append("null"); + } + }else{ + for(int pos=0;pos0) buf.append(","); + Slot s=getSlot(pos); + buf.append(s.getName()+":"); + Object val=this.get(pos); + if(val!=null) s.toString(val,buf); else buf.append("null"); + } + } + buf.append(is_arr?"]":"}"); + return buf.length()-length0; + } + @Override + public Hdr meta(){ + return meta; + } + @Override + public boolean isArray(){ + return meta==null || meta.checkFlags(Hdr.FLAG_ARRAY); + } + @Override + public int count() { + return values.size(); + } + + @Override + public Rec set(int pos, Object val) { + if(pos<0) pos=count()+pos; + values.set(pos,val); + return this; + } + + @Override + public Object get(int pos) { + if(pos<0) pos=count()+pos; + return values.get(pos); + } + + @Override + public Rec add(Object val) { + values.add(val); + if(!isArray()) meta.addSlot(new Slot("arg"+count(),Object.class)); + return this; + } + + @Override + public Rec remove(int s) { + values.remove(s); + if(!isArray()) meta.removeSlot(s); + return this; + } + + + @Override + public Rec set(Slot s, Object val) { + if(s==null) throw new IllegalArgumentException("invalid key provided"); + if(isArray()) throw new IllegalStateException("array not mappable with:"+s.getName()); + int index=s.getPosition(); // try slot position + if(index<0) index=meta.findSlot(s.getName());// fall back to search if slot not set + if(index<0){ + values.add(val); + meta.addSlot(s); + }else{ + values.set(index,val); + meta.setSlot(index,s); + } + return this; + } + /** + * Returns value by slot key. + * If the underlying rec is a vec/array this method might work if slot is positioned else it will + * return def value. + */ + @Override + public Object get(Slot s, Object def) { + if(s==null) throw new IllegalArgumentException("invalid key provided"); + //if(keys==null) throw new IllegalStateException("array not mappable with:"+s.getName()); + int index=s.getPosition(); // try slot position + if(index<0 && !isArray()) index=meta.findSlot(s.getName());// fall back to search if slot not set + return index<0?def:values.get(index); + } + + @Override + public Rec remove(Slot s) { + int index=s.getPosition(); // try slot position + if(index<0 && !isArray()) index=meta.findSlot(s.getName());// fall back to search if slot not set + if(index>=0) remove(index); + return this; + } + +} diff --git a/src/main/java/com/reliancy/rec/Rec.java b/src/main/java/com/reliancy/rec/Rec.java new file mode 100644 index 0000000..fd5c6ec --- /dev/null +++ b/src/main/java/com/reliancy/rec/Rec.java @@ -0,0 +1,20 @@ +package com.reliancy.rec; + +/** + * A record representation like in JSON. + * This is either an array or a map of fields. + * Each field definition we call a slot. + */ +public interface Rec extends Vec{ + public Rec set(Slot s,Object val); + public Object get(Slot s,Object def); + public Rec remove(Slot s); + public default Slot getSlot(String name){ + Hdr m=meta(); + return m!=null?m.getSlot(name):null; + } + public default Slot getSlot(int pos){ + Hdr m=meta(); + return m!=null?m.getSlot(pos):null; + } +} diff --git a/src/main/java/com/reliancy/rec/Slot.java b/src/main/java/com/reliancy/rec/Slot.java new file mode 100644 index 0000000..e0d9b30 --- /dev/null +++ b/src/main/java/com/reliancy/rec/Slot.java @@ -0,0 +1,60 @@ +package com.reliancy.rec; + +/** + * Slot is a definition of a value start with the name. + * We use it to define columns/fields of records. + * It is also used as header of actual records. + */ +public class Slot extends Hdr { + + public static interface Initializer{ + Object getInitalValue(Slot s,Rec rec); + } + public static final Initializer DEFAULT_INITIALIZER=new Initializer(){ + public Object getInitalValue(Slot s,Rec rec) {return s.getDefaultValue();} + }; + int position; + Object defaultValue; + Initializer initValue; + + public Slot(String name){ + this(name,Object.class); + } + public Slot(String name,Class type){ + super(name,type); + this.position=-1; + this.initValue=DEFAULT_INITIALIZER; + } + public int getPosition() { + return position; + } + public void setPosition(int position) { + this.position = position; + } + public Object getDefaultValue() { + return defaultValue; + } + public void setDefaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + public Initializer getInitValue() { + return initValue; + } + public void setInitValue(Initializer initValue) { + this.initValue = initValue; + } + public int toString(Object val, StringBuilder buf) { + int length0=buf.length(); + if(val instanceof Obj) ((Obj)val).toString(buf); + else if(val!=null) buf.append(val.toString()); + else buf.append("null"); + return buf.length()-length0; + } + public Object get(Rec r,Object def){ + return r.get(this, def); + } + public Slot set(Rec r,Object val){ + r.set(this, val); + return this; + } +} diff --git a/src/main/java/com/reliancy/rec/TextDecoder.java b/src/main/java/com/reliancy/rec/TextDecoder.java new file mode 100644 index 0000000..e1087c2 --- /dev/null +++ b/src/main/java/com/reliancy/rec/TextDecoder.java @@ -0,0 +1,17 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.reliancy.rec; + +/** An interface used in parser implementation. + * + * @author amer + */ +public interface TextDecoder { + void beginDocument(Rec init); + Rec endDocument(); + public int parse(int offset,CharSequence in); +} diff --git a/src/main/java/com/reliancy/rec/Vec.java b/src/main/java/com/reliancy/rec/Vec.java new file mode 100644 index 0000000..06b2b48 --- /dev/null +++ b/src/main/java/com/reliancy/rec/Vec.java @@ -0,0 +1,18 @@ +package com.reliancy.rec; +/** + * dimensioned container of values. + * Our setters return this object to make the calls chainable. + * Also positional calls accept negative values which reference from end backward. + * + */ +public interface Vec { + public default boolean isArray(){ + return meta().checkFlags(Hdr.FLAG_ARRAY); + } + public Hdr meta(); + public int count(); + public Rec set(int pos,Object val); + public Object get(int pos); + public Rec add(Object val); + public Rec remove(int s); +} diff --git a/src/main/java/com/reliancy/util/CloseableIterator.java b/src/main/java/com/reliancy/util/CloseableIterator.java new file mode 100644 index 0000000..36d7cca --- /dev/null +++ b/src/main/java/com/reliancy/util/CloseableIterator.java @@ -0,0 +1,7 @@ +package com.reliancy.util; + +import java.io.Closeable; +import java.util.Iterator; + +public interface CloseableIterator extends Iterator, Closeable { +} \ No newline at end of file diff --git a/src/main/java/com/reliancy/util/Handy.java b/src/main/java/com/reliancy/util/Handy.java new file mode 100644 index 0000000..81d5ddb --- /dev/null +++ b/src/main/java/com/reliancy/util/Handy.java @@ -0,0 +1,458 @@ +package com.reliancy.util; +/** + * Common utility methods. + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +public final class Handy { + public static String wrap(String verb, String left, String right) { + if(verb==null) verb=""; + if(verb.startsWith(left) && verb.endsWith(right)) return verb; + return left+verb.trim()+right; + } + public static String unwrap(String verb, String left, String right) { + if(verb==null) return verb; + String ret=verb.trim(); + if(verb.startsWith(left) && verb.endsWith(right)) ret=verb.substring(1,verb.length()-1); + return ret; + } + public static T nz(T val, T def){ + return val!=null?val:def; + } + /** + * Will try to convert incoming value to an expected class. + * @param clazz + * @param val + * @return + */ + public static Object normalize(Class clazz, Object val ) { + if(val==null) return null; // we are null + if(clazz.isAssignableFrom(val.getClass())) return clazz; // we are assignable + if(val instanceof String){ + String value=(String) val; + if(value.isBlank() || value.equals("''") || value.equals("\"\"")) return null; + if( Boolean.class==( clazz ) || boolean.class==( clazz ) ) return Boolean.parseBoolean( value ); + if( Byte.class==( clazz ) || byte.class==( clazz ) ) return Byte.parseByte( value ); + if( Short.class==( clazz ) || short.class==( clazz ) ) return Short.parseShort( value ); + if( Integer.class==( clazz ) || int.class==( clazz ) ) return Integer.parseInt( value ); + if( Long.class==( clazz ) || long.class==( clazz )) return Long.parseLong( value ); + if( Float.class==( clazz ) || float.class==( clazz ) ) return Float.parseFloat( value ); + if( Double.class==( clazz ) || double.class==( clazz )) return Double.parseDouble( value ); + } + + return val; + } + /** + * This method is a bit more complex because it locks onto two delimiters one for grouping and other + * for decimal point and chooses those from a list of [space],'`. which are used all over the world in different places. + * Returns true if the string only contains digits and numeric characters. + * This should match 1000000 also 1,000,000 and also 1,000,000.00 but it is still not possible to differentiate between 1,000 =1000 in us + * from 1000,00 whch is used in europe. So it is difficult to normalize the string so it could process any number. + * @param str string to test + * @return trie if string looks numeric or is null/empty + */ + public static final boolean isNumeric(String str){ + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return true; + } + String delims=" ,.'`"; + int delimNumber=0; // 0 we load, 1 we let one more, 2 we exit on second occurance + char delimUsed=0; // char used as delim + boolean delimLast=false; + int digitCount=0; + for (int i = 0; i < strLen; i++) { + char ch=str.charAt(i); + boolean accept=Character.isDigit(ch); + if(accept) digitCount++; + accept=accept || (ch=='-' && i==0); + accept=accept || (ch=='+' && i==0); + if(delims.indexOf(ch)>=0){ + accept=!delimLast; // prevent delims following each other + delimLast=true; + if(delimNumber==0){ + delimNumber=1; + delimUsed=ch; + }else if(delimNumber==1){ + if(delimUsed!=ch) delimNumber=2; + delimUsed=ch; + }else{ + // we have seen two different delim and whatever is coming here is breaking numeric format like second delim second time or some otehr + accept=false; + } + }else{ + delimLast=false; + } + if(!accept) return false; + } + return digitCount>0; + } + /** + * returns true if the string is null, empty or contains only white space. + */ + public static boolean isBlank(CharSequence str) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if ((Character.isWhitespace(str.charAt(i)) == false)) { + return false; + } + } + return true; + } + /** + * Provides a unified notion of what constitutes an empty value. + * For any object if it is null. + * For string also if it is blank + * For arrays and lists and collection and maps also if no entries or keys exist. + * @param value anything + * @return true if any of the above matches + */ + public static boolean isEmpty(Object value){ + if(value==null) return true; + if(value instanceof CharSequence){ + return isBlank((CharSequence)value); + } + Class cls=value.getClass(); + if(cls.isArray()) { + if(value instanceof Object[]){ + Object[] arr=(Object[]) value; + if(arr.length==0) return true; + for(int i=0;i c=(Collection)value; + if(c.isEmpty()) return true; + for(Object o:c){ + if(isEmpty(o)==false) return false; + } + return true; + } + return false; + } + /** Attempts to take a compact string and beautify it. + * Will uppercase the first letter. Will also expand CamelCase. + * Also will replace _ with empty space. + * @param str + * @return nicely formatted string ready for display + */ + public static final String prettyPrint(String str){ + if(str==null) return ""; + boolean fix=false; + char prevCh=0; + if(str.startsWith("org.") || str.startsWith("net.") || str.startsWith("com.") || str.startsWith("java.")){ + str=str.substring(1+str.lastIndexOf('.')); // we strip class name paths + } + for(int i=0;i0?bufs.charAt(bufs.length()-1):currCh; + if(Character.isWhitespace(currCh)){ + if(!Character.isWhitespace(prevCh)){ + bufs.append(' '); + } + continue; // ignore repeated whitespace otherwise emit space + } + if(bufs.length()==0){ + toUC=true; + }else if((!Character.isUpperCase(prevCh) && ("-+/%*".indexOf(prevCh)==-1 || Character.isLetter(prevCh))) && Character.isUpperCase(currCh)){ + // non uc (a not one of operands) behind, uc ahead + bufs.append(" "); + }else if(Character.isLetter(prevCh) && Character.isDigit(currCh)){ + // letter behind, digit ahead + bufs.append(" "); + }else if(Character.isUpperCase(prevCh) && Character.isUpperCase(currCh) && i<(str.length()-1) && Character.isLowerCase(str.charAt(i+1))){ + // behind me uppercase infrom uppercase then lowercase + bufs.append(" "); + } + bufs.append(toUC?Character.toUpperCase(currCh):currCh); + toUC=false; + } + while(bufs.length()>0 && Character.isWhitespace(bufs.charAt(bufs.length()-1))){ + // trims whitespace from end + bufs.setLength(bufs.length()-1); + } + return bufs.toString(); + } + /** Attempts to take a user string and compact it to camel case. + * @param str nicely formatted string + * @return nicely compact string + */ + public static String toCamelCase(String value) { + if(value==null || value.trim().isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + //final char delimChar = ' '; + boolean flip = false; + for (int charInd = 0; charInd < value.length(); charInd++) { + char ch = value.charAt(charInd); + if (Character.isWhitespace(ch)) { + flip = true; + }else if(flip){ + flip = false; + if(ch==Character.toLowerCase(ch)) sb.append("_"); + sb.append(ch); + }else{ + sb.append(ch); + } + } + return sb.toString(); + } + public static byte[] deflate(byte[] content) throws IOException{ + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION,true); + deflater.setInput(content); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(content.length); + deflater.finish(); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); // returns the generated code... index + outputStream.write(buffer, 0, count); + } + outputStream.close(); + byte[] output = outputStream.toByteArray(); + return output; + } + + public static byte[] inflate(byte[] contentBytes) throws IOException, DataFormatException{ + Inflater inflater = new Inflater(true); + inflater.setInput(contentBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(contentBytes.length); + byte[] buffer = new byte[1024]; + while (!inflater.finished()) { + int count = inflater.inflate(buffer); + outputStream.write(buffer, 0, count); + } + outputStream.close(); + byte[] output = outputStream.toByteArray(); + return output; + } + + public static void shuffle(Object[] e){ + Random rn = new Random(); + for(int i=0;i m){ + String ret=null; + Object[] es=m.keySet().toArray(); + shuffle(es); // we shuffle entries to confuse the string a bit + StringBuilder buf=new StringBuilder(); + for(int i=0;i0) buf.append("\n"); + buf.append(e).append(":").append(m.get(e)); + } + ret=encryptString(key,buf.toString()); + return ret; + } + /** + * This method will encrypt a string and return BASE 64 string that is web safe. + * TO make the string web safe we replace + with - and / with _ + * Must revert this change on the reverse. + * @param key + * @param ret + * @return + */ + public static final String encryptString(String key,String ret){ + try{ + byte[] bkey=key.getBytes("UTF-8"); + byte[] bstr=ret.getBytes("UTF-8"); + for(int i=0;i decrypt(String key,String m){ + m=decryptString(key,m); + Map ret=new HashMap<>(); + //System.out.println("Output:"+m); + Tokenizer tokz=new Tokenizer(m); + tokz.setDelimChars("\n"); + tokz.setWhiteChars(null); + for(String t=tokz.nextToken();t!=null;t=tokz.nextToken()){ + if("\n".equals(t)) continue; + String[] kv=t.split(":",2); + ret.put(kv[0],kv.length>1?kv[1]:null); + } + return ret; + } + public static final String decryptString(String key,String m){ + try{ + //m=URLDecoder.decode(m, "UTF-8"); + m=m.replace('-','+'); + m=m.replace('_','/'); + m=m.replace('.','='); + byte[] bkey=key.getBytes("UTF-8"); + byte[] bstr=decodeBase64(m); + for(int i=0;i> 4]); + sb.append(HEX_CHARS[b & 0x0F]); + } + return sb.toString(); + } + /** + * Finds first occurrence of sub inside body with and without case. + * We implement this search via a FSM and ignore the case. + * @param body + * @param sub + * @param offset + * @return offset of first occurance + */ + public static final int indexOf(CharSequence body,CharSequence sub,int offset){ + if(body==null) return -1; + int state=0; + int blen=body.length(); + int slen=sub.length(); + boolean ignorecase=true; + for(int index=offset;index=slen) return index-slen+1; // we found a match + } + return -1; + } + /** + * Will trim the string from left and right and remove any of the symbols. + */ + public static String trim(String trim,String sym) { + if(trim==null || trim.length()==0) return trim; + int start=0; + int end=trim.length(); + while(start all) { + if(all==null) return null; + String[] ret=new String[all.size()]; + all.toArray(ret); + return ret; + } + +} diff --git a/src/main/java/com/reliancy/util/Path.java b/src/main/java/com/reliancy/util/Path.java new file mode 100644 index 0000000..a58dde7 --- /dev/null +++ b/src/main/java/com/reliancy/util/Path.java @@ -0,0 +1,418 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.reliancy.util; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; + + +/** Path to a resource almost identical to a URL. + * It might but should not hold any handles. It holds the address and + * possibly takes care of looking up addresses. + * The richest syntax is: + * PROTOCOL://USER:PWD@MACHINE:PORT/DATABASE?key=val&... + * Properties are held in their own string and need to be decoded. + * + * We use forward slash for path delimitation of database portion. For the rest we preserve other slashes to allow windows domain\\user or server\\instance. + * Special chars are [/@?,:;] + * if any are found at the end of protocol we skip :// + * if any are found at the beginning of properties we skip ? + * if any are found at the beginning of database we skip / + * + * In windows we have volume or drive letters which are postfixed by colon : then slashes. We treat the volume as part of database. + * We store the database without first slash to allow specification of relative paths. + * To render it as absolute set protocol or host to empty string instead of null. Then a slash will be prefixed and you will get absolute path. + * @author amer + */ +public class Path { + static final String SYMBOLS="/@?,:;"; + String connectstring; + String protocol; ///< protocol guides the interpretation of the other elements in conn, string + String userid; ///< authentication + String password; ///< authorization + String host; ///< machine or computer + String port; ///< access to computer + String database; ///< name of database or filename + String properties; ///< properties are what follows ? in a url + + public Path(String connect) { + setConnectString(connect); + } + + public Path(Path in) { + connectstring=in.connectstring; + protocol=in.protocol; + userid=in.userid; + password=in.password; + host=in.host; + port=in.port; + database=in.database; + properties=in.properties; + } + + @Override + public String toString() { + return getConnectString(); + } + /** + * Converts the path to a file. + * If absolute is true + * @param absolute if true forms absolute path else will return relative path + * @return + */ + public File toFile(boolean absolute){ + String proto=getProtocol(); + String host=getHost(); + try{ + setProtocol(absolute?"":null); + setHost(absolute?"":null); + String path=toString(); + return new File(path); + }finally{ + setProtocol(proto); + setHost(host); + } + } + public URL toURL() throws MalformedURLException{ + String path=toString(); + if(Handy.isBlank(getHost()) && path.contains("://") && !path.contains(":///")) path=path.replace("://",":///"); + return new URL(path); + } + public void clear(){ + connectstring=null; + protocol=null; + userid=null; + password=null; + host=null; + port=null; + database=null; + properties=null; + } + + public String getConnectString() { + if(connectstring!=null) return connectstring; + // assemble the connect string + StringBuilder buf=new StringBuilder(); + //boolean absolute=false; + if(!Handy.isBlank(protocol)){ + buf.append(protocol); + if(SYMBOLS.indexOf(protocol.charAt(protocol.length()-1))<0) buf.append("://"); + } + if(!Handy.isBlank(host)){ + if(userid!=null && password!=null){ + buf.append(userid).append(":").append(password).append("@"); + } + buf.append(host); + if(port!=null) buf.append(":").append(port); + } + if(!Handy.isBlank(database)){ + if(buf.length()>0 && SYMBOLS.indexOf(database.charAt(0))<0){ + // we got something in front so we need to use slash + buf.append("/"); + }else if(protocol!=null || host!=null){ + boolean winvol=database.length()>2 && database.charAt(1)==':' && (database.charAt(2)=='/' || database.charAt(2)=='\\'); + // we got nothing in front but if host or protocol empty but not null we treat as absolute + if(!winvol) buf.append("/"); + } + buf.append(database); + } + if(properties!=null){ + if(SYMBOLS.indexOf(properties.charAt(0))<0) buf.append("?"); + buf.append(properties); + } + connectstring=buf.toString(); + return connectstring; + } + + public void setConnectString(String connect) { + clear(); + if (connect == null) { + return; + } + this.connectstring=connect; + // first get protocol - everything up to : which is not followed by a symbol (includes :// but also c:/ + int oldst=0; + int st=0; + for(int i=0;i<(connectstring.length()-1);i++){ + char curr=connectstring.charAt(i); + if(curr==':'){ + oldst=st; + st=i; + }else if(SYMBOLS.indexOf(curr)!=-1){ + if(curr=='@') st=oldst; // this will back out one : if protocl search ended with @ indicating a server + break; + } + } + if(st==1){ st=0;} // this will supress single letter protocols i.e. c:/ ued in windows as part of database/file + if(2==(st-oldst)) st=oldst; + if(st>0){ + this.protocol=connectstring.substring(0,st); + while(SYMBOLS.indexOf(connectstring.charAt(st))!=-1) st++; // advance over symbols + } + // next assume the rest is a file/database + database = connectstring.substring(st); + // now check for user id and password + st = database.indexOf('@'); + boolean checkhost=st>=0; + if(!Handy.isBlank(protocol)){ + checkhost=!protocol.contains(":file") && !protocol.contains(":mem") && !protocol.equals("file") && !protocol.equals("mem");; + } + if (st != -1) { + userid = database.substring(0, st); + if(userid.contains("%4")) try{userid=URLDecoder.decode(userid,"UTF-8");}catch(Exception e){} + database = database.substring(st + 1); + // now try to split user id into password if possible + st = userid.indexOf(':'); + if (st != -1) { + password = userid.substring(st + 1); + userid = userid.substring(0, st); + } + } + // ok next try to split up machine if possible (only for absolute urls) + st = database.indexOf(':'); + if(st<0) st=database.indexOf('/'); + if (st != -1 && checkhost) { + boolean portfollows=database.charAt(st)==':'; + host = database.substring(0, st); + // now try to recover port + if(portfollows){ + int st2 = database.indexOf(':',st+1); + if(st2<0) st2 = database.indexOf('/',st+1); + if (st2 != -1 && st2>(st+1)) { // we have a port + port = database.substring(st + 1,st2); + st=st2; + }else{ + // no port we have : then / - which is used in windows to indicate volume and we treat as part of database + st=-1; + host=""; + } + } + database = database.substring(st + 1); + } + database=fixSlashes(database); + // finally split the properties from database + st = database.indexOf('?'); + if(st==-1) st=database.indexOf(';'); + if (st != -1) { // we have properties + properties = database.substring(st); + database = database.substring(0, st); + } + } + + /** + * Absolute ResourcePath will have a protocol + */ + public boolean isAbsolute() { + return (protocol != null || host!=null); + } + /// will clear host and protocol using empty string thereby making database absolute path + public Path setAbsolute(){ + setHost(""); + setProtocol(""); + return this; + } + + public String getDatabase() { + return database; + } + + public Path setDatabase(String database) { + this.database = database; + connectstring=null; + return this; + } + + public String getHost() { + return host; + } + + public Path setHost(String host) { + this.host = host; + connectstring=null; + return this; + } + + public String getPassword() { + return password; + } + + public Path setPassword(String password) { + this.password = password; + connectstring=null; + return this; + } + + public String getPort() { + return port; + } + + public Path setPort(String port) { + this.port = port; + connectstring=null; + return this; + } + + public String getProtocol() { + return protocol; + } + + public Path setProtocol(String protocol) { + this.protocol = protocol; + connectstring=null; + return this; + } + + public String getUserid() { + return userid; + } + + public Path setUserid(String userid) { + this.userid = userid; + connectstring=null; + return this; + } + public String getProperties() { + return properties; + } + + public Path setProperties(String userid) { + this.properties = userid; + connectstring=null; + return this; + } + + + public String getBase() { + return Path.getBase(database); + } + + public String getExtension() { + return Path.getExtension(database); + } + + public String getPathItem() { + return Path.getPathItem(database); + } + + /** Ensures that we use forward slashes and that single dot is not present mid or and the end. + * + * @param path a unix or windows or uri path + * @return a path with forward slashes + */ + public static String fixSlashes(String path) { + if(path==null || path.length()==0) return path; + path=path.replace("\\", "/"); + path=path.replace("/./","/"); + while(true){ + if(path.endsWith("/")) path=path.substring(0,path.length()-1); + else if(path.endsWith("/.")) path=path.substring(0,path.length()-2); + else break; + } + return path; + } + + /** returns database path given path and file. + * We assume the path uses forward backslash for delimitation. + */ + public static String getBase(String path) { + int st1 = path.lastIndexOf('/'); + int st2 = path.lastIndexOf('\\'); + int st=st2>st1?st2:st1; + if (st == -1) { + return null; + } + return path.substring(0, st); + } + + public static String getExtension(String path) { + int st=Math.max(path.lastIndexOf('/'),path.lastIndexOf('\\')); + int st2 = path.lastIndexOf('.'); + if (st2 == -1 || (st>0 && st2=url.length()) return null; + return url.substring(base.length()); + } + /** + * unites two paths. + * @param base + * @param url + */ + public static String getUnion(String base,String url){ + if(base==null || base.isEmpty()){ + return url; + } + if(url==null || url.isEmpty()){ + return base; + } + //if(url.startsWith(base)) return url; + if(Handy.indexOf(url,base,0)==0) return url; + StringBuilder ret=new StringBuilder(); + ret.append(base); + if(!base.endsWith("/") && !url.startsWith("/")) ret.append("/"); + if(base.endsWith("/") && url.startsWith("/")) ret.setLength(ret.length()-1); + ret.append(url); + return ret.toString(); + } + /** + * method will split paths used in linux and windows. + * in particular for windows it checks if a single letter precedes a colon in which case it considers it a volume + * and does not split there. + * @param paths paths joined with colon or semi-colon + * @return array of paths + */ + public static String[] splitPaths(String _paths){ + String[] paths=_paths.replaceAll("(;|:|^)([a-zA-Z]):","$1$2##").split("[:;]"); + for (int i = 0; i < paths.length; i++) { + String path=paths[i]; + path = path.replace("##",":"); + path=path.replace("/./","/"); + path=path.replace("//","/"); + path=path.replace("\\.\\","\\"); + path=path.replace("\\\\","\\"); + paths[i]=path; + } + return paths; + } + + /** + * Returns a list of key,value pairs in the order they occur in the string str. + * @param str + */ + public static String[] splitProperties(String str) { + if(str.startsWith("?")) str=str.substring(1); + if(str.startsWith(";")) str=str.substring(1); + return str.split("&"); + } + public static String[] splitKeyValue(String str) { + String[] t=str.split("="); + if(t==null || t.length==0) return null; + t[0]=Handy.trim(t[0],"'\""); + return t; + } + public static String[] split(String str) { + return str.split("/"); + } +} diff --git a/src/main/java/com/reliancy/util/Resources.java b/src/main/java/com/reliancy/util/Resources.java new file mode 100644 index 0000000..3a747e3 --- /dev/null +++ b/src/main/java/com/reliancy/util/Resources.java @@ -0,0 +1,144 @@ +package com.reliancy.util; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class Resources { + public static interface PathRewrite{ + public String rewritePath(String path,Object context); + } + public static URL findFirst(PathRewrite remap,String path,Object ... sp){ + for(Object base:sp){ + if(remap!=null) path=remap.rewritePath(path,base); + if(base instanceof Class){ + URL ret=((Class)base).getResource(path); + return ret; + }else if(base instanceof String){ + File ff=new File(base.toString(),path); + if(ff.exists()){ + try { + return ff.toURI().toURL(); + } catch (MalformedURLException e) { + continue; + } + } + }else if(base instanceof File){ + File ff=new File((File)base,path); + if(ff.exists()){ + try { + return ff.toURI().toURL(); + } catch (MalformedURLException e) { + continue; + } + } + }else if(base instanceof URL){ + try { + URL ret=new URL((URL)base,path); + String proto=ret.getProtocol(); + if(proto.equals("http") || proto.equals("https")){ + HttpURLConnection huc = (HttpURLConnection) ret.openConnection(); + huc.setRequestMethod("HEAD"); + int responseCode = huc.getResponseCode(); + huc.disconnect(); + if(responseCode==HttpURLConnection.HTTP_OK) return ret; + } + if(proto.equals("file")){ + File f=new File(ret.getPath()); + if(f.exists()) return ret; + } + } catch (MalformedURLException e) { + continue; + } catch (IOException e2) { + continue; + } + } + } + return null; + } + public static String toString(URL url) throws IOException{ + return toString(url,StandardCharsets.UTF_8); + } + public static String toString(URL url,Charset chs) throws IOException{ + try(InputStream is=url.openStream()){ + return readChars(is,chs).toString(); + } + } + public static byte[] toBytes(URL url) throws IOException{ + try(InputStream is=url.openStream()){ + return readBytes(is); + } + } + public static long copy(InputStream input, OutputStream output, byte[] buffer) throws IOException { + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + public static long copy(Reader input, Writer output, char[] buffer) throws IOException { + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** Reads a stream in one pass and returns bytes. + * Uses internally Handy.copy and a 4K buffer. + */ + public static final byte[] readBytes(InputStream str) throws IOException{ + ByteArrayOutputStream bout=new ByteArrayOutputStream(); + Resources.copy(str, bout, new byte[4096]); + return bout.toByteArray(); + } + public static final CharSequence readChars(InputStream str) throws IOException{ + return readChars(str,StandardCharsets.UTF_8); + } + public static final CharSequence readChars(InputStream str,Charset chset) throws IOException{ + BufferedReader rdr=new BufferedReader(new InputStreamReader(str,chset)); + StringBuilder ret=new StringBuilder(); + for(String line=rdr.readLine();line!=null;line=rdr.readLine()){ + ret.append(line).append("\n"); + } + return ret; + } + public static CharSequence readChars(Class cls,String name){ + InputStream io=cls.getResourceAsStream(name); + try{ + return readChars(io); + }catch(Exception e){ + return null; + }finally{ + if(io!=null) try{io.close();}catch(Exception e){} + } + } + public static void writeChars(CharSequence seq,OutputStream out,Charset chset) throws IOException{ + OutputStreamWriter dout=new OutputStreamWriter(out,chset); + dout.append(seq); + dout.flush(); + } + public static void writeChars(CharSequence seq,OutputStream out) throws IOException{ + writeChars(seq,out,StandardCharsets.UTF_8); + } + public static void writeBytes(int offset,int len,byte[] seq,OutputStream out) throws IOException{ + out.write(seq,offset, len); + } + +} diff --git a/src/main/java/com/reliancy/util/Template.java b/src/main/java/com/reliancy/util/Template.java new file mode 100644 index 0000000..46276de --- /dev/null +++ b/src/main/java/com/reliancy/util/Template.java @@ -0,0 +1,94 @@ +package com.reliancy.util; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.loader.ResourceLocator; + +/** + * We will manage template rendering thru this class. + */ +public class Template { + static Jinjava jinjava; + static{ + jinjava = new Jinjava(); + jinjava.setResourceLocator(new JinjaLoader()); + } + public static class JinjaLoader implements ResourceLocator{ + @Override + public String getString(String fullName, Charset encoding, JinjavaInterpreter interpreter) throws IOException { + URL loc=Resources.findFirst(null,fullName,Template.search_path); + if(loc==null){ + Logger.getLogger(Template.class.getSimpleName()).warning("Missing template"+fullName); + return ""; + }else{ + return Resources.toString(loc,encoding); + } + } + } + static Object[] search_path; + static HashMap cache=new HashMap<>(); + /** renders a template to string, possibly locates it first. + * + * @param path + * @param context + * @return + * @throws IOException + */ + public static CharSequence render(String path,Map context) throws IOException{ + Template t=find(path,search_path); + if(t==null){ + return null; + }else{ + return t.render(context); + } + } + /** returns a template based on a URL located over a search path. + * + */ + public static Template find(String path,Object ... sp) { + Template ret=cache.get(path); + if(ret!=null) return ret; + URL loc=Resources.findFirst(null, path, (sp!=null && sp.length>0?sp:search_path)); + System.out.println("TLOCL:"+loc); + if(loc==null) return null; + ret=new Template(loc); + cache.put(path,ret); + return ret; + } + public static Object[] search_path(Object...sp){ + if(sp!=null && sp.length>0) search_path=sp; + return search_path; + } + + final URL location; + String source; + public Template(URL location){ + this.location=location; + } + public Template(String src){ + this.location=null; + this.source=src; + } + public URL getLocation(){ + return location; + } + public String getSource(){ + return source; + } + public Template load() throws IOException{ + if(source==null) this.source=Resources.toString(location); + return this; + } + public CharSequence render(Map context) throws IOException{ + if(source==null) load(); + String ret = jinjava.render(source, context); + return ret; + } +} diff --git a/src/main/java/com/reliancy/util/Tokenizer.java b/src/main/java/com/reliancy/util/Tokenizer.java new file mode 100644 index 0000000..bd60852 --- /dev/null +++ b/src/main/java/com/reliancy/util/Tokenizer.java @@ -0,0 +1,260 @@ +package com.reliancy.util; + +import java.util.ArrayList; +import java.util.Iterator; + +/** A utility to help us tokenize text along delimChars. + * This class is a little better than the java version because it allows for escaped delimChars. + * Delimiters are escaped with a slash, also single and double quotes supress delimiting when encountered. + * @author amer + */ +public class Tokenizer implements Iterable,Iterator{ + public static final String WHITECHARS=" \t\r\f\n"; + public static final String DELIMCHARS=" ,:;=<>{}[]()"; + int offset; + CharSequence input; + String delimChars=DELIMCHARS; + String escapeChars="'\""; + String whiteChars=WHITECHARS; + public Tokenizer(CharSequence input){ + this.input=input; + } + public Tokenizer(CharSequence input,int offset){ + this.input=input; + this.offset=offset; + } + + public CharSequence getInput() { + return input; + } + + public int getOffset() { + return offset; + } + + public Tokenizer setOffset(int offset) { + this.offset = offset; + return this; + } + + public Tokenizer setInput(CharSequence input) { + this.input = input; + return this; + } + public boolean hasMoreTokens(){ + if(offset>=input.length()) return false; + for(int i=offset;i buf=new ArrayList(); + final StringBuilder out=new StringBuilder(); + boolean lastSkipped=false; + while(this.nextToken(out)){ + String tok=out.toString(); + out.setLength(0); + if(!withdelims && tok.length()==1 && isElementOf(tok.charAt(0),delimChars)!=-1){ + if(lastSkipped) buf.add(""); + lastSkipped=true; + continue; + } + buf.add(tok); + lastSkipped=false; + } + return buf.toArray(new String[buf.size()]); + } + @Override + public Iterator iterator() { + return this; + } + @Override + public boolean hasNext() { + return this.hasMoreTokens(); + } + + @Override + public String next() { + return this.nextToken(); + } + public static int isElementOf(char ch,String d){ + if(d==null) return -1; + return d.indexOf(ch); + } + /**Returns the next token and updated offset. + * This is an inline tokenizer for text parsing and the workhorse of the class. + * It stops when it encounters a delimiter. It treats delimChars as tokens too. + * It advances the offset whenever it was able to move be it delimiter or not. + * We should not have to adjust it for repeated calls except for special cases. + * @param offset + * @param sets various char sets 0-delimiters,1-escape chars,3-white chars + * @param input input chars + * @param out value of the token + * @return offset after processing + */ + public static int nextToken(int offset,CharSequence input, StringBuilder out,String[] sets){ + String delimChars=(sets!=null && sets.length>=1)?sets[0]:",:;=<>{}[]()"; + String escapeChars=(sets!=null && sets.length>=2)?sets[1]:null; + String whiteChars=(sets!=null && sets.length>=3)?sets[2]:null; + int escChar=-1; // if not -1 then we are escaping + char lastChar=0; + char curChar=0; + int lastOffset=offset; + int isWhiteChar=-1; + int isDelimChar=-1; + boolean weakEscape=false; + int controlCount=0; // counts number of \\ to prevent shortcuit on even number + while(offset=0 || isWhiteChar==-1 || isDelimChar!=-1){ + // emit if escaping or if delimiter or not white char + out.append(curChar); + } + if(isDelimChar!=-1) break; // exit delimiter found + } + if(isDelimChar!=-1 && lastOffset<(offset-1)){ + // fix end of out to not have a delimiter if it has any other string + offset-=1;out.setLength(out.length()-1); + } + return offset; + } + /**Returns the next token and updated offset. + * An improved inline tokenizer using various rules to control delimiting, escaping and text swallowing. + * We supply an array of events or if none is provided a default delimiter event is constructed. + * After that the events are used to control tokenization. We enter a loop and feed the input to + * the events if one or more are armed or triggered (state >=0) we defer emiting chars to output until we determine what to do. + * For events that do escape we just defer until end of escape is detected, for delimit we return back and + * for supress we just swallow the input without emitting it. + */ + /* + public static int nextToken(TokenizerRule state,int offset,CharSequence input, StringBuilder out){ + int emitCount=0; + int oldOffset=offset; + while(offset0){ + offset-=(state.getSize()-1); + } + case TokenizerRule.DO_EXITAFTER: + if(emitCount==0 && oldOffset<=offset){ + // if there is anything left + offset++; + out.append(input,oldOffset,offset); + emitCount+=(offset-oldOffset); + oldOffset=offset; + } + state.clear(); + default: + return st>=0?st:offset; + + } + } + return offset; + } + */ + +} diff --git a/src/test/java/com/reliancy/dbo/TerminalTest.java b/src/test/java/com/reliancy/dbo/TerminalTest.java new file mode 100644 index 0000000..0365dfa --- /dev/null +++ b/src/test/java/com/reliancy/dbo/TerminalTest.java @@ -0,0 +1,42 @@ +package com.reliancy.dbo; +import java.io.IOException; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.Test; +public class TerminalTest { + public static class Maps extends DBO{ + public static Field map_id=new Field("Map_id",Integer.class); + public static Field map_name=new Field("Map_name",String.class); + static{ + Entity.publish(Maps.class); + } + } + /** + * Plain CRUD + * @throws IOException + * @throws SQLException + */ + @Test + public void connection() throws IOException, SQLException{ + String url="jdbc:postgresql://postgres:Ramudin99@bigbang:5432/Test"; + SQLTerminal t=new SQLTerminal(url); + try(Connection c=t.getConnection()){ + System.out.println("Connection:"+c); + try (Statement stmt = c.createStatement()) { + // use stmt here + String sql = "SELECT * from \"dbo\".\"Maps\""; + try (ResultSet resultSet = stmt.executeQuery(sql)) { + // use resultSet here + while (resultSet.next()) { + System.out.println("ROw:"+resultSet.getInt("Map_id")+":"+resultSet.getString("Map_name")); + } + } + + } + } + } + +} diff --git a/src/test/java/com/reliancy/jabba/RouterTest.java b/src/test/java/com/reliancy/jabba/RouterTest.java new file mode 100644 index 0000000..3922b0e --- /dev/null +++ b/src/test/java/com/reliancy/jabba/RouterTest.java @@ -0,0 +1,47 @@ +package com.reliancy.jabba; + +import static org.junit.Assert.assertTrue; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.Test; +/** + * Unit test for simple App. + */ +public class RouterTest +{ + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() + { + Pattern p=Pattern.compile("(/hello)|(/hello2(/c))|(/hello3)"); + Matcher m=p.matcher("/hello2/c"); + if(m.matches()){ + for(int i=0;i pms=new HashMap<>(); + String rt=rep.evalMatcher(m,pms); + System.out.println(rt); + System.out.println(pms); + } + } +} diff --git a/src/test/java/com/reliancy/rec/ObjTest.java b/src/test/java/com/reliancy/rec/ObjTest.java new file mode 100644 index 0000000..7220270 --- /dev/null +++ b/src/test/java/com/reliancy/rec/ObjTest.java @@ -0,0 +1,37 @@ +package com.reliancy.rec; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.Test; + +public class ObjTest { + /** + * Plain CRUD + * @throws IOException + */ + @Test + public void crudVec() throws IOException + { + Obj o=new Obj(); + Obj a=new Obj(true); + System.out.println("O1:"+o); + System.out.println("A1:"+a); + a.add(1).add("three"); + o.add(1).add("three").set(new Slot("arr"),new String[]{"a","b","c"}); + System.out.println("O2meta:"+o.isArray()+"/"+o.meta()); + System.out.println("O2:"+o); + System.out.println("A2:"+a); + o.set(o.getSlot("car"),"bar"); + System.out.println("O3:"+o); + StringBuilder json=new StringBuilder(); + JSON.writes(o,json); + System.out.println("ENC:"+json); + Rec dec=JSON.reads(json); + System.out.println("DEC:"+dec); + } + +} diff --git a/var/base.j2 b/var/base.j2 new file mode 100644 index 0000000..b93bd5c --- /dev/null +++ b/var/base.j2 @@ -0,0 +1,27 @@ + + + + Flask Template Example + + + + + +
+

This is part of my base template

+
+ {% block content %}{% endblock %} +
+

This is part of my base template

+
+ + + + + \ No newline at end of file diff --git a/var/favicon.ico b/var/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..72b6083bd61f7d33994eb721138e6200d57f1eb7 GIT binary patch literal 32038 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