├── .ceylon ├── bootstrap │ ├── ceylon-bootstrap.jar │ └── ceylon-bootstrap.properties ├── config ├── gyokuro.format └── ide-config ├── .classpath ├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .project ├── .settings ├── org.eclipse.core.resources.prefs └── org.eclipse.jdt.core.prefs ├── .travis.yml ├── LICENSE ├── README.md ├── ceylonb ├── ceylonb.bat ├── demos-assets ├── css │ └── main.css ├── gson │ └── index.html ├── index.html ├── js │ └── main.js ├── mustache │ └── hello.mustache ├── rythm │ └── hello.rythm └── thymeleaf │ └── hello.xhtml ├── demos └── gyokuro │ └── demo │ ├── ceylonhtml │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon │ ├── gson │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon │ ├── mustache │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon │ ├── report │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon │ ├── rest │ ├── controllers.ceylon │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon │ ├── rythm │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon │ ├── spring │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon │ └── thymeleaf │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon ├── gyokuro.iml ├── overrides.xml ├── resource └── com │ └── github │ └── bjansen │ └── gyokuro │ └── report │ └── style.css ├── source └── net │ └── gyokuro │ ├── core │ ├── Flash.ceylon │ ├── annotations.ceylon │ ├── application.ceylon │ ├── functions.ceylon │ ├── http │ │ ├── methods.ceylon │ │ └── package.ceylon │ ├── internal │ │ ├── AnnotationScanner.ceylon │ │ ├── DefaultFlash.ceylon │ │ ├── RequestWrapper.ceylon │ │ ├── converters.ceylon │ │ ├── dispatcher.ceylon │ │ ├── package.ceylon │ │ └── router.ceylon │ ├── mimeparse.ceylon │ ├── module.ceylon │ ├── package.ceylon │ └── websocket.ceylon │ ├── report │ ├── GyokuroApiGenerator.ceylon │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon │ ├── transform │ ├── api │ │ ├── Transformer.ceylon │ │ ├── module.ceylon │ │ └── package.ceylon │ └── gson │ │ ├── GsonTransformer.ceylon │ │ ├── module.ceylon │ │ └── package.ceylon │ └── view │ ├── api │ ├── module.ceylon │ ├── package.ceylon │ └── templates.ceylon │ ├── ceylonhtml │ ├── CeylonHtmlRenderer.ceylon │ ├── module.ceylon │ └── package.ceylon │ ├── mustache │ ├── MustacheRenderer.ceylon │ ├── module.ceylon │ └── package.ceylon │ ├── pebble │ ├── PebbleRenderer.ceylon │ ├── module.ceylon │ └── package.ceylon │ ├── rythm │ ├── RythmRenderer.ceylon │ ├── module.ceylon │ └── package.ceylon │ └── thymeleaf │ ├── ThymeleafRenderer.ceylon │ ├── module.ceylon │ └── package.ceylon └── test-source └── test └── net └── gyokuro └── core ├── filtersTest.ceylon ├── internal ├── AnnotationScannerTest.ceylon ├── RequestDispatcherTest.ceylon ├── package.ceylon └── testdata │ ├── AnnotatedClass.ceylon │ ├── ListBinding.ceylon │ ├── ParameterBinding.ceylon │ └── package.ceylon ├── mimeParseTest.ceylon ├── module.ceylon └── package.ceylon /.ceylon/bootstrap/ceylon-bootstrap.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjansen/gyokuro/a3f0db80f7029ae350bc97e33eaf7882a9182d00/.ceylon/bootstrap/ceylon-bootstrap.jar -------------------------------------------------------------------------------- /.ceylon/bootstrap/ceylon-bootstrap.properties: -------------------------------------------------------------------------------- 1 | #Generated by 'ceylon bootstrap' 2 | #Tue Jan 10 20:17:50 CET 2017 3 | distribution=https\://ceylon-lang.org/download/dist/1_3_1 4 | -------------------------------------------------------------------------------- /.ceylon/config: -------------------------------------------------------------------------------- 1 | 2 | [defaults] 3 | encoding=UTF-8 4 | offline=false 5 | flatclasspath=false 6 | autoexportmavendependencies=false 7 | overrides=overrides.xml 8 | 9 | [compiler] 10 | source=source 11 | source=demos 12 | source=test-source 13 | 14 | [formattool] 15 | profile=gyokuro 16 | 17 | [runtool] 18 | module=gyokuro.demo.rest 19 | 20 | -------------------------------------------------------------------------------- /.ceylon/gyokuro.format: -------------------------------------------------------------------------------- 1 | 2 | [formatter] 3 | lineBreakStrategy=default 4 | maxLineLength=unlimited 5 | lineBreaksBetweenImportElements=1..1 6 | elseOnOwnLine=false 7 | lineBreaksBeforeMultiComment=0..3 8 | indentBeforeTypeInfo=2 9 | spaceOptionalAroundOperatorLevel=3 10 | spaceAfterValueIteratorOpeningParenthesis=false 11 | indentationAfterSpecifierExpressionStart=addIndentBefore 12 | lineBreaksBeforeSingleComment=0..3 13 | spaceAroundSatisfiesOf=true 14 | spaceBeforeParamListClosingParen=false 15 | inlineAnnotations=abstract actual annotation default final formal late native optional sealed shared variable controller 16 | lineBreaksInTypeParameterList=0..1 17 | spaceAroundImportAliasEqualsSign=false 18 | spaceBeforeAnnotationPositionalArgumentList=false 19 | spaceAfterTypeParamListComma=true 20 | spaceAfterParamListOpeningParen=false 21 | spaceBeforeValueIteratorClosingParenthesis=false 22 | spaceAfterParamListClosingParen=true 23 | indentMode=4 spaces 24 | braceOnOwnLine=false 25 | spaceBeforeMethodOrClassPositionalArgumentList=false 26 | lineBreaksAfterSingleComment=0..3 27 | spaceAfterSequenceEnumerationOpeningBrace=true 28 | spaceBeforeParamListOpeningParen=false 29 | spaceAfterTypeArgListComma=false 30 | spaceAroundTypeParamListEqualsSign=false 31 | failFast=true 32 | lineBreaksAfterLineComment=1..3 33 | lineBreaksBeforeLineComment=0..3 34 | spaceAfterControlStructureKeyword=true 35 | lineBreak=lf 36 | spaceBeforeSequenceEnumerationClosingBrace=true 37 | indentBlankLines=true 38 | lineBreaksAfterMultiComment=0..3 39 | -------------------------------------------------------------------------------- /.ceylon/ide-config: -------------------------------------------------------------------------------- 1 | 2 | [project] 3 | compile-jvm=true 4 | compile-js=false 5 | system-repository= 6 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.exploded/ 2 | /classes/ 3 | config.old 4 | modules 5 | /.idea/libraries/ 6 | /.idea/workspace.xml 7 | /.idea/shelf -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | gyokuro -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | General 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | gyokuro 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | com.redhat.ceylon.eclipse.ui.ceylonBuilder 15 | 16 | 17 | explodeModules 18 | true 19 | 20 | 21 | 22 | 23 | 24 | com.redhat.ceylon.eclipse.ui.ceylonNature 25 | org.eclipse.jdt.core.javanature 26 | 27 | 28 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding/=UTF-8 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=1.7 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 11 | org.eclipse.jdt.core.compiler.source=1.7 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | jdk: 4 | - openjdk7 5 | install: 6 | - wget --quiet --output-document=/tmp/ceylon.zip $CEYLON 7 | - unzip /tmp/ceylon.zip 8 | - export PATH=$PATH:$PWD/ceylon-1.3.3/bin/ 9 | before_script: 10 | - ceylon compile 11 | script: 12 | - ceylon test $TEST_MODULE 13 | env: 14 | global: 15 | - CEYLON="https://downloads.ceylon-lang.org/cli/ceylon-1.3.3.zip" 16 | - TEST_MODULE="test.net.gyokuro.core" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Bastien Jansen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gyokuro [![Travis](https://travis-ci.org/bjansen/gyokuro.svg?branch=master)](https://travis-ci.org/bjansen/gyokuro) [![Gitter](https://badges.gitter.im/bjansen/gyokuro.svg)](https://gitter.im/bjansen/gyokuro) 2 | 3 | A web framework written in Ceylon, which allows: 4 | 5 | * routing GET/POST requests to simple `(Request, Response)` handlers 6 | * creating annotated controllers containing more powerful handlers 7 | * serving static assets (HTML, CSS, JS, ...) from a directory 8 | 9 | gyokuro is based on the [Ceylon SDK](http://github.com/ceylon/ceylon-sdk), 10 | and uses `ceylon.net`'s server API. 11 | 12 | ## Creating a simple webapp 13 | 14 | Create a new Ceylon module: 15 | 16 | ```ceylon 17 | module gyokuro.demo.rest "1.0.0" { 18 | import net.gyokuro.core "0.2"; 19 | import ceylon.net "1.3.1"; 20 | } 21 | ``` 22 | 23 | Add a runnable top level function that bootstraps a gyokuro application: 24 | 25 | ```ceylon 26 | import net.gyokuro.core { 27 | Application, 28 | get, 29 | post, 30 | serve 31 | } 32 | 33 | "Run an HTTP server listening on port 8080, that will react to requests on /hello. 34 | Static assets will be served from the `assets` directory." 35 | shared void run() { 36 | 37 | // React to GET/POST requests using a basic handler 38 | get("/hello", void (Request request, Response response) { 39 | response.writeString("Hello yourself!"); 40 | }); 41 | 42 | // Shorter syntax that lets Ceylon infer types and lets gyokuro 43 | // write the response 44 | post("/hello", (request, response) => "You're the POST master!"); 45 | 46 | value app = Application { 47 | assets = serve("assets"); 48 | }; 49 | 50 | app.run(); 51 | } 52 | ``` 53 | 54 | ## Binding parameters 55 | 56 | In addition to basic handlers, gyokuro allows you to bind GET/POST data 57 | directly to function parameters, and return an object that represents your response: 58 | 59 | ```ceylon 60 | shared void run() { 61 | // ... 62 | post("/hello", `postHandler`); 63 | // ... 64 | } 65 | 66 | "Advanced handlers have more flexible parameters, you're 67 | not limited to `Request` and `Response`, you can bind 68 | GET/POST values directly to handler parameters! 69 | The returned value will be written to the response." 70 | String postHandler(Float float, Integer? optionalInt, String who = "world") { 71 | // `float` is required, `optionalInt` is optional and 72 | // `who` will be defaulted to "world" if it's not in POST data. 73 | return "Hello, " + who + "!\n"; 74 | } 75 | ``` 76 | 77 | GET/POST values are mapped by name and automatically converted to the correct type. 78 | Note that optional types and default values are also supported! 79 | 80 | ## Using annotated controllers 81 | 82 | In addition to `get` and `post` functions, gyokuro supports annotated controllers. 83 | Using annotations, you can easily group related handlers in a same controller. 84 | 85 | Let's see how it works on a simple example: 86 | 87 | ```ceylon 88 | shared void run() { 89 | 90 | value app = Application { 91 | // You can use REST-style annotated controllers like this: 92 | controllers = bind(`package gyokuro.demo.rest`, "/rest"); 93 | }; 94 | 95 | app.run(); 96 | } 97 | ``` 98 | 99 | The package `gyokuro.demo.rest` will be scanned for classes annotated with `controller`. 100 | Each function annotated with `route` will be mapped to the corresponding path. For example: 101 | 102 | ```ceylon 103 | import ceylon.net.http.server { 104 | Response 105 | } 106 | import net.gyokuro.core { 107 | controller, 108 | route 109 | } 110 | 111 | route("duck") 112 | controller class SimpleRestController() { 113 | 114 | route("talk") 115 | shared void makeDuckTalk(Response resp) { 116 | resp.writeString("Quack world!"); 117 | } 118 | } 119 | ``` 120 | 121 | Will be mapped to `http://localhost:8080/rest/duck/talk`. 122 | 123 | ## Want to learn more? 124 | 125 | See the [complete documentation](http://bjansen.github.io/gyokuro/doc/0.2/) for more info. 126 | 127 | You can find examples in the [demos directory](https://github.com/bjansen/gyokuro/tree/master/demos/). 128 | 129 | -------------------------------------------------------------------------------- /ceylonb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # resolve links - $0 may be a softlink 4 | PRG="$0" 5 | while [ -h "$PRG" ]; do 6 | ls="$(ls -ld "$PRG")" 7 | link="${ls##*-> }" # remove largest prefix: yields link target (behind ->) 8 | if [ "$link" != "${link#/}" ]; then # remove prefix / if present 9 | # path was absolute 10 | PRG="$link" 11 | else 12 | # was not 13 | PRG="$(dirname "$PRG")/$link" 14 | fi 15 | done 16 | 17 | DIR="$(dirname "$PRG")" 18 | 19 | # Check if we should use a distribution bootstrap 20 | if [ -f "$DIR/.ceylon/bootstrap/ceylon-bootstrap.properties" ] && [ -f "$DIR/.ceylon/bootstrap/ceylon-bootstrap.jar" ]; then 21 | # Using bootstrap 22 | LIB="$DIR/.ceylon/bootstrap" 23 | else 24 | # Normal execution 25 | CEYLON_HOME="$DIR/.." 26 | LIB="$CEYLON_HOME/lib" 27 | 28 | if [ "$1" = "--show-home" ]; then 29 | echo "$CEYLON_HOME" 30 | exit 31 | fi 32 | fi 33 | 34 | if [ -z "$JAVA_HOME" ]; then 35 | JAVA="java" 36 | else 37 | JAVA="$JAVA_HOME/bin/java" 38 | fi 39 | 40 | # Make sure we have java installed 41 | if ! hash java 2>&- 42 | then 43 | echo >&2 "Java not found, you must install Java in order to compile and run Ceylon programs" 44 | echo >&2 "Go to http://www.java.com/getjava/ to download the latest version of Java" 45 | exit 1 46 | fi 47 | 48 | #JAVA_DEBUG_OPTS="-Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=y" 49 | 50 | if [ "$PRESERVE_JAVA_OPTS" != "true" ]; then 51 | PREPEND_JAVA_OPTS="$JAVA_DEBUG_OPTS" 52 | if [ -n "$COLUMNS" ]; then 53 | CEYL_COLS="$COLUMNS" 54 | elif stty size 2>/dev/null >/dev/null; then 55 | CEYL_COLS="$(stty size 2>/dev/null | cut -d' ' -f2)" 56 | else 57 | CEYL_COLS="$(tput 2>/dev/null cols)" 58 | fi 59 | PREPEND_JAVA_OPTS="$PREPEND_JAVA_OPTS -Dcom.redhat.ceylon.common.tool.terminal.width=$CEYL_COLS" 60 | PREPEND_JAVA_OPTS="$PREPEND_JAVA_OPTS -Dcom.redhat.ceylon.common.tool.progname=$(basename "$PRG")" 61 | fi 62 | for arg; do 63 | case $arg in 64 | --java=*) JAVA_OPTS="$JAVA_OPTS ${arg#--java=}";; 65 | [!-]*) break;; 66 | esac 67 | done 68 | JAVA_OPTS="$PREPEND_JAVA_OPTS $JAVA_OPTS" 69 | 70 | BOOTSTRAP="$LIB/ceylon-bootstrap.jar" 71 | 72 | # Check for cygwin, convert bootstrap path to Windows format 73 | case "`uname`" in 74 | CYGWIN*) [ -n "$LIB" ] && BOOTSTRAP=`cygpath -w "$BOOTSTRAP"` 75 | esac 76 | 77 | exec "$JAVA" \ 78 | $JAVA_OPTS \ 79 | -jar "$BOOTSTRAP" \ 80 | "$@" 81 | 82 | -------------------------------------------------------------------------------- /ceylonb.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal ENABLEDELAYEDEXPANSION 3 | 4 | :: Check if we should use a distribution bootstrap 5 | pushd "%~dp0" 6 | set "DIR=%CD%" 7 | popd 8 | if NOT exist "%DIR%\.ceylon\bootstrap\ceylon-bootstrap.properties" ( 9 | goto :normal 10 | ) 11 | if NOT exist "%DIR%\.ceylon\bootstrap\ceylon-bootstrap.jar" ( 12 | goto :normal 13 | ) 14 | 15 | :: Using bootstrap 16 | set "LIB=%DIR%\.ceylon\bootstrap" 17 | 18 | goto :endbs 19 | 20 | :normal 21 | 22 | :: Normal execution 23 | 24 | :: Find CEYLON_HOME 25 | pushd "%~dp0.." 26 | set "CEYLON_HOME=%CD%" 27 | popd 28 | set "LIB=%CEYLON_HOME%\lib" 29 | 30 | if "%~1" == "--show-home" ( 31 | @echo %CEYLON_HOME% 32 | exit /b 1 33 | ) 34 | 35 | :endbs 36 | 37 | :: Find Java 38 | 39 | :: Only check the registry if JAVA_HOME is not already set 40 | IF NOT "%JAVA_HOME%" == "" ( 41 | goto :javaend 42 | ) 43 | 44 | :: Find Java in the registry 45 | set "KEY_NAME=HKLM\SOFTWARE\JavaSoft\Java Runtime Environment" 46 | set "KEY_NAME2=HKLM\SOFTWARE\Wow6432Node\JavaSoft\Java Runtime Environment" 47 | 48 | :: get the current version 49 | FOR /F "usebackq skip=2 tokens=3" %%A IN (`REG QUERY "%KEY_NAME%" /v CurrentVersion 2^>nul`) DO ( 50 | set "ValueValue=%%A" 51 | ) 52 | 53 | if "%ValueValue%" NEQ "" ( 54 | set "JAVA_CURRENT=%KEY_NAME%\%ValueValue%" 55 | ) else ( 56 | rem Try again for 64bit systems 57 | 58 | FOR /F "usebackq skip=2 tokens=3" %%A IN (`REG QUERY "%KEY_NAME2%" /v CurrentVersion 2^>nul`) DO ( 59 | set "JAVA_CURRENT=%KEY_NAME2%\%%A" 60 | ) 61 | ) 62 | 63 | if "%ValueValue%" NEQ "" ( 64 | set "JAVA_CURRENT=%KEY_NAME%\%ValueValue%" 65 | ) else ( 66 | rem Try again for 64bit systems from a 32-bit process 67 | 68 | FOR /F "usebackq skip=2 tokens=3" %%A IN (`REG QUERY "%KEY_NAME%" /v CurrentVersion /reg:64 2^>nul`) DO ( 69 | set "JAVA_CURRENT=%KEY_NAME%\%%A" 70 | ) 71 | ) 72 | 73 | if "%JAVA_CURRENT%" == "" ( 74 | @echo Java not found, you must install Java in order to compile and run Ceylon programs 75 | @echo Go to http://www.java.com/getjava/ to download the latest version of Java 76 | exit /b 1 77 | ) 78 | 79 | :: get the javahome 80 | FOR /F "usebackq skip=2 tokens=3*" %%A IN (`REG QUERY "%JAVA_CURRENT%" /v JavaHome 2^>nul`) DO ( 81 | set "JAVA_HOME=%%A %%B" 82 | ) 83 | 84 | if "%JAVA_HOME%" EQU "" ( 85 | rem Try again for 64bit systems from a 32-bit process 86 | FOR /F "usebackq skip=2 tokens=3*" %%A IN (`REG QUERY "%JAVA_CURRENT%" /v JavaHome /reg:64 2^>nul`) DO ( 87 | set "JAVA_HOME=%%A %%B" 88 | ) 89 | ) 90 | 91 | :javaend 92 | 93 | set "JAVA=%JAVA_HOME%\bin\java.exe" 94 | 95 | :: Check that Java executable actually exists 96 | if not exist "%JAVA%" ( 97 | @echo "Cannot find java.exe at %JAVA%, check that your JAVA_HOME variable is pointing to the right place" 98 | exit /b 1 99 | ) 100 | 101 | rem set JAVA_DEBUG_OPTS="-Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=y" 102 | 103 | if NOT "%PRESERVE_JAVA_OPTS%" == "true" ( 104 | set PREPEND_JAVA_OPTS=%JAVA_DEBUG_OPTS% 105 | rem Other java opts go here 106 | ) 107 | 108 | rem Find any --java options and add their values to JAVA_OPTS 109 | for %%x in (%*) do ( 110 | set ARG=%%~x 111 | if "!ARG:~0,7!" EQU "--java=" ( 112 | set OPT=!ARG:~7! 113 | set "JAVA_OPTS=!JAVA_OPTS! !OPT!" 114 | ) else if "!ARG!" EQU "--java" ( 115 | @echo Error: use --java options with an equal sign and quotes, eg: "--java=-Xmx500m" 116 | exit /b 1 117 | ) else if "!ARG:~0,1!" NEQ "-" ( 118 | goto :breakloop 119 | ) 120 | ) 121 | :breakloop 122 | 123 | set "JAVA_OPTS=%PREPEND_JAVA_OPTS% %JAVA_OPTS%" 124 | 125 | "%JAVA%" ^ 126 | %JAVA_OPTS% ^ 127 | -jar "%LIB%\ceylon-bootstrap.jar" ^ 128 | %* 129 | 130 | endlocal 131 | 132 | if %errorlevel% neq 0 exit /B %errorlevel% 133 | -------------------------------------------------------------------------------- /demos-assets/css/main.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: #222; 3 | } 4 | 5 | body { 6 | background-color: #EEE; 7 | } -------------------------------------------------------------------------------- /demos-assets/gson/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Gson demo

5 | 6 |

Serialization

7 | 17 | 18 | 19 | 20 | 21 | 22 |

Deserialization

23 | 24 | Name:
25 | Salary:
26 | 27 | 28 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demos-assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Coin 4 | 5 | 6 | 7 | 8 | 9 |

It works!

10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /demos-assets/js/main.js: -------------------------------------------------------------------------------- 1 | getDataFromServer = function() { 2 | // Output manually written in Response 3 | sendRequest('/rest/duck/talk', function(xhr) { 4 | document.getElementById('response').innerHTML = 'Server said "' + xhr.responseText + '"'; 5 | }); 6 | 7 | // Object automatically serialized to JSON and written in Response 8 | sendRequest('/rest/duck/actions', function(xhr) { 9 | document.getElementById('response2').innerHTML = 'A duck can ' + xhr.responseText; 10 | }); 11 | } 12 | 13 | sendRequest = function(url, callback) { 14 | var xhr = new XMLHttpRequest(); 15 | xhr.open('GET', url, true); 16 | xhr.onload = function (e) { 17 | if (xhr.readyState === 4) { 18 | if (xhr.status === 200) { 19 | callback(xhr); 20 | } else { 21 | console.error(xhr.statusText); 22 | } 23 | } 24 | }; 25 | 26 | xhr.onerror = function (e) { 27 | console.error(xhr.statusText); 28 | }; 29 | 30 | xhr.send(null); 31 | } -------------------------------------------------------------------------------- /demos-assets/mustache/hello.mustache: -------------------------------------------------------------------------------- 1 | Hello, {{who}}! -------------------------------------------------------------------------------- /demos-assets/rythm/hello.rythm: -------------------------------------------------------------------------------- 1 | Hello, @who! -------------------------------------------------------------------------------- /demos-assets/thymeleaf/hello.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | Thymeleaf demo 8 | 9 | 10 | 11 | 12 | 13 | Hello, John Doe! 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/ceylonhtml/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module gyokuro.demo.ceylonhtml "0.4-SNAPSHOT" { 3 | import net.gyokuro.view.ceylonhtml "0.4-SNAPSHOT"; 4 | } 5 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/ceylonhtml/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package gyokuro.demo.ceylonhtml; 2 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/ceylonhtml/run.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.html { 2 | Html, 3 | Head, 4 | Title, 5 | Body, 6 | Div 7 | } 8 | 9 | import net.gyokuro.core { 10 | render, 11 | Application, 12 | get 13 | } 14 | import net.gyokuro.view.ceylonhtml { 15 | CeylonHtmlRenderer, 16 | HtmlTemplate 17 | } 18 | 19 | "Run the module `gyokuro.demo.ceylonhtml`." 20 | shared void run() { 21 | get("/hello", `hello`); 22 | 23 | Application { 24 | renderer = CeylonHtmlRenderer(); 25 | }.run(); 26 | } 27 | 28 | HtmlTemplate hello() { 29 | value html = Html { 30 | Head { 31 | Title { "Hello world" } 32 | }, 33 | Body { 34 | Div { 35 | clazz = "mycls"; 36 | children = { 37 | "Hello from Ceylon HTML!" 38 | }; 39 | } 40 | } 41 | }; 42 | 43 | return render(html); 44 | } 45 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/gson/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module gyokuro.demo.gson "0.4-SNAPSHOT" { 3 | import net.gyokuro.core "0.4-SNAPSHOT"; 4 | import net.gyokuro.transform.gson "0.4-SNAPSHOT"; 5 | import ceylon.logging "1.3.4-SNAPSHOT"; 6 | } 7 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/gson/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package gyokuro.demo.gson; 2 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/gson/run.ceylon: -------------------------------------------------------------------------------- 1 | import net.gyokuro.core { 2 | get, 3 | Application, 4 | serve, 5 | post 6 | } 7 | import net.gyokuro.transform.gson { 8 | GsonTransformer 9 | } 10 | import ceylon.logging { 11 | defaultPriority, 12 | trace, 13 | writeSimpleLog, 14 | addLogWriter 15 | } 16 | 17 | "Run the module `gyokuro.demo.gson`." 18 | shared void run() { 19 | get("/bob", (req, resp) => Employee("Bob", 75049.4)); 20 | post("/save", `saveEmp`); 21 | 22 | addLogWriter(writeSimpleLog); 23 | defaultPriority = trace; 24 | 25 | Application { 26 | transformers = [GsonTransformer()]; 27 | assets = serve("demos-assets/gson"); 28 | }.run(); 29 | } 30 | 31 | class Employee(name, salary) { 32 | shared String name; 33 | shared Float salary; 34 | } 35 | 36 | String saveEmp(Employee emp) => "Saved ``emp.name`` in DB"; -------------------------------------------------------------------------------- /demos/gyokuro/demo/mustache/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module gyokuro.demo.mustache "0.4-SNAPSHOT" { 3 | import net.gyokuro.core "0.4-SNAPSHOT"; 4 | import net.gyokuro.view.mustache "0.4-SNAPSHOT"; 5 | } 6 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/mustache/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package gyokuro.demo.mustache; 2 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/mustache/run.ceylon: -------------------------------------------------------------------------------- 1 | import net.gyokuro.core { 2 | get, 3 | Application, 4 | Template, 5 | render 6 | } 7 | import net.gyokuro.view.mustache { 8 | MustacheRenderer 9 | } 10 | 11 | "Run the module `gyokuro.demo.mustache`." 12 | shared void run() { 13 | 14 | get("/hello", `hello`); 15 | 16 | Application { 17 | renderer = MustacheRenderer("demos-assets/mustache/", ".mustache"); 18 | }.run(); 19 | } 20 | 21 | Template hello() => render("hello", map {"who" -> "World"}); -------------------------------------------------------------------------------- /demos/gyokuro/demo/report/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module gyokuro.demo.report "0.4-SNAPSHOT" { 3 | import net.gyokuro.report "0.4-SNAPSHOT"; 4 | import gyokuro.demo.rest "1.0.0"; 5 | } 6 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/report/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package gyokuro.demo.report; 2 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/report/run.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.file { 2 | parsePath, 3 | Directory, 4 | Nil 5 | } 6 | 7 | import net.gyokuro.report { 8 | GyokuroApiGenerator 9 | } 10 | "Run the module `gyokuro.demo.report`." 11 | shared void run() { 12 | value output = "modules/reports/gyokuro/"; 13 | value path = 14 | let (p = parsePath(output).resource) 15 | if (is Directory p) then p 16 | else if (is Nil p) then p.createDirectory(true) 17 | else null; 18 | 19 | if (exists path) { 20 | GyokuroApiGenerator(`package gyokuro.demo.rest`, path).run(); 21 | } else { 22 | print("Can't access directory " + output); 23 | } 24 | } -------------------------------------------------------------------------------- /demos/gyokuro/demo/rest/controllers.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Response, 3 | Request 4 | } 5 | 6 | import net.gyokuro.core { 7 | controller, 8 | route, 9 | halt 10 | } 11 | 12 | route("duck") 13 | controller class SimpleRestController() { 14 | 15 | "Make the duck talk!" 16 | route("talk") 17 | shared void makeDuckTalk(Response resp, Request req) { 18 | resp.writeString("Quack world!"); 19 | } 20 | 21 | "Lists all the things a duck can do." 22 | route("actions") 23 | shared String[] listThingsDucksCanDo() { 24 | return ["fly", "quack", "eat", "dive"]; 25 | } 26 | 27 | "Tries to find a duck." 28 | suppressWarnings("expressionTypeNothing") 29 | route("find") 30 | shared String findDuck(Integer id) { 31 | // If we can't find the duck in DB, 32 | // return a 404 response. 33 | return daoFind(id) else halt(404, "Duck not found"); 34 | } 35 | 36 | String? daoFind(Integer id) => null; 37 | } 38 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/rest/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module gyokuro.demo.rest "1.0.0" { 3 | import net.gyokuro.core "0.4-SNAPSHOT"; 4 | import ceylon.logging "1.3.4-SNAPSHOT"; 5 | } 6 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/rest/package.ceylon: -------------------------------------------------------------------------------- 1 | "Demo for annotated controllers." 2 | shared package gyokuro.demo.rest; 3 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/rest/run.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.logging { 2 | addLogWriter, 3 | defaultPriority, 4 | trace, 5 | writeSimpleLog 6 | } 7 | import ceylon.http.server { 8 | Request, 9 | Response 10 | } 11 | 12 | import net.gyokuro.core { 13 | Application, 14 | get, 15 | post, 16 | Template, 17 | render, 18 | serve, 19 | bind, 20 | websocket 21 | } 22 | import net.gyokuro.view.api { 23 | TemplateRenderer 24 | } 25 | 26 | shared void run() { 27 | 28 | addLogWriter(writeSimpleLog); 29 | defaultPriority = trace; 30 | 31 | // React to GET/POST requests using a basic handler 32 | get("/hello", void(Request request, Response response) { 33 | response.writeString("Hello yourself!"); 34 | }); 35 | 36 | // You can also use more advanced handlers 37 | post("/hello", `postHandler`); 38 | 39 | // And render templates 40 | get("/render", `renderingHandler`); 41 | 42 | // WebSockets are also supported 43 | websocket("/ws", (channel, text) => channel.sendText("Hello, ``text``!")); 44 | 45 | websocket("/chat", (channel, text) { 46 | for (peer in channel.peerConnections) { 47 | peer.sendText(text); 48 | } 49 | }); 50 | 51 | value app = Application { 52 | // You can also use annotated controllers, if you're 53 | // a nostalgic Java developer ;-) 54 | controllers = bind(`package gyokuro.demo.rest`, "/rest"); 55 | 56 | // And serve static assets 57 | assets = serve("demos-assets"); 58 | 59 | // You can use any template engine you want 60 | renderer = object satisfies TemplateRenderer<> { 61 | 62 | // this is a dummy template renderer 63 | shared actual String render(String templateName, Map context, 64 | Request req, Response resp) { 65 | 66 | variable value result = templateName; 67 | for (key->val in context) { 68 | if (exists val) { 69 | result = result.replace(key, val.string); 70 | } 71 | } 72 | return result; 73 | } 74 | }; 75 | }; 76 | 77 | // By default, the server will be started on 0.0.0.0:8080 78 | app.run(); 79 | } 80 | 81 | "Advanced handlers have more flexible parameters, you're 82 | not limited to `Request` and `Response`, you can bind 83 | GET/POST values directly to handler parameters!" 84 | String postHandler(String who = "world") { 85 | // `who` will get its value from POST data, and will 86 | // be defaulted to "world". 87 | return "Hello, " + who + "!\n"; 88 | } 89 | 90 | Template renderingHandler() { 91 | return render("foobar", map { "bar"->"baz" }); 92 | } 93 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/rythm/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module gyokuro.demo.rythm "0.4-SNAPSHOT" { 3 | import net.gyokuro.core "0.4-SNAPSHOT"; 4 | import net.gyokuro.view.rythm "0.4-SNAPSHOT"; 5 | } 6 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/rythm/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package gyokuro.demo.rythm; 2 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/rythm/run.ceylon: -------------------------------------------------------------------------------- 1 | import net.gyokuro.core { 2 | render, 3 | Application, 4 | Template, 5 | get 6 | } 7 | import net.gyokuro.view.rythm { 8 | RythmRenderer 9 | } 10 | 11 | "Run the module `gyokuro.demo.rythm`." 12 | shared void run() { 13 | 14 | get("/hello", `hello`); 15 | 16 | Application { 17 | renderer = RythmRenderer("demos-assets/rythm/", ".rythm"); 18 | }.run(); 19 | } 20 | 21 | Template hello() => render("hello", map { "who"->"World" }); 22 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/spring/module.ceylon: -------------------------------------------------------------------------------- 1 | "Shows how to inject Spring beans in gyokuro controllers." 2 | native ("jvm") 3 | module gyokuro.demo.spring "0.4-SNAPSHOT" { 4 | import ceylon.logging "1.3.4-SNAPSHOT"; 5 | 6 | import net.gyokuro.core "0.4-SNAPSHOT"; 7 | 8 | import maven:"org.springframework:spring-core" "4.3.5.RELEASE"; 9 | import maven:"org.springframework:spring-beans" "4.3.5.RELEASE"; 10 | import maven:"org.springframework:spring-context" "4.3.5.RELEASE"; 11 | import maven:"commons-logging:commons-logging" "1.2"; 12 | } 13 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/spring/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package gyokuro.demo.spring; 2 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/spring/run.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Response 3 | } 4 | import ceylon.logging { 5 | trace, 6 | addLogWriter, 7 | writeSimpleLog, 8 | defaultPriority 9 | } 10 | 11 | import java.lang { 12 | Types 13 | } 14 | 15 | import net.gyokuro.core { 16 | controller, 17 | route, 18 | Application, 19 | bind, 20 | ControllerAnnotation 21 | } 22 | 23 | import org.springframework.beans.factory.annotation { 24 | autowired 25 | } 26 | import org.springframework.context.annotation { 27 | AnnotationConfigApplicationContext 28 | } 29 | import org.springframework.stereotype { 30 | component 31 | } 32 | 33 | shared void run() { 34 | addLogWriter(writeSimpleLog); 35 | defaultPriority = trace; 36 | 37 | print("Scanning current package for Spring-annotated classes"); 38 | value springContext = AnnotationConfigApplicationContext(`package`.qualifiedName); 39 | 40 | print("Starting gyokuro application"); 41 | 42 | value controllerAnnotation = Types.classForAnnotationType(); 43 | value controllers = [*springContext.getBeansWithAnnotation(controllerAnnotation).values()]; 44 | 45 | Application { 46 | // We provide our own controller instances instead of letting gyokuro scan a package 47 | controllers = bind(controllers); 48 | }.run(); 49 | } 50 | 51 | "A gyokuro [[controller]] that will be instantiated by Spring." 52 | component controller class MyController() { 53 | 54 | "Could also be injected in the parameter list: 55 | 56 | class MyController(autowired IService service) 57 | " 58 | late autowired IService service; 59 | 60 | route ("/hello") 61 | shared void hello(Response resp, String who = "world") { 62 | resp.writeString(service.greet(who)); 63 | } 64 | } 65 | 66 | interface IService { 67 | shared formal String greet(String who) ; 68 | } 69 | 70 | "A Spring bean defining a simple service." 71 | component class Service() satisfies IService { 72 | greet(String who) => "Hello, ``who`` from a Spring service!"; 73 | } 74 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/thymeleaf/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module gyokuro.demo.thymeleaf "0.4-SNAPSHOT" { 3 | import net.gyokuro.core "0.4-SNAPSHOT"; 4 | import net.gyokuro.view.thymeleaf "0.4-SNAPSHOT"; 5 | import ceylon.logging "1.3.4-SNAPSHOT"; 6 | } 7 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/thymeleaf/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package gyokuro.demo.thymeleaf; 2 | -------------------------------------------------------------------------------- /demos/gyokuro/demo/thymeleaf/run.ceylon: -------------------------------------------------------------------------------- 1 | import net.gyokuro.core { 2 | render, 3 | Application, 4 | Template, 5 | get 6 | } 7 | import net.gyokuro.view.thymeleaf { 8 | ThymeleafRenderer 9 | } 10 | import ceylon.logging { 11 | defaultPriority, 12 | debug, 13 | addLogWriter, 14 | writeSimpleLog 15 | } 16 | 17 | "Run the module `gyokuro.demo.thymeleaf`." 18 | shared void run() { 19 | defaultPriority = debug; 20 | addLogWriter(writeSimpleLog); 21 | 22 | get("/hello", `hello`); 23 | 24 | Application { 25 | renderer = ThymeleafRenderer("demos-assets/thymeleaf/", ".xhtml"); 26 | }.run(); 27 | } 28 | 29 | Template hello() => render("hello", map { "who"->"World" }); 30 | -------------------------------------------------------------------------------- /gyokuro.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /overrides.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resource/com/github/bjansen/gyokuro/report/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #EEE; 3 | width: 1024px; 4 | margin: auto; 5 | font-family: Verdana, Geneva, Tahoma, sans-serif; 6 | font-size: 0.9em; 7 | } 8 | 9 | .route-header { 10 | display: flex; 11 | cursor: pointer; 12 | } 13 | 14 | .route-header > :first-child { 15 | border-radius: 2px; 16 | padding: 5px; 17 | color: #EEE; 18 | min-width: 45px; 19 | text-align: center; 20 | } 21 | 22 | .route-header > :nth-child(2) { 23 | padding: 5px 15px; 24 | } 25 | 26 | .route-header > :nth-child(3) { 27 | padding: 5px 10px; 28 | text-align: right; 29 | flex: 1; 30 | } 31 | 32 | div[class^="method-"] { 33 | margin-bottom: 20px; 34 | } 35 | 36 | /* GET */ 37 | .method-get { 38 | background-color: #e7f6ec; 39 | border: 1px solid #c3e8d1; 40 | } 41 | .method-get .route-params { 42 | border-top: 1px solid #c3e8d1; 43 | } 44 | 45 | .method-get .route-header > :first-child { 46 | background-color: #10a54a; 47 | } 48 | 49 | .method-get .route-header > :nth-child(3) { 50 | color: #10a54a; 51 | } 52 | 53 | /* POST */ 54 | .method-post { 55 | background-color: #e7f0f7; 56 | border: 1px solid #c3d9ec; 57 | } 58 | 59 | .method-post .route-params { 60 | border-top: 1px solid #c3d9ec; 61 | } 62 | 63 | .method-post .route-header > :first-child { 64 | background-color: #0f6ab4; 65 | } 66 | 67 | .method-post .route-header > :nth-child(3) { 68 | color: #0f6ab4; 69 | } 70 | 71 | .collapsed { 72 | display: none; 73 | } 74 | 75 | /* Parameters */ 76 | .route-params { 77 | border-top: 1px solid rgba(0, 0, 0, 0.5); 78 | padding: 10px; 79 | } 80 | .route-params h2 { 81 | font-size: 1.1em; 82 | } 83 | 84 | .route-params table { 85 | width: 100%; 86 | border-collapse:collapse; 87 | text-align: left; 88 | } 89 | 90 | .route-params table th { 91 | color: #666; 92 | font-weight: normal; 93 | border-bottom: 1px solid #CCC; 94 | } 95 | 96 | .route-params table td { 97 | padding: 2px 5px; 98 | } -------------------------------------------------------------------------------- /source/net/gyokuro/core/Flash.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Session 3 | } 4 | 5 | import net.gyokuro.core.internal { 6 | DefaultFlash 7 | } 8 | 9 | "A holder for special messages stored in the session, 10 | meant to be used exactly once. Flash messages are removed 11 | from the session as soon as they are accessed." 12 | shared interface Flash { 13 | "Adds a flash object to the session." 14 | shared formal void add(String key, Object val); 15 | 16 | "Gets a flash object if it exists, and removes it 17 | immediately from the session." 18 | shared formal Object? get(String key); 19 | 20 | "Gets a flash object if it exists, without removing 21 | it from the session." 22 | shared formal Object? peek(String key); 23 | } 24 | 25 | "Creates a new instance of a [[Flash]]. You shouldn't have 26 | to use this function directly unless you are creating a custom 27 | [[net.gyokuro.view.api::TemplateRenderer]]. 28 | If you want to access a `Flash` instance from a handler, 29 | use parameters injection instead: 30 | 31 | route(\"/login\") 32 | shared void login(Flash flash) { 33 | if (loginOk()) { 34 | flash.add(\"info\", \"You have been logged in\"); 35 | redirect(\"/\"); 36 | } 37 | } 38 | " 39 | shared Flash newFlash(Session session) 40 | => DefaultFlash(session); 41 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/annotations.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.language.meta.declaration { 2 | FunctionDeclaration, 3 | ClassDeclaration, 4 | ValueDeclaration 5 | } 6 | import ceylon.http.common { 7 | AbstractMethod, 8 | get, 9 | post 10 | } 11 | 12 | "Declares a partial path associated to a class or a function. 13 | Routes declared on a class will be concatenated with its member 14 | routes. 15 | For example, the following code will result in a route `/foo/bar`: 16 | 17 | route(\"foo\") 18 | controller class MyController() { 19 | route(\"bar\") 20 | void hello() { } 21 | } 22 | " 23 | shared annotation RouteAnnotation route(String path, 24 | {AbstractMethod+} methods = { get, post }) => RouteAnnotation(path, methods); 25 | 26 | "The annotation class for the [[route]] annotation." 27 | shared final annotation class RouteAnnotation(path, methods) 28 | satisfies OptionalAnnotation { 29 | 30 | shared String path; 31 | shared {AbstractMethod+} methods; 32 | } 33 | 34 | "Declares a class or an object as a controller, allowing routes to be scanned." 35 | see(`function route`) 36 | shared annotation ControllerAnnotation controller() => ControllerAnnotation(); 37 | 38 | "The annotation class for the [[controller]] annotation." 39 | shared final annotation class ControllerAnnotation() 40 | satisfies OptionalAnnotation { 41 | } 42 | 43 | "Declares that a handler parameter should be retrieved from the current 44 | HTTP session instead of GET/POST data. If no value can be retrieved from 45 | the session, a 400 response will be sent back to the client." 46 | shared annotation SessionAnnotation session() => SessionAnnotation(); 47 | 48 | "The annotation class for the [[session]] annotation." 49 | shared final annotation class SessionAnnotation() 50 | satisfies OptionalAnnotation { 51 | } 52 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/application.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | ArrayList 3 | } 4 | import ceylon.http.common { 5 | post, 6 | get, 7 | Method 8 | } 9 | import ceylon.http.server { 10 | Options, 11 | Request, 12 | newServer, 13 | Response, 14 | Server, 15 | startsWith, 16 | AsynchronousEndpoint, 17 | HttpEndpoint, 18 | Status 19 | } 20 | import ceylon.http.server.endpoints { 21 | serveStaticFile, 22 | RepositoryEndpoint 23 | } 24 | import ceylon.http.server.websocket { 25 | WebSocketEndpoint 26 | } 27 | import ceylon.io { 28 | SocketAddress 29 | } 30 | import ceylon.language.meta.declaration { 31 | Package 32 | } 33 | 34 | import net.gyokuro.core.internal { 35 | RequestDispatcher, 36 | router 37 | } 38 | import net.gyokuro.transform.api { 39 | Transformer 40 | } 41 | import net.gyokuro.view.api { 42 | TemplateRenderer 43 | } 44 | 45 | "A web server application that can route requests to handler functions 46 | or annotated controllers, and serve static assets." 47 | shared class Application( 48 | "The address or hostname on which the HTTP server will be bound." 49 | shared String address = "0.0.0.0", 50 | "The port on which the server will listen." 51 | shared Integer port = 8080, 52 | "Additional controllers in which [[route]]s will be scanned, that will be 53 | associated to the given context root. 54 | 55 | If a package is provided, gyokuro will look for classes and objects annotated 56 | with the [[controller]] annotation and instantiate them automatically. 57 | 58 | If a stream of [[Object]]s is provided, gyokuro will look for existing instances 59 | annotated with [[controller]]. 60 | 61 | See also the [[bind]] function." 62 | [String, Package|{Object*}]? controllers = null, 63 | "A tuple [filesystem folder, context root] used to serve static assets. 64 | See the [[serve]] function." 65 | [String, String]? assets = null, 66 | "A context root used to serve modules." 67 | String? modulesPath = null, 68 | "Additional (chained) filters run before each request." 69 | Filter[] filters = [], 70 | "A template renderer" 71 | TemplateRenderer? renderer = null, 72 | "Transformers that can serialize to responses and deserialize from request bodies." 73 | Transformer[] transformers = []) { 74 | 75 | variable Server? server = null; 76 | variable Boolean stopped = false; 77 | 78 | "A filter applied to each incoming request before it is dispatched to 79 | its matching handler. Multiple filters can be chained, and returning 80 | [[false]] will stop the chain. In this case, the filter returning `false` 81 | should modify the [[Response]] such as it can be returned to the client." 82 | shared alias Filter => Anything(Request, Response, Anything(Request, Response)); 83 | 84 | "Starts the web application." 85 | shared void run(Anything(Status) statusListener = noop) { 86 | if (stopped) { 87 | return; 88 | } 89 | value endpoints = ArrayList(); 90 | 91 | endpoints.add(RequestDispatcher(controllers, filter, renderer, transformers).endpoint()); 92 | if (exists modulesPath) { 93 | endpoints.add(RepositoryEndpoint(modulesPath)); 94 | } 95 | 96 | if (exists assets) { 97 | value assetsEndpoint = AsynchronousEndpoint(startsWith(assets[1]), 98 | serveRoot(assets), 99 | { get, post, special }); 100 | 101 | endpoints.add(assetsEndpoint); 102 | } 103 | 104 | for (path -> handler in router.webSocketHandlers) { 105 | WebSocketHandler wsHandler; 106 | 107 | if (is WebSocketHandler handler) { 108 | wsHandler = handler; 109 | } else { 110 | wsHandler = object extends WebSocketHandler() { 111 | onText = handler; 112 | }; 113 | } 114 | 115 | endpoints.add(WebSocketEndpoint { 116 | path = startsWith(path); 117 | onOpen = wsHandler.onOpen; 118 | onClose = wsHandler.onClose; 119 | onError = wsHandler.onError; 120 | onText = wsHandler.onText; 121 | onBinary = wsHandler.onBinary; 122 | }); 123 | } 124 | 125 | value s = server = newServer(endpoints); 126 | s.addListener(statusListener); 127 | s.start(SocketAddress(address, port), Options()); 128 | server = null; 129 | } 130 | 131 | "Stops the web application, if started, and inhibits any further attempts to start it." 132 | shared void stop() { 133 | stopped = true; 134 | if (exists s = server) { 135 | s.stop(); 136 | } 137 | } 138 | 139 | object special satisfies Method { 140 | string => "BREW"; 141 | hash => string.hash; 142 | shared actual Boolean equals(Object that) { 143 | if (is Method that) { 144 | return that.string == string; 145 | } 146 | return false; 147 | } 148 | } 149 | 150 | "Runs the first element in the filter chain. Each filter has the responsibility to 151 | run the next filter in the chain." 152 | void filter(Request req, Response resp, Anything(Request, Response) last) { 153 | void lastFilter(Request req, Response resp, Anything(Request, Response) next) { 154 | last(req, resp); 155 | } 156 | 157 | void next(Filter[] filters, Request req, Response resp) { 158 | if (exists filter = filters.first) { 159 | filter(req, resp, (newReq, newResp) { 160 | next(filters.rest, newReq, newResp); 161 | }); 162 | } 163 | } 164 | 165 | value ourFilters = filters.withTrailing(lastFilter); 166 | next(ourFilters, req, resp); 167 | } 168 | 169 | void serveRoot([String, String] conf)(Request req, Response resp, void complete()) { 170 | filter(req, resp, (req, resp) { 171 | if (req.method == special) { 172 | resp.status = 418; 173 | resp.writeString("418 - I'm a teapot"); 174 | } else { 175 | value root = conf[1]; 176 | value assetsPath = conf[0]; 177 | value file = root.empty then req.path else req.path[root.size...]; 178 | serveStaticFile(assetsPath, (req) => req.path.equals("/") then "/index.html" else file)(req, resp, complete); 179 | } 180 | }); 181 | } 182 | } 183 | 184 | "Tells gyokuro to bind [[controllers|controller]] scanned in [[pkgToScan]] to the given 185 | [[context]] root. This function is meant to be used for [[Application.controllers]]. 186 | 187 | value app = Application { 188 | controllers = bind(\"rest\", `package com.myapp.controllers`); 189 | }; 190 | 191 | " 192 | shared [String, Package|{Object*}] bind(Package|{Object*} pkgToScan, String context = "/") { 193 | return [context, pkgToScan]; 194 | } 195 | 196 | "Tells gyokuro to serve static assets located in the filesystem folder [[path]] under the 197 | given [[context]] root. For example, all routes starting with `/public` will serve files 198 | located in `./assets`: 199 | 200 | value app = Application { 201 | assets = serve(\"assets\", \"/public\"); 202 | }; 203 | " 204 | shared [String, String] serve(String path, String context = "") { 205 | return [path, context]; 206 | } 207 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/functions.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.buffer.charset { 2 | Charset, 3 | utf8 4 | } 5 | import ceylon.http.common { 6 | getMethod=get, 7 | postMethod=post, 8 | optionsMethod=options, 9 | deleteMethod=delete, 10 | connectMethod=connect, 11 | traceMethod=trace, 12 | putMethod=put, 13 | headMethod=head, 14 | sdkContentType=contentType 15 | } 16 | import ceylon.http.server { 17 | Request, 18 | Response 19 | } 20 | import ceylon.language.meta.model { 21 | Function 22 | } 23 | 24 | import net.gyokuro.core.internal { 25 | router, 26 | HaltException, 27 | RedirectException 28 | } 29 | import net.gyokuro.core.http { 30 | patchMethod=patch 31 | } 32 | import net.gyokuro.view.api { 33 | TemplateRenderer 34 | } 35 | 36 | "A function capable of handling a request." 37 | shared alias Handler => Function|Callable; 38 | 39 | "Declares a new GET route for the given [[path]] and [[handler]]." 40 | shared void get(String path, Handler handler) 41 | given Params satisfies Anything[] 42 | => router.registerRoute(path, { getMethod }, handler); 43 | 44 | "Declares a new POST route for the given [[path]] and [[handler]]." 45 | shared void post(String path, Handler handler) 46 | given Params satisfies Anything[] 47 | => router.registerRoute(path, { postMethod }, handler); 48 | 49 | "Declares a new OPTIONS route for the given [[path]] and [[handler]]." 50 | shared void options(String path, Handler handler) 51 | given Params satisfies Anything[] 52 | => router.registerRoute(path, { optionsMethod }, handler); 53 | 54 | "Declares a new DELET route for the given [[path]] and [[handler]]." 55 | shared void delete(String path, Handler handler) 56 | given Params satisfies Anything[] 57 | => router.registerRoute(path, { deleteMethod }, handler); 58 | 59 | "Declares a new CONNECT route for the given [[path]] and [[handler]]." 60 | shared void connect(String path, Handler handler) 61 | given Params satisfies Anything[] 62 | => router.registerRoute(path, { connectMethod }, handler); 63 | 64 | "Declares a new TRACE route for the given [[path]] and [[handler]]." 65 | shared void trace(String path, Handler handler) 66 | given Params satisfies Anything[] 67 | => router.registerRoute(path, { traceMethod }, handler); 68 | 69 | "Declares a new PUT route for the given [[path]] and [[handler]]." 70 | shared void put(String path, Handler handler) 71 | given Params satisfies Anything[] 72 | => router.registerRoute(path, { putMethod }, handler); 73 | 74 | "Declares a new HEAD route for the given [[path]] and [[handler]]." 75 | shared void head(String path, Handler handler) 76 | given Params satisfies Anything[] 77 | => router.registerRoute(path, { headMethod }, handler); 78 | 79 | "Declares a new PATCH route for the given [[path]] and [[handler]]." 80 | shared void patch(String path, Handler handler) 81 | given Params satisfies Anything[] 82 | => router.registerRoute(path, { patchMethod }, handler); 83 | 84 | "Interrupts the current handler immediately, resulting in an HTTP 85 | response with code [[errorCode]] and a body equal to [[message]]. 86 | 87 | This can be used for example to indicate that something was 88 | not found in the database: 89 | 90 | shared void findAuthor(Integer authorId) { 91 | value author = authorDao.findById(authorId) 92 | else halt(404, \"Author not found\"); 93 | } 94 | " 95 | shared Nothing halt(Integer errorCode, String? message = null) { 96 | throw HaltException(errorCode, message); 97 | } 98 | 99 | "Interrupts the current handler immediately, and asks the client 100 | browser to redirect to the specified [[url]]. 101 | 102 | shared void login(String username, String password) { 103 | if (exists user = ...) { 104 | session.put(\"user\", user); 105 | redirect(\"/\"); 106 | } 107 | ... 108 | } 109 | " 110 | shared Nothing redirect(String url, Integer redirectCode = 303) { 111 | throw RedirectException(url, redirectCode); 112 | } 113 | 114 | "A template that can be called by a [[TemplateRenderer]]." 115 | shared alias AnyTemplate => Anything(TemplateRenderer, Request, Response); 116 | shared alias Template => AnyTemplate; 117 | 118 | "Renders a template that will be returned as the response body." 119 | shared void render( 120 | "The template name" 121 | T template, 122 | "A map of things to pass to the template." 123 | Map context = emptyMap, 124 | "The content type to be used in the response." 125 | String contentType = "text/html", 126 | "The charset to be used in the response." 127 | Charset charset = utf8) 128 | (TemplateRenderer renderer, Request request, Response response) { 129 | 130 | value result = renderer.render(template, context, request, response); 131 | 132 | response.addHeader(sdkContentType(contentType, charset)); 133 | response.writeString(result); 134 | } 135 | 136 | "Clears every registered route." 137 | shared void clearRoutes() { 138 | router.clear(); 139 | } 140 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/http/methods.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.common { 2 | Method 3 | } 4 | 5 | "Workaround until the SDK contains it." 6 | shared object patch satisfies Method { 7 | string => "PATCH"; 8 | hash => string.hash; 9 | equals(Object that) => 10 | if (is Method that) 11 | then that.string == this.string 12 | else false; 13 | } 14 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/http/package.ceylon: -------------------------------------------------------------------------------- 1 | "Default documentation for package `net.gyokuro.core.http`." 2 | shared package net.gyokuro.core.http; 3 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/internal/AnnotationScanner.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.common { 2 | AbstractMethod 3 | } 4 | import ceylon.language.meta { 5 | annotations, 6 | classDeclaration 7 | } 8 | import ceylon.language.meta.declaration { 9 | FunctionDeclaration, 10 | Package, 11 | ClassDeclaration, 12 | ValueDeclaration 13 | } 14 | import ceylon.logging { 15 | logger 16 | } 17 | 18 | import net.gyokuro.core { 19 | ControllerAnnotation, 20 | RouteAnnotation 21 | } 22 | 23 | shared object annotationScanner { 24 | 25 | value log = logger(`module`); 26 | 27 | shared alias Consumer => Anything(String, [Object, FunctionDeclaration], {AbstractMethod+}); 28 | 29 | "Looks for controller definitions in the given [[controllers]]. 30 | Scanned controllers and routes will be registered in the [[router]] 31 | for GET and POST methods." 32 | shared void scanControllers(String contextRoot, Package|{Object*} controllers, 33 | Consumer consumer = router.registerControllerRoute) { 34 | 35 | Anything>[] members; 36 | 37 | if (is Package controllers) { 38 | members = [ for (member in controllers.members()) 39 | member -> null]; 40 | log.trace("Scanning members in package ``controllers.name``"); 41 | } else { 42 | members = [ for (o in controllers) 43 | classDeclaration(o) -> o ]; 44 | log.trace("Scanning members in existing instances"); 45 | } 46 | 47 | for (member -> possibleInstance in members) { 48 | if (exists controller = annotations(`ControllerAnnotation`, member)) { 49 | log.trace("Scanning member ``member.name``"); 50 | 51 | String controllerRoute; 52 | 53 | if (exists route = annotations(`RouteAnnotation`, member)) { 54 | controllerRoute = buildPath(contextRoot, route.path); 55 | } else { 56 | controllerRoute = contextRoot; 57 | } 58 | 59 | value classDecl = if (is ClassDeclaration member) 60 | then member 61 | else member.objectClass; 62 | 63 | if (!exists classDecl) { 64 | log.warn("Skipped non-object value ``member.qualifiedName``"); 65 | continue; 66 | } 67 | 68 | Object instance; 69 | if (exists possibleInstance) { 70 | instance = possibleInstance; 71 | } else if (is ClassDeclaration member) { 72 | instance = member.classApply()(); 73 | } 74 | else { 75 | assert(is Object val = member.get()); 76 | instance = val; 77 | } 78 | value functions = classDecl.memberDeclarations(); 79 | 80 | for (func in functions) { 81 | if (exists route = annotations(`RouteAnnotation`, func)) { 82 | value functionRoute = buildPath(controllerRoute, route.path); 83 | 84 | log.trace("Binding function ``func.name`` to path ``functionRoute``"); 85 | consumer(functionRoute, [instance, func], route.methods); 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | String buildPath(String prefix, String suffix) { 93 | value stripped = suffix.startsWith("/") 94 | then suffix.spanFrom(1) else suffix; 95 | 96 | if (prefix.endsWith("/")) { 97 | return prefix + stripped; 98 | } 99 | 100 | return prefix + "/" + stripped; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/internal/DefaultFlash.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Session 3 | } 4 | 5 | import net.gyokuro.core { 6 | Flash 7 | } 8 | 9 | shared class DefaultFlash(Session session) satisfies Flash { 10 | 11 | shared actual void add(String key, Object val) { 12 | session.put("__flash__" + key, val); 13 | } 14 | 15 | shared actual Object? get(String key) { 16 | if (exists obj = session.get("__flash__" + key)) { 17 | session.remove("__flash__" + key); 18 | return obj; 19 | } 20 | 21 | return null; 22 | } 23 | 24 | shared actual Object? peek(String key) 25 | => session.get("__flash__" + key); 26 | } 27 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/internal/RequestWrapper.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Request 3 | } 4 | 5 | import java.lang { 6 | JString=String, 7 | ObjectArray 8 | } 9 | import java.lang.reflect { 10 | InvocationHandler, 11 | Method 12 | } 13 | 14 | "Allows adding things to a request, like values for `:named` parts of the URL." 15 | class RequestWrapper(Request req, Map namedParams) 16 | satisfies InvocationHandler { 17 | 18 | shared actual Object? invoke(Object proxy, Method method, ObjectArray? args) { 19 | if (method.name == "queryParameter", exists args, args.size == 1, is JString arg = args[0]) { 20 | return queryParameter(req, arg.string); 21 | } 22 | if (method.name == "queryParameters", exists args, args.size == 1, is JString arg = args[0]) { 23 | return queryParameters(req, arg.string); 24 | } 25 | 26 | return if (exists args) 27 | then method.invoke(req, *args) 28 | else method.invoke(req); 29 | } 30 | 31 | shared String? queryParameter(Request req, String name) { 32 | if (namedParams.defines(name)) { 33 | return namedParams.get(name); 34 | } 35 | return req.queryParameter(name); 36 | } 37 | 38 | shared String[] queryParameters(Request req, String name) { 39 | if (namedParams.defines(name)) { 40 | assert (exists val = namedParams.get(name)); 41 | return [val]; 42 | } 43 | return req.queryParameters(name); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/internal/converters.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | ArrayList 3 | } 4 | import ceylon.language.meta { 5 | type 6 | } 7 | import ceylon.language.meta.declaration { 8 | OpenType, 9 | OpenClassOrInterfaceType 10 | } 11 | import ceylon.language.meta.model { 12 | ClassOrInterface 13 | } 14 | import ceylon.http.server { 15 | UploadedFile 16 | } 17 | 18 | interface Converter { 19 | shared formal Boolean supports(OpenType type); 20 | shared formal Anything convert(OpenType type, Type str); 21 | } 22 | 23 | interface MultiConverter satisfies Converter { 24 | } 25 | 26 | object primitiveTypesConverter satisfies Converter<> { 27 | 28 | value supportedTypes = [`class String`, `class Integer`, `class Float`, `class Boolean`].map((cls) => cls.openType); 29 | 30 | shared actual Anything convert(OpenType type, String str) { 31 | if (type == `class Integer`.openType) { 32 | return Integer.parse(str); 33 | } else if (type == `class String`.openType) { 34 | return str; 35 | } else if (type == `class Float`.openType) { 36 | return Float.parse(str); 37 | } else if (type == `class Boolean`.openType) { 38 | if (str == "0") { return false; } 39 | if (str == "1") { return true; } 40 | 41 | return Boolean.parse(str); 42 | } 43 | 44 | return null; 45 | } 46 | 47 | shared actual Boolean supports(OpenType type) => supportedTypes.contains(type); 48 | } 49 | 50 | object listsConverter satisfies MultiConverter { 51 | 52 | value supportedTypes = [`List<>`, `Sequential<>`, `Sequence<>`] 53 | .map((_) => _.declaration.qualifiedName); 54 | 55 | shared actual Anything convert(OpenType t, Object[] values) { 56 | if (exists typeArg = getTypeArgument(t)) { 57 | assert (is OpenClassOrInterfaceType t); 58 | 59 | if (!primitiveTypesConverter.supports(typeArg) 60 | && typeArg!=`UploadedFile`.declaration.openType) { 61 | throw BindingException("Only lists of primitive types or 62 | UploadedFile are supported"); 63 | } 64 | value closedTypeArg = typeArg.declaration.apply(); 65 | value tName = t.declaration.qualifiedName; 66 | 67 | if (values.empty) { 68 | if (tName == `Sequence<>`.declaration.qualifiedName) { 69 | throw BindingException("Cannot bind empty array to nonempty sequence"); 70 | } 71 | return empty; 72 | } else if (tName == `List<>`.declaration.qualifiedName) { 73 | return convertList(closedTypeArg, values, typeArg); 74 | } else { 75 | return convertSequence(closedTypeArg, values, typeArg); 76 | } 77 | } 78 | 79 | return null; 80 | } 81 | 82 | shared actual Boolean supports(OpenType type) { 83 | if (is OpenClassOrInterfaceType type, 84 | supportedTypes.contains(type.declaration.qualifiedName)) { 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | shared OpenClassOrInterfaceType? getTypeArgument(OpenType t) { 91 | if (is OpenClassOrInterfaceType t, 92 | supportedTypes.contains(t.declaration.qualifiedName), 93 | is OpenClassOrInterfaceType typeArg = t.typeArgumentList.first) { 94 | 95 | return typeArg; 96 | } 97 | 98 | return null; 99 | } 100 | 101 | Anything convertList(ClassOrInterface closedTypeArg, Object[] values, OpenClassOrInterfaceType typeArg) { 102 | value list = `class ArrayList`.instantiate([closedTypeArg]); 103 | for (val in values) { 104 | value converted = switch (val) 105 | case (is String) primitiveTypesConverter.convert(typeArg, val) 106 | case (is UploadedFile) val 107 | else null; 108 | 109 | if (exists converted) { 110 | `function ArrayList.add` 111 | .memberApply<>(type(list)) 112 | .bind(list).apply(converted); 113 | } 114 | } 115 | return list; 116 | } 117 | 118 | Anything convertSequence(ClassOrInterface closedTypeArg, Object[] values, OpenClassOrInterfaceType typeArg) { 119 | if (is Object list = convertList(closedTypeArg, values, typeArg)) { 120 | return `function ArrayList.sequence` 121 | .memberApply<>(type(list)) 122 | .bind(list).apply(); 123 | } 124 | return null; 125 | } 126 | } 127 | 128 | // TODO convert to a bean using reflection 129 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/internal/dispatcher.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.buffer.charset { 2 | utf8 3 | } 4 | import ceylon.collection { 5 | HashMap 6 | } 7 | import ceylon.http.common { 8 | post, 9 | get, 10 | contentType, 11 | Header, 12 | options, 13 | head, 14 | put, 15 | delete, 16 | trace, 17 | connect 18 | } 19 | import ceylon.http.server { 20 | Response, 21 | Request, 22 | Endpoint, 23 | Matcher, 24 | UploadedFile 25 | } 26 | import ceylon.interop.java { 27 | createJavaObjectArray 28 | } 29 | import ceylon.language.meta { 30 | classDeclaration, 31 | type 32 | } 33 | import ceylon.language.meta.declaration { 34 | FunctionDeclaration, 35 | Package, 36 | ValueDeclaration, 37 | FunctionOrValueDeclaration, 38 | OpenUnion, 39 | OpenType, 40 | OpenClassType 41 | } 42 | import ceylon.language.meta.model { 43 | ClassModel 44 | } 45 | import ceylon.logging { 46 | logger 47 | } 48 | 49 | import java.lang { 50 | Types 51 | } 52 | import java.lang.reflect { 53 | Proxy 54 | } 55 | 56 | import net.gyokuro.core { 57 | SessionAnnotation, 58 | Flash, 59 | mimeParse, 60 | AnyTemplate 61 | } 62 | import net.gyokuro.core.http { 63 | patch 64 | } 65 | import net.gyokuro.transform.api { 66 | Transformer 67 | } 68 | import net.gyokuro.view.api { 69 | TemplateRenderer 70 | } 71 | 72 | shared class RequestDispatcher([String, Package|{Object*}]? packageToScan, 73 | Anything(Request, Response, Anything(Request, Response)) filter, 74 | TemplateRenderer? renderer = null, Transformer[] transformers = []) { 75 | 76 | value log = logger(`module`); 77 | 78 | Converter[] converters = [primitiveTypesConverter, listsConverter]; 79 | 80 | if (exists [contextRoot, controllers] = packageToScan) { 81 | annotationScanner.scanControllers(contextRoot, controllers); 82 | } 83 | 84 | object routerMatcher extends Matcher() { 85 | shared actual Boolean matches(String path) 86 | => router.canHandlePath(path); 87 | } 88 | 89 | shared Endpoint endpoint() { 90 | return Endpoint(routerMatcher, dispatch, 91 | { options, get, head, post, put, delete, trace, connect, patch }); 92 | } 93 | 94 | "Dispatch the incoming request to the matching method." 95 | void dispatch(Request req, Response resp) { 96 | filter(req, resp, (req, resp) { 97 | value namedParams = HashMap(); 98 | 99 | if (is Handler? handler = router.routeRequest(req, namedParams)) { 100 | if (!exists handler) { 101 | // We know this path, but not for this method 102 | respond(405, "Method Not Allowed", resp); 103 | return; 104 | } 105 | assert(is Request enhancedReq = 106 | if (namedParams.empty) 107 | then req 108 | else Proxy.newProxyInstance( 109 | Types.classForType().classLoader, 110 | createJavaObjectArray {Types.classForType()}, 111 | RequestWrapper(req, namedParams) 112 | ) 113 | ); 114 | 115 | if (is [Object?, FunctionDeclaration] handler) { 116 | dispatchToController(enhancedReq, resp, handler); 117 | } else { 118 | writeResult(handler(enhancedReq, resp), req, resp); 119 | } 120 | } else { 121 | respond(404, "Not Found", resp); 122 | } 123 | }); 124 | } 125 | 126 | void dispatchToController(Request req, Response resp, [Object?, FunctionDeclaration] handler) { 127 | value func = handler[1]; 128 | value args = HashMap(); 129 | 130 | try { 131 | for (param in func.parameterDeclarations) { 132 | value arg = bindParameter(param, req, resp); 133 | 134 | if (exists arg) { 135 | args.put(param.name, arg); 136 | } else if (param.defaulted) { 137 | // use default value 138 | } else if (isOptional(param)) { 139 | args.put(param.name, null); 140 | } else { 141 | throw BindingException("Cannot bind parameter ``param.name``"); 142 | } 143 | } 144 | } catch (BindingException e) { 145 | log.error("", e); 146 | respond(400, "Bad Request", resp); 147 | return; 148 | } 149 | 150 | try { 151 | value method = if (exists o = handler[0]) 152 | then func.memberApply<>(type(o)).bind(o) 153 | else func.apply(); 154 | value result = method.namedApply(args); 155 | writeResult(result, req, resp); 156 | } catch (HaltException e) { 157 | respond(e.errorCode, e.message, resp); 158 | } catch (RedirectException e) { 159 | resp.addHeader(Header("Location", e.url)); 160 | respond(e.redirectCode, "Moved", resp); 161 | } catch (AssertionError|Exception e) { 162 | log.error("Invocation of ``func.qualifiedName`` threw an error:\n", e); 163 | respond(500, "Internal Server Error", resp); 164 | } 165 | } 166 | 167 | Boolean isOptional(FunctionOrValueDeclaration param) { 168 | if (is OpenUnion paramType = param.openType, paramType.caseTypes.size == 2) { 169 | if (exists nullType = paramType.caseTypes.find((elem) => elem == `class Null`.openType)) { 170 | return true; 171 | } 172 | } 173 | return false; 174 | } 175 | 176 | OpenType getNonOptionalType(FunctionOrValueDeclaration param) { 177 | assert (is OpenUnion paramType = param.openType); 178 | 179 | value nonOptionalType = paramType.caseTypes.find((elem) => elem != `class Null`.openType); 180 | 181 | assert (exists nonOptionalType); 182 | 183 | return nonOptionalType; 184 | } 185 | 186 | Anything? bindParameter(FunctionOrValueDeclaration param, Request req, Response resp) { 187 | if (is ValueDeclaration param) { 188 | if (param.openType == `interface Response`.openType) { 189 | return resp; 190 | } else if (param.openType == `interface Request`.openType) { 191 | return req; 192 | } else if (param.openType == `interface Flash`.openType) { 193 | return DefaultFlash(req.session); 194 | } else if (param.annotated()) { 195 | return bindSessionValue(param, req); 196 | } else { 197 | return bindRequestParameter(param, req); 198 | } 199 | } 200 | 201 | return null; 202 | } 203 | 204 | Anything? bindSessionValue(ValueDeclaration param, Request req) { 205 | if (exists val = req.session.get(param.name)) { 206 | value valType = classDeclaration(val).qualifiedName; 207 | 208 | if (valType == param.openType.string) { 209 | return val; 210 | } else if (is String val) { 211 | return convertParameter(param, val); 212 | } else { 213 | throw BindingException("Cannot bind parameter ``param.name`` from session: \ 214 | type ``valType`` cannot be assigned nor converted \ 215 | to ``param.openType``"); 216 | } 217 | } 218 | return null; 219 | } 220 | 221 | Anything? bindRequestParameter(ValueDeclaration param, Request req) { 222 | 223 | if (exists val = getValueFromRequest(req, param)) { 224 | return convertParameter(param, val); 225 | } 226 | // missing values can still be mapped to List or Sequential 227 | if (listsConverter.supports(param.openType)) { 228 | return listsConverter.convert(param.openType, []); 229 | } 230 | 231 | // Try to deserialize using a Transformer 232 | if (exists contentType = req.contentType, 233 | is OpenClassType ot = param.openType, 234 | exists tr = transformers.find((t) => t.contentTypes.contains(contentType)), 235 | exists meth = `Transformer`.getMethod<>("deserialize", ot.declaration.apply<>())) { 236 | 237 | try { 238 | return meth.bind(tr).apply(req.read()); 239 | } catch (Exception e) { 240 | throw BindingException("Could not deserialize request body to \ 241 | ``param.qualifiedName``", e); 242 | } 243 | } 244 | 245 | return null; 246 | } 247 | 248 | Anything getValueFromRequest(Request req, ValueDeclaration decl) { 249 | value targetType = if (isOptional(decl)) 250 | then getNonOptionalType(decl) 251 | else decl.openType; 252 | 253 | if (targetType == `UploadedFile`.declaration.openType) { 254 | return req.file(decl.name); 255 | } else if (listsConverter.supports(targetType)) { 256 | if (exists typeArg = listsConverter.getTypeArgument(targetType), 257 | typeArg == `UploadedFile`.declaration.openType) { 258 | 259 | return req.files(decl.name); 260 | } 261 | 262 | return req.queryParameters(decl.name) 263 | .append(req.formParameters(decl.name)); 264 | } 265 | 266 | return req.queryParameter(decl.name) 267 | else req.formParameter(decl.name); 268 | } 269 | 270 | Anything convertParameter(ValueDeclaration param, Object val) { 271 | value targetType = if (isOptional(param)) then getNonOptionalType(param) else param.openType; 272 | 273 | for (converter in converters) { 274 | if (converter.supports(targetType)) { 275 | if (is String[] val, is MultiConverter converter) { 276 | return converter.convert(targetType, val); 277 | } else if (is String val, is Converter converter) { 278 | return converter.convert(targetType, val); 279 | } 280 | } 281 | } 282 | 283 | throw BindingException("Cannot bind parameter ``param.name``: \ 284 | no converter found for type ``param.openType``"); 285 | } 286 | 287 | void writeResult(Anything result, Request req, Response resp) { 288 | if (is AnyTemplate result) { 289 | if (is TemplateRenderer renderer) { 290 | result(renderer, req, resp); 291 | } else { 292 | respond(500, "No template renderer is configured.", resp); 293 | } 294 | } else if (is String result) { 295 | resp.addHeader(contentType("text/plain", utf8)); 296 | resp.writeString(result); 297 | } else if (exists result, extendsHtmlNode(result)) { 298 | // TODO do we need this if we have net.gyokuro.view.ceylonhtml?? 299 | resp.addHeader(contentType("text/html", utf8)); 300 | resp.writeString(result.string); 301 | } else if (is Object result, exists tr = findTransformerFor(req)) { 302 | // TODO if the transformer accepts text/*, we can't set the content type to text/* 303 | log.trace("Matched content type ``req.header("Accept") else "*"`` to \ 304 | response transformer ``className(tr[0])``"); 305 | resp.addHeader(contentType(tr[1], utf8)); 306 | resp.writeString(tr[0].serialize(result)); 307 | } else if (is Object result) { 308 | respond(500, "Don't know how to write ``className(result)`` to response", resp); 309 | } else { 310 | resp.writeString(""); 311 | } 312 | } 313 | 314 | [Transformer, String]? findTransformerFor(Request req) { 315 | value accept = req.header("Accept") else "*"; 316 | 317 | for (tr in transformers) { 318 | if (exists match = mimeParse.bestMatch(tr.contentTypes, accept)) { 319 | return [tr, match]; 320 | } 321 | } 322 | 323 | return null; 324 | } 325 | 326 | void respond(Integer code, String? message, Response resp) { 327 | resp.status = code; 328 | resp.writeString("Error" 329 | + code.string + " - " + (message else "") + ""); 330 | } 331 | 332 | // Checks for HTML nodes without having a hardcoded dependency on `ceylon.html` 333 | Boolean extendsHtmlNode(Object result) 334 | => modelExtendsHtmlNode(type(result).extendedType); 335 | 336 | Boolean modelExtendsHtmlNode(ClassModel? extended) { 337 | value node = "ceylon.html::Node"; 338 | 339 | if (exists extended) { 340 | return extended.declaration.qualifiedName == node 341 | || modelExtendsHtmlNode(extended.extendedType); 342 | } 343 | return false; 344 | } 345 | } 346 | 347 | class BindingException(String? description = null, Throwable? cause = null) 348 | extends Exception(description, cause) { 349 | } 350 | 351 | shared class HaltException(shared Integer errorCode, String? message = null) 352 | extends Exception(message) { 353 | } 354 | 355 | shared class RedirectException(shared String url, shared Integer redirectCode) 356 | extends Exception() { 357 | } 358 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/internal/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.core.internal; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/internal/router.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | HashMap, 3 | ArrayList 4 | } 5 | import ceylon.http.common { 6 | Method, 7 | AbstractMethod 8 | } 9 | import ceylon.http.server { 10 | Request, 11 | Response 12 | } 13 | import ceylon.language.meta.declaration { 14 | FunctionDeclaration 15 | } 16 | import ceylon.language.meta.model { 17 | Function 18 | } 19 | 20 | import net.gyokuro.core { 21 | WSHandler 22 | } 23 | 24 | shared alias Handler => [Object?, FunctionDeclaration]|Callable; 25 | 26 | shared object router { 27 | 28 | value wsHandlers = HashMap(); 29 | 30 | shared Node root = Node(""); 31 | shared Map webSocketHandlers 32 | => wsHandlers; 33 | 34 | shared void registerRoute(String path, {Method+} methods, 35 | Function|Callable handler) 36 | given Param satisfies Anything[] { 37 | 38 | value parts = path.rest.split('/'.equals); 39 | value node = findOrCreateNode(root, parts); 40 | 41 | for (method in methods) { 42 | if (node.handles(method)) { 43 | throw Exception("Path '``path``' already defined for method '``method``'."); 44 | } 45 | if (is Function<> handler) { 46 | node.addHandler(method, [null, handler.declaration]); 47 | } else { 48 | node.addHandler(method, handler); 49 | } 50 | } 51 | } 52 | 53 | shared void registerControllerRoute(String path, 54 | [Object, FunctionDeclaration] controllerHandler, 55 | {AbstractMethod+} methods) { 56 | 57 | value node = findOrCreateNode(root, path.rest.split('/'.equals)); 58 | 59 | for (method in methods) { 60 | if (node.handles(method)) { 61 | value handler = node.getHandler(method); 62 | value desc = switch (handler) 63 | case (is [Object?, FunctionDeclaration]) handler[1].string 64 | else (handler?.string else ""); 65 | 66 | throw Exception("Path '``path``' already defined for method '``method``' 67 | by handler '``desc``'."); 68 | } 69 | node.addHandler(method, controllerHandler); 70 | } 71 | } 72 | 73 | shared void registerWebSocketHandler(String path, WSHandler handler) { 74 | 75 | if (webSocketHandlers.defines(path)) { 76 | throw Exception("Trying to override WebSocket handler for path ``path``."); 77 | } else { 78 | wsHandlers.put(path, handler); 79 | } 80 | } 81 | 82 | shared Boolean canHandlePath(String path) { 83 | return findNodeByPath(path) exists; 84 | } 85 | 86 | shared Handler?|Boolean routeRequest(Request request, HashMap namedParams) { 87 | if (exists node = findNodeByPath(request, namedParams)) { 88 | return node.getHandler(request.method); 89 | } 90 | 91 | return false; 92 | } 93 | 94 | Node? findNodeByPath(String|Request obj, HashMap? namedParams = null) { 95 | value path = if (is String obj) then obj else obj.path; 96 | value parts = path.rest.split('/'.equals, true, false); 97 | variable value node = root; 98 | value foundNode = parts.every((part) { 99 | if (exists result = node.findChild(part)) { 100 | if (result.isNamedParameter, 101 | is Request req = obj, 102 | exists namedParams) { 103 | if (part == "") { 104 | // TODO halt(404) because the route is not valid, 105 | // or throw later if the handler's parameter is not optional? 106 | } else { 107 | namedParams.put(result.subPath.rest, part); 108 | } 109 | } 110 | node = result; 111 | return true; 112 | } 113 | return false; 114 | }); 115 | 116 | return foundNode then node else null; 117 | } 118 | 119 | Node findOrCreateNode(Node root, {String*} path) { 120 | return path.fold(root, (node, nosubPath) => node.findOrCreateChild(nosubPath)); 121 | } 122 | 123 | shared void clear() { 124 | root.clear(); 125 | } 126 | } 127 | 128 | shared class Node(shared String subPath) { 129 | 130 | shared Boolean isNamedParameter = 131 | if (exists first = subPath.first, first == ':') 132 | then true else false; 133 | 134 | if (isNamedParameter) { 135 | value isValidIdentifier = if (subPath.size > 1, 136 | exists firstChar = subPath[1], 137 | firstChar.lowercase || firstChar=='_', 138 | subPath.rest.every((e) => e.letter || e=='_' || e.digit)) 139 | then true else false; 140 | 141 | if (!isValidIdentifier) { 142 | throw BindingException("Invalid named parameter '``subPath``', expected 143 | semicolon followed by a valid Ceylon LIdentifier"); 144 | } 145 | } 146 | 147 | value kids = ArrayList(); 148 | variable Map handlers = emptyMap; 149 | 150 | shared void addChild(Node node) { 151 | kids.add(node); 152 | } 153 | 154 | shared Node? findChild(String path) { 155 | return kids.find((el) => el.isNamedParameter || el.subPath==path); 156 | } 157 | 158 | shared Node findOrCreateChild(String path) { 159 | if (exists child = findChild(path)) { 160 | return child; 161 | } 162 | 163 | value child = Node(path); 164 | addChild(child); 165 | return child; 166 | } 167 | 168 | shared Boolean handles(Method method) { 169 | return handlers.defines(method); 170 | } 171 | 172 | shared Handler? getHandler(Method method) { 173 | return handlers.get(method); 174 | } 175 | 176 | shared void addHandler(Method method, Handler handler) { 177 | if (handlers == emptyMap) { 178 | handlers = HashMap(); 179 | } 180 | 181 | assert (is HashMap h = handlers); 182 | h.put(method, handler); 183 | } 184 | 185 | shared void clear() { 186 | if (is HashMap h = handlers) { 187 | h.clear(); 188 | } 189 | 190 | kids.clear(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/mimeparse.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | HashMap, 3 | MutableMap, 4 | LinkedList 5 | } 6 | 7 | "Adapted from [MIMEParse](https://code.google.com/p/mimeparse/source/browse/trunk/java/MIMEParse.java)." 8 | shared object mimeParse { 9 | 10 | "Takes a list of supported mime-types and finds the best match for all the 11 | media-ranges listed in header. The value of header must be a string that 12 | conforms to the format of the HTTP Accept: header. The value of 13 | `supported` is a list of mime-types. 14 | 15 | mimeParse.bestMatch([\"application/xbel+xml\", \"text/xml\"], 16 | \"text/*;q=0.5,*; q=0.1\") == \"text/xml\" 17 | " 18 | shared String? bestMatch(String[] supported, String header) { 19 | value parseResults = header.split(','.equals).map(parseMediaRange); 20 | 21 | value weightedMatches = LinkedList(); 22 | 23 | for (s in supported) { 24 | weightedMatches.add(fitnessAndQualityParsed(s, parseResults)); 25 | } 26 | 27 | if (exists last = sort(weightedMatches).last) { 28 | return last.quality != 0.0 then last.mimeType else null; 29 | } 30 | 31 | //FitnessAndQuality lastOne = weightedMatches 32 | // .get(weightedMatches.size() - 1); 33 | //return NumberUtils.compare(lastOne.quality, 0) != 0 ? lastOne.mimeType 34 | // : ""; 35 | return null; 36 | } 37 | 38 | FitnessAndQuality fitnessAndQualityParsed(String mimeType, {ParseResults+} parsedRanges) { 39 | variable Integer bestFitness = -1; 40 | variable Float bestFitQ = 0.0; 41 | ParseResults target = parseMediaRange(mimeType); 42 | 43 | for (range in parsedRanges) { 44 | if ((target[0] == range[0] || range[0] == "*" || target[0] == "*") 45 | && (target[1] == range[1] || range[1] == "*" || target[1] == "*")) { 46 | 47 | for (k -> v in target[2]) { 48 | variable Integer paramMatches = 0; 49 | if (!k.equals("q"), exists v2 = range[2].get(k), v == v2) { 50 | paramMatches++; 51 | } 52 | 53 | variable Integer fitness = if (range[0] == target[0]) then 100 else 0; 54 | fitness += if (range[1] == target[1]) then 10 else 0; 55 | fitness += paramMatches; 56 | 57 | if (fitness > bestFitness) { 58 | bestFitness = fitness; 59 | bestFitQ = if (is Float f = Float.parse(range[2].get("q") else "0")) 60 | then f 61 | else 0.0; 62 | } 63 | } 64 | } 65 | } 66 | 67 | return FitnessAndQuality(bestFitness, bestFitQ, mimeType); 68 | } 69 | 70 | class FitnessAndQuality(shared Integer fitness, shared Float quality, shared String mimeType) 71 | satisfies Comparable { 72 | 73 | shared actual Comparison compare(FitnessAndQuality o) { 74 | if (fitness == o.fitness) { 75 | return quality.compare(o.quality); 76 | } else { 77 | return fitness.compare(o.fitness); 78 | } 79 | } 80 | } 81 | 82 | alias ParseResults => [String, String, Map]; 83 | 84 | ParseResults parseMediaRange(String header) { 85 | value results = parseMimeType(header); 86 | value q = results[2].get("q") else "1.0"; 87 | value f = if (is Float _f = Float.parse(q)) 88 | then if (_f < 0.0 || _f > 1.0) 89 | then 1.0 90 | else _f 91 | else 1.0; 92 | 93 | assert(is MutableMap m = results[2]); 94 | m.put("q", f.string); 95 | 96 | return results; 97 | } 98 | 99 | ParseResults parseMimeType(String mimeType) { 100 | value parts = mimeType.split(';'.equals); 101 | value params = HashMap(); 102 | 103 | for (p in parts) { 104 | value seq = p.split('='.equals).sequence(); 105 | if (exists second = seq[1]) { 106 | params.put(seq.first.trimmed, second.trimmed); 107 | } 108 | } 109 | 110 | value fullType = parts.first.trimmed == "*" then "*/*" else parts.first.trimmed; 111 | value types = fullType.split('/'.equals); 112 | 113 | return [types.first.trimmed, types.sequence()[1]?.trimmed else "", params]; 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /source/net/gyokuro/core/module.ceylon: -------------------------------------------------------------------------------- 1 | "gyokuro is a framework written in Ceylon, similar to Sinatra 2 | and Spark, for creating web applications with very little boilerplate. 3 | It is based on the Ceylon SDK and uses `ceylon.http.server`." 4 | native("jvm") 5 | module net.gyokuro.core "0.4-SNAPSHOT" { 6 | value ceylonVersion = "1.3.4-SNAPSHOT"; 7 | 8 | shared import net.gyokuro.view.api "0.4-SNAPSHOT"; 9 | shared import net.gyokuro.transform.api "0.4-SNAPSHOT"; 10 | 11 | shared import ceylon.http.server ceylonVersion; 12 | shared import ceylon.json ceylonVersion; 13 | 14 | import ceylon.logging ceylonVersion; 15 | import ceylon.collection ceylonVersion; 16 | import ceylon.io ceylonVersion; 17 | 18 | import java.base "7"; 19 | } 20 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.core; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/core/websocket.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server.websocket { 2 | WebSocketChannel, 3 | CloseReason 4 | } 5 | import ceylon.buffer { 6 | ByteBuffer 7 | } 8 | import net.gyokuro.core.internal { 9 | router 10 | } 11 | 12 | 13 | "A handler that can react to WebSocket events." 14 | shared alias WSHandler => Anything(WebSocketChannel, String)|WebSocketHandler; 15 | 16 | "A handler for WebSockets that reacts to advanced events." 17 | shared abstract class WebSocketHandler() { 18 | shared default void onOpen(WebSocketChannel channel) {} 19 | 20 | shared default void onClose(WebSocketChannel channel, CloseReason closeReason) {} 21 | 22 | shared default void onError(WebSocketChannel channel, Throwable? throwable) {} 23 | 24 | shared default void onText(WebSocketChannel channel, String text) {} 25 | 26 | shared default void onBinary(WebSocketChannel channel, ByteBuffer binary) {} 27 | } 28 | 29 | "Registers a new web socket handler for the given [[path]]. The handler can be a simple 30 | 'onText' function, or a more advanced [[WebSocketHandler]]." 31 | shared void websocket(String path, WSHandler handler) 32 | => router.registerWebSocketHandler(path, handler); -------------------------------------------------------------------------------- /source/net/gyokuro/report/GyokuroApiGenerator.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | HashMap 3 | } 4 | import ceylon.file { 5 | Directory, 6 | File, 7 | Nil, 8 | createFileIfNil 9 | } 10 | import ceylon.html { 11 | Div, 12 | Html, 13 | Head, 14 | Body, 15 | H1, 16 | P, 17 | Em, 18 | Span, 19 | Link, 20 | H2, 21 | Table, 22 | Th, 23 | Td, 24 | Tr, 25 | THead, 26 | TBody 27 | } 28 | import ceylon.language.meta.declaration { 29 | Package, 30 | FunctionDeclaration, 31 | AnnotatedDeclaration, 32 | ValueDeclaration, 33 | InterfaceDeclaration, 34 | OpenType, 35 | OpenUnion, 36 | FunctionOrValueDeclaration 37 | } 38 | import ceylon.http.common { 39 | AbstractMethod 40 | } 41 | import ceylon.http.server { 42 | Request, 43 | Response 44 | } 45 | 46 | import net.gyokuro.core.internal { 47 | annotationScanner 48 | } 49 | 50 | shared class GyokuroApiGenerator(Package controllersPkg, Directory output) { 51 | 52 | value routes = HashMap(); 53 | value exludedTypes = 54 | [`interface Request`, `interface Response`].map(InterfaceDeclaration.openType); 55 | 56 | shared void run() { 57 | print("Scanning controllers..."); 58 | annotationScanner.scanControllers("/", controllersPkg, addRoute); 59 | 60 | print("Generating report..."); 61 | generateReport(); 62 | 63 | print("Done."); 64 | } 65 | 66 | void addRoute(String path, [Object, FunctionDeclaration] controllerHandler, 67 | {AbstractMethod+} methods) { 68 | 69 | routes.put(path, [controllerHandler[1], methods]); 70 | } 71 | 72 | void writeReport(Html html) { 73 | value resource = output.childResource("report.html"); 74 | value file = if (is File resource) 75 | then resource.Overwriter() 76 | else if (is Nil resource) then resource.createFile().Overwriter() 77 | else null; 78 | if (exists file) { 79 | file.write(html.string); 80 | file.flush(); 81 | file.close(); 82 | } else { 83 | print("Can't write to file ``resource.path.string``"); 84 | } 85 | if (exists css = `module`.resourceByPath("style.css")) { 86 | if (is File|Nil target = output.childResource("style.css")) { 87 | value outCss = createFileIfNil(target).Overwriter(); 88 | outCss.write(css.textContent()); 89 | outCss.flush(); 90 | outCss.close(); 91 | } else { 92 | print("Couldn't copy style.css"); 93 | } 94 | } else { 95 | print("Couldn't find style.css"); 96 | } 97 | } 98 | 99 | void generateReport() { 100 | value html = Html { 101 | Head { 102 | title = "gyokuro app API"; 103 | children = { 104 | Link {rel="stylesheet"; href = "style.css";} 105 | }; 106 | }, 107 | Body { 108 | H1 { 109 | "API for package \```controllersPkg.qualifiedName``\`" 110 | }, 111 | P { 112 | getDocumentation(controllersPkg) 113 | }, 114 | for (path->[func, methods] in routes) 115 | for (method in methods) 116 | generateRoute(path, method.string, func) 117 | } 118 | }; 119 | 120 | writeReport(html); 121 | } 122 | 123 | function generateRouteHeader(String method, String path, FunctionDeclaration func) { 124 | return Div { 125 | clazz = "route-header"; 126 | attributes = 127 | ["onclick"->"this.nextElementSibling.classList.toggle('collapsed')"]; 128 | children = { 129 | Span { 130 | method 131 | }, 132 | Span { 133 | path 134 | }, 135 | Span { 136 | getDocumentation(func) 137 | } 138 | }; 139 | }; 140 | } 141 | 142 | Div generateRouteBody(String method, String path, FunctionDeclaration func) { 143 | value parameters = { 144 | for (p in func.parameterDeclarations) 145 | if (is ValueDeclaration p, !exludedTypes.contains(p.openType)) 146 | Tr { 147 | Td { p.name }, 148 | Td { getDocumentation(p) }, 149 | Td { prettifyType(p.openType) } 150 | } 151 | }; 152 | 153 | return Div { 154 | clazz = "collapsed route-params"; 155 | children = { 156 | H2 {"Parameters"}, 157 | if (parameters.empty) 158 | then Em {"No parameters"} 159 | else Table { 160 | THead { 161 | Tr { 162 | Th {"Parameter"}, Th {"Description"}, Th {"Parameter type"} 163 | } 164 | }, 165 | TBody {parameters} 166 | }, 167 | H2{"Returns" }, 168 | Div {prettifyType(func.openType)}, 169 | if (!parameters.empty) 170 | then { 171 | H2 {"Response messages"}, 172 | Table { 173 | THead { 174 | Tr { 175 | Th {"HTTP status code"}, Th {"Reason"} 176 | } 177 | }, 178 | TBody { 179 | if (hasRequiredParameters(func)) 180 | then Tr { 181 | Td {"400"}, 182 | Td {"Missing required parameter"} 183 | } 184 | else null, 185 | Tr { 186 | Td {"400"}, 187 | Td {"Invalid parameter value"} 188 | } 189 | } 190 | } 191 | } 192 | else {} 193 | }; 194 | }; 195 | } 196 | 197 | String prettifyType(OpenType type) 198 | => type.string.replace("ceylon.language::", ""); 199 | 200 | Div generateRoute(String path, String method, FunctionDeclaration func) { 201 | return Div { 202 | clazz = "method-" + method.lowercased; 203 | generateRouteHeader(method, path, func), 204 | generateRouteBody(method, path, func) 205 | }; 206 | } 207 | 208 | String|Em getDocumentation(AnnotatedDeclaration decl) { 209 | if (exists ann = decl.annotations().first) { 210 | return ann.description; 211 | } 212 | 213 | return Em {"No description"}; 214 | } 215 | 216 | Boolean hasRequiredParameters(FunctionDeclaration func) { 217 | for (p in func.parameterDeclarations) { 218 | if (is ValueDeclaration p, 219 | !exludedTypes.contains(p.openType), 220 | !isOptional(p)) { 221 | 222 | return true; 223 | } 224 | } 225 | 226 | return false; 227 | } 228 | 229 | Boolean isOptional(FunctionOrValueDeclaration param) { 230 | if (is OpenUnion paramType = param.openType, paramType.caseTypes.size == 2) { 231 | if (exists nullType = paramType.caseTypes.find((elem) => elem == `class Null`.openType)) { 232 | return true; 233 | } 234 | } 235 | return false; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /source/net/gyokuro/report/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module net.gyokuro.report "0.4-SNAPSHOT" { 3 | value ceylonVersion = "1.3.4-SNAPSHOT"; 4 | 5 | shared import ceylon.file ceylonVersion; 6 | 7 | import ceylon.html ceylonVersion; 8 | import net.gyokuro.core "0.4-SNAPSHOT"; 9 | } 10 | -------------------------------------------------------------------------------- /source/net/gyokuro/report/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.report; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/report/run.ceylon: -------------------------------------------------------------------------------- 1 | "Run the module `net.gyokuro.report`." 2 | shared void run() { 3 | 4 | } -------------------------------------------------------------------------------- /source/net/gyokuro/transform/api/Transformer.ceylon: -------------------------------------------------------------------------------- 1 | 2 | "A transformer that can convert the body of a Request to an Object, 3 | or transform an Object to the body of a Response." 4 | shared interface Transformer { 5 | "Trasnforms an object to a string that will be the body of a Response." 6 | shared formal String serialize(Object o); 7 | 8 | "Tranforms the body of a Request to an Object that can be passed to a Request handler." 9 | shared formal Instance deserialize(String serialized) 10 | given Instance satisfies Object; 11 | 12 | "Specifies which MIME types this transformer supports." 13 | shared formal [String+] contentTypes; 14 | } 15 | -------------------------------------------------------------------------------- /source/net/gyokuro/transform/api/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module net.gyokuro.transform.api "0.4-SNAPSHOT" { 3 | import ceylon.interop.java "1.3.4-SNAPSHOT"; 4 | } 5 | -------------------------------------------------------------------------------- /source/net/gyokuro/transform/api/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.transform.api; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/transform/gson/GsonTransformer.ceylon: -------------------------------------------------------------------------------- 1 | import com.google.gson { 2 | GsonBuilder 3 | } 4 | 5 | import java.lang { 6 | Types 7 | } 8 | 9 | import net.gyokuro.transform.api { 10 | Transformer 11 | } 12 | 13 | shared class GsonTransformer() satisfies Transformer { 14 | 15 | shared GsonBuilder gson = GsonBuilder(); 16 | 17 | shared actual [String+] contentTypes => ["application/json", "application/javascript"]; 18 | 19 | shared actual Instance deserialize(String serialized) 20 | given Instance satisfies Object 21 | => gson.create().fromJson(serialized, Types.classForType()); 22 | 23 | shared actual String serialize(Object o) 24 | => gson.create().toJson(o); 25 | } 26 | -------------------------------------------------------------------------------- /source/net/gyokuro/transform/gson/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module net.gyokuro.transform.gson "0.4-SNAPSHOT" { 3 | shared import net.gyokuro.transform.api "0.4-SNAPSHOT"; 4 | shared import maven:"com.google.code.gson:gson" "2.5"; 5 | 6 | import ceylon.interop.java "1.3.4-SNAPSHOT"; 7 | } 8 | -------------------------------------------------------------------------------- /source/net/gyokuro/transform/gson/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.transform.gson; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/api/module.ceylon: -------------------------------------------------------------------------------- 1 | "An extension point for gyokuro to provide support for 2 | a template engine." 3 | native("jvm") 4 | module net.gyokuro.view.api "0.4-SNAPSHOT" { 5 | value ceylonVersion = "1.3.4-SNAPSHOT"; 6 | 7 | shared import ceylon.http.server ceylonVersion; 8 | shared import ceylon.interop.java ceylonVersion; 9 | } 10 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/api/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.view.api; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/api/templates.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Request, 3 | Response 4 | } 5 | import java.lang { 6 | JString=String, 7 | Types 8 | } 9 | import java.util { 10 | JMap=Map, 11 | HashMap 12 | } 13 | import ceylon.interop.java { 14 | JavaList, 15 | JavaIterable 16 | } 17 | 18 | "A wrapper for a template engine capable of rendering 19 | a template to a [[String]]." 20 | shared interface TemplateRenderer { 21 | shared formal String render( 22 | "The template to be rendered." 23 | Template template, 24 | "A map of named values that can be used in the template." 25 | Map context, 26 | "The HTTP request." 27 | Request req, 28 | "The HTTP response." 29 | Response resp); 30 | } 31 | 32 | "An abstract [[TemplateRenderer]] based on a Java templating engine, that automatically 33 | converts Ceylon collections to Java collections." 34 | shared abstract class JavaTemplateRenderer(contextEnhancer = noop) 35 | satisfies TemplateRenderer<> { 36 | 37 | "A callback that can add custom entries to the context before 38 | passing it to the templating engine." 39 | void contextEnhancer(Request req, Response resp, JMap context); 40 | 41 | shared JMap wrapMap(Map context, 42 | Request request, Response response) { 43 | 44 | value result = HashMap(); 45 | 46 | contextEnhancer(request, response, result); 47 | 48 | for (key->val in context) { 49 | value javaVal = switch (val) 50 | case (is List) JavaList(val) 51 | else if (is Iterable<> val) then JavaIterable(val) 52 | else val; 53 | 54 | result.put(Types.nativeString(key), javaVal else null); 55 | } 56 | 57 | return result; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/ceylonhtml/CeylonHtmlRenderer.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.html { 2 | Element 3 | } 4 | import ceylon.http.server { 5 | Request, 6 | Response 7 | } 8 | 9 | import net.gyokuro.core { 10 | AnyTemplate 11 | } 12 | import net.gyokuro.view.api { 13 | TemplateRenderer 14 | } 15 | 16 | shared alias HtmlTemplate => AnyTemplate; 17 | 18 | shared class CeylonHtmlRenderer() satisfies TemplateRenderer { 19 | 20 | shared actual String render(Element template, Map context, Request req, Response resp) 21 | => template.string; 22 | 23 | } -------------------------------------------------------------------------------- /source/net/gyokuro/view/ceylonhtml/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module net.gyokuro.view.ceylonhtml "0.4-SNAPSHOT" { 3 | shared import net.gyokuro.view.api "0.4-SNAPSHOT"; 4 | shared import net.gyokuro.core "0.4-SNAPSHOT"; 5 | shared import ceylon.html "1.3.4-SNAPSHOT"; 6 | } 7 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/ceylonhtml/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.view.ceylonhtml; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/mustache/MustacheRenderer.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Request, 3 | Response 4 | } 5 | 6 | import net.gyokuro.view.api { 7 | JavaTemplateRenderer 8 | } 9 | import com.github.mustachejava { 10 | DefaultMustacheFactory 11 | } 12 | 13 | import java.io { 14 | StringWriter 15 | } 16 | import java.lang { 17 | JString=String 18 | } 19 | import java.util { 20 | JMap=Map 21 | } 22 | 23 | "A [[net.gyokuro.view.api::TemplateRenderer]] based on the 24 | [Mustache.java](https://github.com/spullara/mustache.java) templating engine." 25 | shared class MustacheRenderer(prefix = "", suffix = "", contextEnhancer = noop) 26 | extends JavaTemplateRenderer(contextEnhancer) { 27 | 28 | "A prefix to be added before the template name." 29 | String prefix; 30 | 31 | "A suffix to be added after the template name." 32 | String suffix; 33 | 34 | "A callback that can add custom entries to the context before passing it to Mustache. 35 | Custom entries can be overriden by handlers using the `render()` function." 36 | void contextEnhancer(Request req, Response resp, JMap context); 37 | 38 | value factory = DefaultMustacheFactory(); 39 | 40 | shared actual String render(String templateName, Map context, 41 | Request req, Response resp) { 42 | 43 | value mustache = factory.compile(prefix + templateName + suffix); 44 | value writer = StringWriter(); 45 | 46 | value javaContext = wrapMap(context, req, resp); 47 | mustache.execute(writer, javaContext).flush(); 48 | 49 | return writer.string; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/mustache/module.ceylon: -------------------------------------------------------------------------------- 1 | "An extension of gyokuro that adds support for 2 | [Mustache.java](https://github.com/spullara/mustache.java)." 3 | native("jvm") 4 | module net.gyokuro.view.mustache "0.4-SNAPSHOT" { 5 | shared import net.gyokuro.view.api "0.4-SNAPSHOT"; 6 | shared import maven:"com.github.spullara.mustache.java:compiler" "0.8.18"; 7 | 8 | import ceylon.interop.java "1.3.4-SNAPSHOT"; 9 | } 10 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/mustache/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.view.mustache; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/pebble/PebbleRenderer.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Request, 3 | Response 4 | } 5 | 6 | import net.gyokuro.view.api { 7 | JavaTemplateRenderer 8 | } 9 | import com.mitchellbosecke.pebble { 10 | PebbleEngine { 11 | Builder 12 | } 13 | } 14 | import com.mitchellbosecke.pebble.loader { 15 | FileLoader 16 | } 17 | 18 | import java.io { 19 | StringWriter 20 | } 21 | import java.lang { 22 | JString=String 23 | } 24 | import java.util { 25 | JMap=Map 26 | } 27 | 28 | "A [[net.gyokuro.view.api::TemplateRenderer]] based on the 29 | [Pebble](http://www.mitchellbosecke.com/pebble) templating engine." 30 | shared class PebbleRenderer(prefix = null, suffix = null, contextEnhancer = noop, builder = noop) 31 | extends JavaTemplateRenderer(contextEnhancer) { 32 | 33 | "The optional prefix passed to the [[FileLoader]], for example `views/`." 34 | String? prefix; 35 | 36 | "The optional suffix passed to the [[FileLoader]], for example `.pebble`." 37 | String? suffix; 38 | 39 | "A callback that can add custom entries to the context before passing it to Pebble. 40 | Custom entries can be overriden by handlers using the `render()` function." 41 | void contextEnhancer(Request req, Response resp, JMap context); 42 | 43 | void builder(Builder b); 44 | 45 | value loader = FileLoader(); 46 | 47 | "The Pebble engine." 48 | shared PebbleEngine engine { 49 | value b = Builder().loader(loader); 50 | builder(b); 51 | return b.build(); 52 | } 53 | 54 | loader.prefix = prefix; 55 | loader.suffix = suffix; 56 | 57 | shared actual String render(String templateName, Map context, 58 | Request req, Response resp) { 59 | 60 | value tpl = engine.getTemplate(templateName); 61 | value writer = StringWriter(); 62 | value jMap = wrapMap(context, req, resp); 63 | 64 | tpl.evaluate(writer, jMap); 65 | 66 | return writer.string; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/pebble/module.ceylon: -------------------------------------------------------------------------------- 1 | "An extension of gyokuro that adds support for 2 | [Pebble](http://www.mitchellbosecke.com/pebble)." 3 | native("jvm") 4 | module net.gyokuro.view.pebble "0.4-SNAPSHOT" { 5 | shared import net.gyokuro.core "0.4-SNAPSHOT"; 6 | shared import maven:"com.mitchellbosecke:pebble" "2.2.0"; 7 | import java.base "7"; 8 | import ceylon.interop.java "1.3.4-SNAPSHOT"; 9 | } 10 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/pebble/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.view.pebble; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/rythm/RythmRenderer.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Request, 3 | Response 4 | } 5 | 6 | import net.gyokuro.view.api { 7 | JavaTemplateRenderer 8 | } 9 | 10 | import java.lang { 11 | JString=String 12 | } 13 | import java.util { 14 | JMap=Map 15 | } 16 | 17 | import org.rythmengine { 18 | RythmEngine 19 | } 20 | 21 | "A [[net.gyokuro.view.api::TemplateRenderer]] based on the 22 | [Rythm](http://rythmengine.org/) templating engine." 23 | shared class RythmRenderer(prefix = "", suffix = "", contextEnhancer = noop) 24 | extends JavaTemplateRenderer(contextEnhancer) { 25 | 26 | "A prefix to be used as the property `home.template`." 27 | String prefix; 28 | 29 | "A suffix to be added after the template name." 30 | String suffix; 31 | 32 | "A callback that can add custom entries to the context before passing it to Rythm. 33 | Custom entries can be overriden by handlers using the `render()` function." 34 | void contextEnhancer(Request req, Response resp, JMap context); 35 | 36 | shared RythmEngine engine = RythmEngine(); 37 | 38 | shared actual String render(String templateName, Map context, 39 | Request req, Response resp) { 40 | 41 | value javaMap = wrapMap(context, req, resp); 42 | return engine.render(prefix + templateName + suffix, javaMap); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/rythm/module.ceylon: -------------------------------------------------------------------------------- 1 | "An extension of gyokuro that adds support for 2 | [Rythm](http://rythmengine.org/)." 3 | native("jvm") 4 | module net.gyokuro.view.rythm "0.4-SNAPSHOT" { 5 | shared import net.gyokuro.view.api "0.4-SNAPSHOT"; 6 | shared import maven:"org.rythmengine:rythm-engine" "1.0.1"; 7 | 8 | import java.base "7"; 9 | import ceylon.interop.java "1.3.4-SNAPSHOT"; 10 | } 11 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/rythm/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.view.rythm; 2 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/thymeleaf/ThymeleafRenderer.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Request, 3 | Response 4 | } 5 | 6 | import net.gyokuro.view.api { 7 | JavaTemplateRenderer 8 | } 9 | 10 | import java.lang { 11 | JString=String 12 | } 13 | import java.util { 14 | JMap=Map, 15 | Locale 16 | } 17 | 18 | import org.thymeleaf { 19 | TemplateEngine 20 | } 21 | import org.thymeleaf.context { 22 | Context 23 | } 24 | import org.thymeleaf.templateresolver { 25 | FileTemplateResolver 26 | } 27 | 28 | "A [[net.gyokuro.view.api::TemplateRenderer]] based on the 29 | [Thymeleaf](http://www.thymeleaf.org/) templating engine." 30 | shared class ThymeleafRenderer(prefix = null, suffix = null, contextEnhancer = noop) 31 | extends JavaTemplateRenderer(contextEnhancer) { 32 | 33 | "A prefix to be added before the template name." 34 | String? prefix; 35 | 36 | "A suffix to be added after the template name." 37 | String? suffix; 38 | 39 | "A callback that can add custom entries to the context before passing it to Thymeleaf. 40 | Custom entries can be overriden by handlers using the `render()` function." 41 | void contextEnhancer(Request req, Response resp, JMap context); 42 | 43 | value resolver = FileTemplateResolver(); 44 | 45 | "The Thymeleaf engine." 46 | shared TemplateEngine engine = TemplateEngine(); 47 | 48 | resolver.prefix = prefix; 49 | resolver.suffix = suffix; 50 | engine.setTemplateResolver(resolver); 51 | 52 | shared actual String render(String templateName, Map context, 53 | Request req, Response resp) { 54 | 55 | value jMap = wrapMap(context, req, resp); 56 | 57 | return engine.process(templateName, Context(Locale.default, jMap)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/thymeleaf/module.ceylon: -------------------------------------------------------------------------------- 1 | "An extension of gyokuro that adds support for 2 | [Thymeleaf](http://www.thymeleaf.org/)." 3 | native("jvm") 4 | module net.gyokuro.view.thymeleaf "0.4-SNAPSHOT" { 5 | shared import net.gyokuro.view.api "0.4-SNAPSHOT"; 6 | shared import maven:"org.thymeleaf:thymeleaf" "3.0.0.BETA01"; 7 | 8 | import java.base "7"; 9 | import ceylon.interop.java "1.3.4-SNAPSHOT"; 10 | } 11 | -------------------------------------------------------------------------------- /source/net/gyokuro/view/thymeleaf/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package net.gyokuro.view.thymeleaf; 2 | -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/filtersTest.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.http.server { 2 | Request, 3 | Response, 4 | started 5 | } 6 | import ceylon.test { 7 | test, 8 | assertEquals 9 | } 10 | 11 | import net.gyokuro.core { 12 | clearRoutes, 13 | Application, 14 | get 15 | } 16 | 17 | import test.net.gyokuro.core.internal { 18 | request 19 | } 20 | 21 | test shared void testFilters() { 22 | clearRoutes(); 23 | 24 | void filter1(Request req, Response resp, void next(Request req, Response resp)) { 25 | resp.writeString("beforeFilter1"); 26 | next(req, resp); 27 | resp.writeString("afterFilter1"); 28 | } 29 | void filter2(Request req, Response resp, void next(Request req, Response resp)) { 30 | resp.writeString("beforeFilter2"); 31 | next(req, resp); 32 | resp.writeString("afterFilter2"); 33 | } 34 | 35 | get("/filters", (req, resp) => resp.writeString("hello world")); 36 | 37 | value app = Application { 38 | port = 23456; 39 | filters = [filter1, filter2]; 40 | }; 41 | app.run((status) { 42 | if (status == started) { 43 | assertEquals( 44 | request("/filters"), 45 | "".join { "beforeFilter1", "beforeFilter2", "hello world", "afterFilter2", "afterFilter1" } 46 | ); 47 | app.stop(); 48 | } 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/internal/AnnotationScannerTest.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.test { 2 | test, 3 | assertTrue 4 | } 5 | 6 | import net.gyokuro.core.internal { 7 | annotationScanner, 8 | router 9 | } 10 | import net.gyokuro.core { 11 | clearRoutes 12 | } 13 | 14 | shared test void scanClass() { 15 | clearRoutes(); 16 | annotationScanner.scanControllers("/", 17 | `package test.net.gyokuro.core.internal.testdata`); 18 | 19 | assertTrue(router.canHandlePath("/cls/func")); 20 | //value func = rou.get("/cls/func"); 21 | //assert(exists func); 22 | //value handler = func[1]; 23 | //assertEquals("func", handler.name); 24 | // 25 | //assert(exists otherfunc = controllers.get("/cls/otherfunc")); 26 | } 27 | 28 | shared test void scanPathWithSlashes() { 29 | clearRoutes(); 30 | annotationScanner.scanControllers("/", 31 | `package test.net.gyokuro.core.internal.testdata`); 32 | 33 | assertTrue(router.canHandlePath("/path/function")); 34 | } 35 | 36 | shared test void scanControllerWithoutRoute() { 37 | clearRoutes(); 38 | annotationScanner.scanControllers("/", 39 | `package test.net.gyokuro.core.internal.testdata`); 40 | 41 | assertTrue(router.canHandlePath("/func3")); 42 | } 43 | 44 | shared test void scanObjectController() { 45 | clearRoutes(); 46 | annotationScanner.scanControllers("/", 47 | `package test.net.gyokuro.core.internal.testdata`); 48 | 49 | assertTrue(router.canHandlePath("/obj/func4")); 50 | } -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/internal/RequestDispatcherTest.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.html { 2 | Html, 3 | Body, 4 | H1 5 | } 6 | import ceylon.http.client { 7 | Request 8 | } 9 | import ceylon.http.common { 10 | Method, 11 | getMethod=get, 12 | postMethod=post 13 | } 14 | import ceylon.http.server { 15 | newServer, 16 | Status, 17 | started, 18 | Response 19 | } 20 | import ceylon.io { 21 | SocketAddress 22 | } 23 | import ceylon.logging { 24 | addLogWriter, 25 | Priority, 26 | Category 27 | } 28 | import ceylon.test { 29 | test, 30 | assertEquals, 31 | assertTrue 32 | } 33 | import ceylon.uri { 34 | Uri, 35 | Authority, 36 | Path, 37 | PathSegment, 38 | Parameter 39 | } 40 | 41 | import net.gyokuro.core { 42 | get, 43 | halt, 44 | clearRoutes, 45 | patch 46 | } 47 | import net.gyokuro.core.http { 48 | patchMethod=patch 49 | } 50 | import net.gyokuro.core.internal { 51 | RequestDispatcher 52 | } 53 | 54 | shared test 55 | void testDispatcher() { 56 | clearRoutes(); 57 | 58 | value dispatcher = 59 | RequestDispatcher( 60 | ["/", `package test.net.gyokuro.core.internal.testdata`], 61 | (req, resp, next) => next(req, resp)) 62 | .endpoint(); 63 | 64 | addLogWriter { 65 | void log(Priority p, Category c, String m, Throwable? e) { 66 | print("``p.string`` ``m``"); 67 | if (exists e) { 68 | printStackTrace(e); 69 | } 70 | } 71 | }; 72 | 73 | value server = newServer({ dispatcher }); 74 | server.addListener(void(Status status) { 75 | if (status == started) { 76 | try { 77 | runTests(); 78 | } finally { 79 | server.stop(); 80 | } 81 | } 82 | }); 83 | server.start(SocketAddress("127.0.0.1", 23456)); 84 | } 85 | 86 | void runTests() { 87 | // single param 88 | assertEquals(request("/param/f1", { Parameter("string", "foo") }), "foo"); 89 | 90 | // multiple params 91 | assertEquals(request("/param/f2", 92 | { Parameter("boolean", "true"), 93 | Parameter("integer", "42") }), "true42"); 94 | 95 | // booleans 96 | assertEquals(request("/param/f3", 97 | { Parameter("b1", "true"), 98 | Parameter("b2", "1"), 99 | Parameter("b3", "false"), 100 | Parameter("b4", "0") }), 101 | "truetruefalsefalse"); 102 | 103 | // floats 104 | assertEquals(request("/param/f4", 105 | { Parameter("f1", "0"), 106 | Parameter("f2", "3.14159265359"), 107 | Parameter("f3", "-2.71828182") }), 108 | "0.03.14159265359-2.71828182"); 109 | 110 | // optional types 111 | assertEquals(request("/param/f5", 112 | { Parameter("s1", "stup") }), 113 | "stupeflip"); 114 | 115 | // default values 116 | assertEquals(request("/param/f6", 117 | { Parameter("s1", "Ceylon") }), 118 | "Ceylon4ever"); 119 | assertEquals(request("/param/f6", 120 | { Parameter("s1", "log"), 121 | Parameter("s2", "j") }), 122 | "log4j"); 123 | assertEquals(request("/param/f6", 124 | { Parameter("s1", "map"), 125 | Parameter("s2", "list"), 126 | Parameter("i", "2") }), 127 | "map2list"); 128 | 129 | get("/simple", (req, res) => "Hello!"); 130 | assertEquals(request("/simple", {}), "Hello!"); 131 | 132 | get("/myRoute", `myHandler`); 133 | assertEquals(request("/myRoute", 134 | { Parameter("s1", "abc"), 135 | Parameter("i1", "123") }), 136 | "abc123"); 137 | 138 | get("/testHalt", `testHalt`); 139 | assertTrue(request("/testHalt", {}) 140 | .contains("500 - I can haz an error")); 141 | 142 | assertTrue(request("/simple", {}, postMethod) 143 | .contains("405 - Method Not Allowed")); 144 | 145 | assertTrue(request("/notfound", {}) 146 | .contains("404 - Not Found")); 147 | 148 | assertEquals(request("/lists/list", { 149 | Parameter("strings", "a"), 150 | Parameter("strings", "b"), 151 | Parameter("strings", "c"), 152 | Parameter("strings", "d"), 153 | Parameter("strings", "e") 154 | }), "abcde"); 155 | 156 | assertEquals(request("/lists/list2", { 157 | Parameter("bools", "1"), 158 | Parameter("bools", "0"), 159 | Parameter("ints", "6"), 160 | Parameter("ints", "2"), 161 | Parameter("ints", "4") 162 | }), "truefalse624"); 163 | 164 | assertEquals(request("/lists/sequential", { 165 | Parameter("ints", "8"), 166 | Parameter("ints", "4"), 167 | Parameter("ints", "5") 168 | }), "845"); 169 | 170 | assertEquals(request("/lists/sequence", { 171 | Parameter("ints", "8") 172 | }), "8"); 173 | 174 | // can't bind an empty array to a [Integer+] 175 | assertTrue(request("/lists/sequence", {}) 176 | .contains("400")); 177 | 178 | // named arguments 179 | assertEquals(request("/param/hello/world", {}), "Hello, world!"); 180 | assertEquals(request("/param/hello/234", {}), "Hello, 234!"); 181 | 182 | get("/ceylon.html", (req, resp) => 183 | Html { 184 | Body { 185 | H1 {"hello"} 186 | } 187 | } 188 | ); 189 | assertTrue(request("/ceylon.html").contains("

hello

")); 190 | 191 | patch("/patchme", (req, resp) => "patched"); 192 | assertTrue(request("/patchme", {}, patchMethod).contains("patched")); 193 | } 194 | 195 | void myHandler(String s1, Integer i1, Response resp) { 196 | resp.writeString(s1 + i1.string); 197 | } 198 | 199 | suppressWarnings("expressionTypeNothing") 200 | void testHalt() { 201 | halt(500, "I can haz an error"); 202 | } 203 | shared String request(String path, {Parameter*} params = {}, Method method = getMethod) { 204 | value segments = path.split('/'.equals, true, false) 205 | .filter((_) => !_.empty) 206 | .map(PathSegment); 207 | value uri = Uri("http", 208 | Authority(null, null, "127.0.0.1", 23456, false), 209 | Path(true, *segments) 210 | ); 211 | value request = Request { 212 | uri = uri; 213 | initialParameters = params; 214 | method = method; 215 | }; 216 | 217 | value response = request.execute(); 218 | 219 | return response.contents; 220 | } 221 | -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/internal/package.ceylon: -------------------------------------------------------------------------------- 1 | package test.net.gyokuro.core.internal; 2 | -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/internal/testdata/AnnotatedClass.ceylon: -------------------------------------------------------------------------------- 1 | import net.gyokuro.core { 2 | route, 3 | controller 4 | } 5 | 6 | route("cls") 7 | controller class AnnotatedClass() { 8 | 9 | route("func") 10 | shared String func() => ""; 11 | 12 | route("otherfunc") 13 | shared String otherFunc() => ""; 14 | } 15 | 16 | route("/path/") 17 | controller class AnnotatedClass2() { 18 | 19 | route("/function") 20 | shared String func2() => ""; 21 | } 22 | 23 | controller class AnnotatedClass3() { 24 | 25 | route("/func3") 26 | shared String func3() => ""; 27 | } 28 | 29 | route("obj") 30 | controller object myController { 31 | 32 | route("/func4") 33 | shared String func4() => ""; 34 | } 35 | -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/internal/testdata/ListBinding.ceylon: -------------------------------------------------------------------------------- 1 | import net.gyokuro.core { 2 | route, 3 | controller 4 | } 5 | 6 | route("lists") 7 | controller object listBinding { 8 | 9 | route("list") 10 | shared String list(List strings) { 11 | return strings.reduce((String partial, String element) => partial + element) 12 | else "empty"; 13 | } 14 | 15 | route("list2") 16 | shared String list2(List bools, List ints) { 17 | return "".join(bools.chain(ints).map(Object.string)); 18 | } 19 | 20 | route("sequential") 21 | shared String sequential([Integer*] ints) { 22 | return "".join(ints.map(Object.string)); 23 | } 24 | 25 | route("sequence") 26 | shared String sequence([Integer+] ints) { 27 | return "".join(ints.map(Object.string)); 28 | } 29 | } -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/internal/testdata/ParameterBinding.ceylon: -------------------------------------------------------------------------------- 1 | import net.gyokuro.core { 2 | controller, 3 | route 4 | } 5 | 6 | route("param") 7 | controller class ParameterBinding() { 8 | 9 | route("f1") 10 | shared String func1(String string) { 11 | return string; 12 | } 13 | 14 | route("f2") 15 | shared String func2(Boolean boolean, Integer integer) { 16 | return boolean.string + integer.string; 17 | } 18 | 19 | route("f3") 20 | shared String func3(Boolean b1, Boolean b2, Boolean b3, Boolean b4) { 21 | return b1.string + b2.string + b3.string + b4.string; 22 | } 23 | 24 | route("f4") 25 | shared String func4(Float f1, Float f2, Float f3) { 26 | return f1.string + f2.string + f3.string; 27 | } 28 | 29 | route("f5") 30 | shared String func5(String s1, String? s2) { 31 | return "``s1``e``s2 else "flip"``"; 32 | } 33 | 34 | route("f6") 35 | shared String func6(String s1, Integer i = 4, String s2 = "ever") { 36 | return s1 + i.string + s2; 37 | } 38 | 39 | route("hello/:who") 40 | shared String hello(String who) => "Hello, ``who``!"; 41 | } -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/internal/testdata/package.ceylon: -------------------------------------------------------------------------------- 1 | package test.net.gyokuro.core.internal.testdata; 2 | -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/mimeParseTest.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.test { 2 | test, 3 | assertEquals 4 | } 5 | import net.gyokuro.core { 6 | mimeParse 7 | } 8 | 9 | test void testMimeParse() { 10 | // direct match 11 | assertEquals(mimeParse.bestMatch(["application/xbel+xml", "application/xml"], 12 | "application/xbel+xml"), "application/xbel+xml"); 13 | 14 | // direct match with a q parameter 15 | assertEquals(mimeParse.bestMatch(["application/xbel+xml", "application/xml"], 16 | "application/xbel+xml;q=1"), "application/xbel+xml"); 17 | 18 | // direct match of our second choice with a q parameter 19 | assertEquals(mimeParse.bestMatch(["application/xbel+xml", "application/xml"], 20 | "application/xml;q=1"), "application/xml"); 21 | 22 | // match using a subtype wildcard 23 | assertEquals(mimeParse.bestMatch(["application/xbel+xml", "application/xml"], 24 | "application/*;q=1"), "application/xml"); 25 | 26 | // match using a type wildcard 27 | assertEquals(mimeParse.bestMatch(["application/xbel+xml", "application/xml"], 28 | "*/*"), "application/xml"); 29 | 30 | // match using a type versus a lower weighted subtype 31 | assertEquals(mimeParse.bestMatch(["application/xbel+xml", "text/xml"], 32 | "text/*;q=0.5,*/*;q=0.1"), "text/xml"); 33 | 34 | // fail to match anything 35 | assertEquals(mimeParse.bestMatch(["application/xbel+xml", "text/xml"], 36 | "text/html,application/atom+xml; q=0.9"), null); 37 | 38 | // common AJAX scenario 39 | assertEquals(mimeParse.bestMatch(["application/json", "text/html"], 40 | "application/json,text/javascript, */*"), "application/json"); 41 | 42 | // verify fitness ordering 43 | assertEquals(mimeParse.bestMatch(["application/json", "text/html"], 44 | "application/json,text/html;q=0.9"), "application/json"); 45 | } -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/module.ceylon: -------------------------------------------------------------------------------- 1 | native("jvm") 2 | module test.net.gyokuro.core "0.4-SNAPSHOT" { 3 | value ceylonVersion = "1.3.4-SNAPSHOT"; 4 | 5 | import net.gyokuro.core "0.4-SNAPSHOT"; 6 | import ceylon.test ceylonVersion; 7 | import ceylon.logging ceylonVersion; 8 | import ceylon.html ceylonVersion; 9 | import ceylon.http.client ceylonVersion; 10 | import ceylon.uri ceylonVersion; 11 | } 12 | -------------------------------------------------------------------------------- /test-source/test/net/gyokuro/core/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package test.net.gyokuro.core; 2 | --------------------------------------------------------------------------------