inital commit

This commit is contained in:
2021-11-02 13:38:59 -05:00
commit 01dd8525b1
65 changed files with 5267 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="build/classes/java/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="build/classes/java/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="build-default"/>
</classpath>
Vendored
+35
View File
@@ -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
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>jabba</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1614986289528</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>
@@ -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
@@ -0,0 +1,4 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/test/java=UTF-8
encoding/<project>=UTF-8
+2
View File
@@ -0,0 +1,2 @@
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=false
+9
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1
+132
View File
@@ -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
+15
View File
@@ -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
+181
View File
@@ -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/"
}
}
}
}
+145
View File
@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.reliancy.jabba</groupId>
<artifactId>jabba</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>jabba</name>
<url>http://www.reliancy.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>11.0.7</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>11.0.1</version>
</dependency>
<dependency>
<groupId>com.hubspot.jinjava</groupId>
<artifactId>jinjava</artifactId>
<version>2.5.10</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
</execution>
</executions>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</plugin>
<!-- Maven Assembly Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<!-- get all project dependencies -->
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<!-- MainClass in mainfest make a executable jar -->
<archive>
<manifest>
<mainClass>com.reliancy.jabba.Router</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<!-- bind to the packaging phase -->
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<mainClass>com.reliancy.jabba.Router</mainClass>
</configuration>
</plugin>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
+164
View File
@@ -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<DBO>,CloseableIterator<DBO>{
public static enum Type{
NONE,LOAD,SAVE,DELETE
}
Terminal terminal;
Type type;
Entity entity;
Object[] params;
CloseableIterator<DBO> 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<DBO> it=null;
if(itms!=null){
it=new CloseableIterator<DBO>() {
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<DBO> itms){
if(items!=null){
try {
items.close();
} catch (Exception e) {
}
}
items=itms;
return this;
}
protected CloseableIterator<DBO> getItems(){
return items;
}
@Override
public Iterator<DBO> 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;
}
}
@@ -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;
}
}
+34
View File
@@ -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;
}
}
@@ -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<String,Entity> 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<? extends DBO> cls){
return null;
}
Entity base;
String dbName;
Field pk;
public Entity(String name) {
super(name);
}
public Field getPk(){
return pk;
}
}
+16
View File
@@ -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);
}
}
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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<String,Object> 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<String,AppSession> directory){
}
/** allows specialized appsessions to deregister with more ids. */
protected void onRetract(HashMap<String,AppSession> directory){
}
static HashMap<String,AppSession> 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;
}
}
@@ -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{
}
}
@@ -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<Processor> 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<CallSession> instance=new ThreadLocal<>();
public static CallSession getInstance(){
CallSession ret=instance.get();
if(ret==null) instance.set(ret=new CallSession());
return ret;
}
}
@@ -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);
}
@@ -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;
}
@@ -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<String,Object[]> map;
final HashMap<String,Filter> 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<String> streamRoutes() {
return map.keySet().stream();
}
public Iterator<String> enumRoutes(){
return map.keySet().iterator();
}
public void exportRoutes(RouterEndPoint rep) {
streamRoutes().forEach(up->rep.addRoute("GET",up+".*",this));
}
}
@@ -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<String,String> 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;
}
}
@@ -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<argVals.length;i++){
Parameter p=params[i];
Class<?> 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);
}
}
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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<String,String> pathParams=new HashMap<>();
public Request(HttpServletRequest http_request) {
this.http_request = http_request;
}
public Map<String,String> 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();
}
}
@@ -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<HTTP.Header> headers=new ArrayList<>();
protected final ArrayList<HTTP.Cookie> 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<HTTP.Header> 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<HTTP.Cookie> getCookies(){
return cookies;
}
}
@@ -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<String> 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;
}
}
@@ -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 "";
}
@@ -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<Method> 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<String, Object> 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;
}
}
@@ -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<String,EndPoint> routes=new HashMap<>();
HashMap<String,ArrayList<String>> routeParams=new HashMap<>();
ArrayList<String> 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<String,String> 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<String> params=new ArrayList<String>();
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<String,String> p){
String gstr=null;
int gindex=1;
while(gindex<m.groupCount() && gstr==null){
gstr=m.group(gindex);
if(gstr==null) gindex++;
}
if(gstr==null) return null; // no group found
// now find route given group
int rindex=-1;
for (int i = 0; i < indexes.length && rindex<0; i++) {
if(gindex==indexes[i]) rindex=i;
}
if(rindex<0) return null; // we can't match route to group
String ret=patterns.get(rindex);
if(p!=null && routeParams.containsKey(ret)){
ArrayList<String> pms=routeParams.get(ret);
for(int i=0;i<pms.size();i++){
String val=m.group(gindex+2+i);
String byName=pms.get(i).toLowerCase();
p.put(byName,val);
p.put("_arg"+i,val);
}
}
return ret;
}
}
@@ -0,0 +1,11 @@
package com.reliancy.jabba;
/** Session is temporary storage.
* we will have application session but also call session.
* for methodendpoints we must still be able to access request,response objects.
*
*/
public interface Session {
public void setValue(String key,Object val);
public Object getValue(String key);
}
@@ -0,0 +1,11 @@
package com.reliancy.jabbasec;
public class NotAuthentic extends RuntimeException {
public NotAuthentic(String message){
super(message);
}
public NotAuthentic(String message,Throwable cause){
super(message,cause);
}
}
@@ -0,0 +1,12 @@
package com.reliancy.jabbasec;
/**
* Our own exception to throw when we are not allowed access.
*/
public class NotPermitted extends RuntimeException{
public NotPermitted(String message){
super(message);
}
public NotPermitted(String message,Throwable cause){
super(message,cause);
}
}
@@ -0,0 +1,20 @@
package com.reliancy.jabbasec;
import java.util.List;
/**
* Any entity that can be secured or permissions set for an actor.
*/
public interface Securable {
public Integer getId();
public Integer getOwnerId();
public String getKind();
public String getName();
public String getTitle();
public String getIcon();
public Securable getOwner();
public List<Securable> getOwnedSecurables();
public List<SecurityPermit> getDirectPermits();
public SecurityPermit getPermit(Securable sec);
public SecurityPolicy getPolicy();
}
@@ -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{
}
@@ -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();
}
@@ -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<SecurityProtocol> 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<String,String> 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{
}
}
@@ -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;
}
}
}
@@ -0,0 +1,4 @@
{% extends "base.j2" %}
{% block content %}
<div>Hello from code: {{name}}!</div>
{% endblock %}
@@ -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);
}
+126
View File
@@ -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<Slot> 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 extends Hdr> T castAs(Class<T> clazz){
return clazz.cast(this);
}
public int findSlot(String name){
return findSlot(name,0);
}
public int findSlot(String name,int ofs){
ListIterator<Slot> 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<Slot> 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<slots.length;i++) keys.add(slots[i]);
}
return keys.toArray(new Slot[keys.size()]);
}
}
+22
View File
@@ -0,0 +1,22 @@
package com.reliancy.rec;
import java.io.IOException;
/**
* Static methods related to JSON format.
*/
public class JSON {
private JSON(){
}
public static final Rec reads(CharSequence seq){
JSONDecoder dec=new JSONDecoder();
dec.beginDocument();
dec.parse(0, seq);
return dec.endDocument();
}
public static final void writes(Rec rec,Appendable sink) throws IOException{
JSONEncoder.encode(rec, sink);
}
}
@@ -0,0 +1,331 @@
/*
* 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;
import java.util.LinkedList;
import com.reliancy.util.Tokenizer;
import com.reliancy.util.Handy;
/** Special class which will tokenize string according to rules for JSON and feed the info to a listener.
*
* @author amer
*/
public class JSONDecoder implements TextDecoder,DecoderSink {
DecoderSink handler;
String[] inBody;
String[] sets;
String lastToken=null;
StringBuilder out = new StringBuilder();
public JSONDecoder(DecoderSink h){
handler=h;
String delimChars="{}[],;:=";
String escapeChars="'\"";
String whiteChars=" \t\r\f\n";//" \t\r\f\n";
inBody = new String[]{delimChars,escapeChars,whiteChars};
sets=inBody;
}
public JSONDecoder(){
this(null);
handler=this;
}
@Override
public int parse(int offset,CharSequence in){
int noffset=0;
while((noffset = Tokenizer.nextToken(offset, in, out, sets))!=offset){
offset=noffset;
if(out.length()==0) continue;
String token=out.toString();
out.setLength(0);
if("{".equals(token)){
if(lastToken!=null){
if(lastToken.startsWith("/*") || lastToken.startsWith("//")){
handler.setValue(lastToken); // support comments in our stream
}else{
handler.setKey(lastToken); // we consider string before { a key or name unless comment
}
lastToken=null;
}
handler.beginElement("object");
}else if("}".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.endElement("object");
}else if("[".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.beginElement("array");
}else if("]".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.endElement("array");
}else if(",".equals(token) || ";".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
}else if(":".equals(token) || "=".equals(token)){
if(lastToken!=null){
handler.setKey(lastToken);
lastToken=null;
}
}else{
lastToken=token;
}
}
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
return offset;
}
Slot KEY=new Slot("__key",String.class);
/** We use a stack structure to manage recusion. */
LinkedList<Rec> stack=new LinkedList<Rec>();
/** 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<stop && (name.charAt(start)=='"' || name.charAt(start)=='\'')) start++;
while(start<stop && (name.charAt(stop-1)=='"' || name.charAt(stop-1)=='\'')) stop--;
sub.set(KEY, name.subSequence(start, stop));
//System.out.println("BeginAttribute:"+name);
}
@Override
public void setValue(CharSequence seq) {
if(seq==null) return;
Rec sub=this.getSubject();
String key=(String) sub.get(KEY,null);
if(key==null){
if(isWhitespaceIgnored() && Handy.isEmpty(seq)){
// skip empty strings
return;
}
// now key we are adding to body
Object val=interpretString(seq);
sub.add(val);
}else{
// we are setting attribute
Object val=interpretString(seq);
Slot keyslot=sub.getSlot(key);
sub.remove(KEY).set(keyslot,val);
// it should bomb if array and comes with key
//sub.setArray(false); // if it needs to be array why does it have a key
}
//System.out.println("Data:"+seq);
}
public Object interpretString(CharSequence seq){
int start=0;int stop=seq.length();
while(start<stop && seq.charAt(start)=='"' && seq.charAt(stop-1)=='"'){
start++;
stop--;
}
if(start==0 && stop==seq.length()){
// we do not trim single quotes unless double are missing
while(start<stop && seq.charAt(start)=='\'' && seq.charAt(stop-1)=='\''){
start++;
stop--;
}
}
seq=seq.subSequence(start, stop);
Object val=seq;
if(start==0){
String sVal=String.valueOf(seq);
// we did not have quotes - so try to interpet a few things
if("null".equalsIgnoreCase(sVal)){
val=null;
}else
if("true".equalsIgnoreCase(sVal)){
val=Boolean.TRUE;
}else
if("false".equalsIgnoreCase(sVal)){
val=Boolean.FALSE;
}else
if(Handy.isNumeric(sVal)){
if (sVal.indexOf(".") >= 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;
}
}
@@ -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<val.count();i++) {
Slot k=val.getSlot(i);
Object v=val.get(i);
if (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;
}
}
+153
View File
@@ -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<Object> 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<Slot> k,List<Object> 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;pos<count();pos++){
if(pos>0) 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;pos<count();pos++){
if(pos>0) 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;
}
}
+20
View File
@@ -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;
}
}
+60
View File
@@ -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;
}
}
@@ -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);
}
+18
View File
@@ -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);
}
@@ -0,0 +1,7 @@
package com.reliancy.util;
import java.io.Closeable;
import java.util.Iterator;
public interface CloseableIterator<T> extends Iterator<T>, Closeable {
}
+458
View File
@@ -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> 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<arr.length;i++) if(isEmpty(arr[i])==false) return false;
return true;
}if(value instanceof byte[]){
return ((byte[])value).length==0;
}else if(value instanceof short[]){
return ((short[])value).length==0;
}else if(value instanceof int[]){
return ((int[])value).length==0;
}else if(value instanceof long[]){
return ((long[])value).length==0;
}else if(value instanceof float[]){
return ((float[])value).length==0;
}else if(value instanceof double[]){
return ((double[])value).length==0;
}else{
return false;
}
}
if(value instanceof Collection){
Collection<?> 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;i<str.length();i++){
char currCh=str.charAt(i);
if(i==0 && Character.isLowerCase(currCh)) fix=true;
if(Character.isLowerCase(prevCh) && Character.isUpperCase(currCh)) fix=true;
if(Character.isUpperCase(prevCh) && Character.isUpperCase(currCh) && i<(str.length()-1) && Character.isLowerCase(str.charAt(i+1))) fix=true;
if(!Character.isLetter(currCh)) fix=true;
prevCh=currCh;
}
if(!fix) return str;
StringBuilder bufs=new StringBuilder();
boolean toUC=false;
for(int i=0;i<str.length();i++){
char currCh=str.charAt(i);
if(currCh=='_') currCh=' ';
prevCh=bufs.length()>0?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<e.length;i++){
int other=rn.nextInt(e.length);
Object tmp=e[i];
e[i]=e[other];
e[other]=tmp;
}
}
public static String encodeBase64(byte[] data){
return Base64.getEncoder().encodeToString(data);
}
public static byte[] decodeBase64(String data){
return Base64.getDecoder().decode(data);
}
/** Simple XOR encryption of a map of key-value pairs.
* We randomize the order of key value pairs to make the string more unpredictable.
* Returned string is base64 and web safe
* @param key
* @param m
* @return a string of encoded map key-value pairs which were then encrypted
*/
public static final String encrypt(String key,Map<String,String> 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;i<es.length;i++){
Object e=es[i];
if(i>0) 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<bstr.length;i++){
bstr[i]=(byte)(bstr[i] ^ bkey[i%bkey.length]);
}
// now need to encode this
ret=encodeBase64(bstr);
ret=ret.replace('+','-');
ret=ret.replace('/','_');
ret=ret.replace('=','.');
ret=ret.replace("\n","");
}catch(Exception e){
ret="";
}
return ret;
}
/**Reverses the effects of encrypt.
* Also changes
* @param key
* @param m
* @return values decrypted and parsed into key-value pair along newline.
*/
public static final Map<String,String> decrypt(String key,String m){
m=decryptString(key,m);
Map<String,String> 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<bstr.length;i++){
bstr[i]=(byte)(bstr[i] ^ bkey[i%bkey.length]);
}
m=new String(bstr,"UTF-8");
}catch(Exception e){
}
return m;
}
/**
* Generates a hash string with the algorithm name prefixed.
* @param message
* @param algorithm
* @return
*/
public static String hashString(String message, String algorithm) throws NoSuchAlgorithmException, UnsupportedEncodingException{
if(message==null) return message;
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] hashedBytes = digest.digest(message.getBytes("UTF-8"));
return algorithm.toLowerCase()+":"+encodeBase64(hashedBytes);
}
public static String hashSHA256(String message){
try{
return hashString(message,"SHA-256");
}catch(Exception ex){
return "sha-256:"+Integer.toHexString(message.hashCode());
}
}
public static String hashMD5(String input){
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());
BigInteger no = new BigInteger(1, messageDigest);
String hashtext = no.toString(16);
while (hashtext.length() < 32) {
hashtext = "0" + hashtext;
}
return hashtext;
}catch (NoSuchAlgorithmException e) { // For specifying wrong message digest algorithms
throw new RuntimeException(e);
}
}
public static String toHexString(byte[] hash){
char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte b : hash) {
sb.append(HEX_CHARS[(b & 0xF0) >> 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<blen;index++){
char bC=body.charAt(index);
char sC=sub.charAt(state);
if(ignorecase){
bC=Character.toLowerCase(bC);
sC=Character.toLowerCase(sC);
}
if(bC==sC) state+=1; else state=0;
if(state>=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<trim.length() && sym.indexOf(trim.charAt(start))!=-1) start++;
while(0<end && sym.indexOf(trim.charAt(end-1))!=-1) end--;
if(start==0 && end==trim.length()) return trim;
return start<end?trim.substring(start, end):"";
}
/** will copy contents of a list into a fixed length array */
public static String[] asArray(List<String> all) {
if(all==null) return null;
String[] ret=new String[all.size()];
all.toArray(ret);
return ret;
}
}
+418
View File
@@ -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<st)) {
return null;
}
return path.substring(st2 + 1);
}
public static String getPathItem(String path) {
path=path.replace('\\','/');
int st11 = path.lastIndexOf('/');
int st1=1+st11;
int st2 = path.lastIndexOf('.');
if (st2 <0) {
st2 = path.length();
}
return path.substring(st1, st2);
}
/**
* Assuming that url starts with base will return string beyond base in url.
* @param base
* @param url
*/
public static String getRemainder(String base,String url){
if(base.length()>=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("/");
}
}
@@ -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);
}
}
@@ -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<String,Template> 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<String,?> 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<String,?> context) throws IOException{
if(source==null) load();
String ret = jinjava.render(source, context);
return ret;
}
}
@@ -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<String>,Iterator<String>{
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<input.length();i++){
char ch=input.charAt(i);
if(Tokenizer.isElementOf(ch,whiteChars)==-1) return true;
}
return false;
}
public String nextToken(){
final StringBuilder out=new StringBuilder();
if(nextToken(out)){
return out.toString();
}else{
return null;
}
}
public boolean nextToken(StringBuilder out){
String[] sets={delimChars,escapeChars,whiteChars};
int noffset=nextToken(offset,input,out,sets);
if(noffset==offset){
return false;
}else{
offset=noffset;
return true;
}
}
public String getDelimChars() {
return delimChars;
}
public Tokenizer setDelimChars(String delimChars) {
this.delimChars = delimChars;
return this;
}
public String getEscapeChars() {
return escapeChars;
}
public Tokenizer setEscapeChars(String escapeChars) {
this.escapeChars = escapeChars;
return this;
}
public String getWhiteChars() {
return whiteChars;
}
public Tokenizer setWhiteChars(String whiteChars) {
this.whiteChars = whiteChars;
return this;
}
/**
* Utility method which collects all tokens and returns an array of them.
* Use it for small length strings when parsing user input.
* @param withdelims if tru returns delimiters as well
*/
public String[] getTokens(boolean withdelims){
final ArrayList<String> buf=new ArrayList<String>();
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<String> 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<input.length()){ // only scan until the end
lastChar=curChar;
curChar=input.charAt(offset++); // from here on offset is ahead
isWhiteChar=isElementOf(curChar,whiteChars);
isDelimChar=isElementOf(curChar,delimChars);
// determine if we should ignore testing for exit
int isEscapeChar=isElementOf(curChar,escapeChars);
controlCount=(lastChar=='\\')?controlCount+1:0; // control count counts number of \\ to
if((controlCount%2)==1){
isDelimChar=isEscapeChar=-1; // shortcircuit delimiting or escaping if prev char was \\ but only unevent number of times
}
if(escChar==-1){ // should we enter escaping
if(isEscapeChar!=-1){
// will enter escChar but only once
escChar=isEscapeChar;
}
}else{ // should we exit escaping
if(weakEscape==false) isDelimChar=-1; // shortcircuit delim signal if in escape mode and not weak
// exit back to normal if escape found second time
if(isEscapeChar==escChar){
// special rule:if oldchar==curchar and next is not delim or whitespace we ignore escape char
boolean isletter=offset<input.length() && !(isElementOf(input.charAt(offset),delimChars)!=-1 || isElementOf(input.charAt(offset),whiteChars)!=-1);
if(lastChar==curChar && isletter){
// we are special enter weak escaping (where delimiter is not ignored)
// this will correct spurios double quotes but will recover forgotter delimiters between two parts
// we stay in escape mode but listen for delims
weakEscape=true;
}else{
escChar=-1; // we are exiting escaping
}
}
}
// emit chars and test for exit
if(escChar>=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(offset<input.length()){
int st=state.consume(offset, input);
st=(st==TokenizerRule.DO_DEFER && offset==(input.length()))?TokenizerRule.DO_EMIT:st;
switch(st){
case TokenizerRule.DO_EMIT:
// we can emit what we got so far
if(oldOffset<=offset){
offset++;
out.append(input,oldOffset,offset);
emitCount+=(offset-oldOffset);
oldOffset=offset;
}
break;
case TokenizerRule.DO_DEFER:
// we need to defer emitting
offset++;
break;
case TokenizerRule.DO_SKIP:
// we just skip over this input
offset++;
oldOffset=offset;
break;
case TokenizerRule.DO_EXITBEFORE:
if(emitCount>0){
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;
}
*/
}
@@ -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"));
}
}
}
}
}
}
@@ -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<m.groupCount();i++){
System.out.println(i+":"+m.group(i));
}
}
assertTrue( true );
}
@Test
public void initRouter()
{
//assertTrue( true );
System.out.println("Test router init...");
Router r=new Router();
RouterEndPoint rep=r.importEndPoints(r);
rep.compile();
//Matcher m=rep.match("GET","/helloPlain");
Matcher m=rep.match("GET","/hello3/45");
//Matcher m=rep.match("GET","/helloP");
if(m!=null){
HashMap<String,String> pms=new HashMap<>();
String rt=rep.evalMatcher(m,pms);
System.out.println(rt);
System.out.println(pms);
}
}
}
@@ -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);
}
}
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Flask Template Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<style type="text/css">
.container {
max-width: 500px;
padding-top: 100px;
}
h2 {color: red;}
</style>
</head>
<body>
<div class="container">
<h2>This is part of my base template</h2>
<br>
{% block content %}{% endblock %}
<br>
<h2>This is part of my base template</h2>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB