commit b41479bf7f22e54049e3a0f1ea4b71dc79a38f7b Author: Jörg Prante Date: Fri Nov 25 00:06:17 2016 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e42bcc --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/data +/work +/logs +/.idea +/target +.DS_Store +*.iml +/.settings +/.classpath +/.project +/.gradle +/build \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..57000f7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +sudo: false +language: java +jdk: + - oraclejdk8 +cache: + directories: + - $HOME/.m2 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..951580c --- /dev/null +++ b/README.adoc @@ -0,0 +1,8 @@ +# xbib Contextual Query Language Compiler + +image:https://api.travis-ci.org/xbib/cql.svg[title="Build status", link="https://travis-ci.org/xbib/cql/"] +image:https://img.shields.io/sonar/http/nemo.sonarqube.com/org.xbib%3Acql/coverage.svg?style=flat-square[title="Coverage", link="https://sonarqube.com/dashboard/index?id=org.xbib%3Acql"] +image:https://maven-badges.herokuapp.com/maven-central/org.xbib/cql/badge.svg[title="Maven Central", link="http://search.maven.org/#search%7Cga%7C1%7Cxbib%20cql"] +image:https://img.shields.io/badge/License-Apache%202.0-blue.svg[title="Apache License 2.0", link="https://opensource.org/licenses/Apache-2.0"] +image:https://img.shields.io/twitter/url/https/twitter.com/xbib.svg?style=social&label=Follow%20%40xbib[title="Twitter", link="https://twitter.com/xbib"] +image:https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif[title="PayPal", link="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=GVHFQYZ9WZ8HG"] diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..436e3e6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'org.xbib.gradle.plugin.jflex' version '1.1.0' + id 'org.xbib.gradle.plugin.jacc' version '1.1.3' + id "org.sonarqube" version '2.2' +} + +group = 'org.xbib' +version = '1.0.0' + +apply plugin: 'java' +apply plugin: 'maven' +apply plugin: 'signing' +apply plugin: 'findbugs' +apply plugin: 'pmd' +apply plugin: 'checkstyle' +apply plugin: 'jacoco' + +repositories { + mavenCentral() +} + +configurations { + wagon +} + +dependencies { + compile 'org.xbib:content-core:1.0.6' + testCompile 'junit:junit:4.12' + wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10' +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-profile" << "compact1" +} + +test { + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + } +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier 'sources' + from sourceSets.main.allSource +} +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' +} +artifacts { + archives sourcesJar, javadocJar +} +if (project.hasProperty('signing.keyId')) { + signing { + sign configurations.archives + } +} + +apply from: 'gradle/ext.gradle' +apply from: 'gradle/publish.gradle' +apply from: 'gradle/sonarqube.gradle' diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..52fe33c --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/ext.gradle b/gradle/ext.gradle new file mode 100644 index 0000000..9b3403e --- /dev/null +++ b/gradle/ext.gradle @@ -0,0 +1,8 @@ +ext { + user = 'xbib' + projectName = 'cql' + projectDescription = 'Contextual Query Language compiler for Java' + scmUrl = 'https://github.com/xbib/cql' + scmConnection = 'scm:git:git://github.com/xbib/cql.git' + scmDeveloperConnection = 'scm:git:git://github.com/xbib/cql.git' +} diff --git a/gradle/publish.gradle b/gradle/publish.gradle new file mode 100644 index 0000000..caf0531 --- /dev/null +++ b/gradle/publish.gradle @@ -0,0 +1,66 @@ + +task xbibUpload(type: Upload, dependsOn: build) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('xbibUsername')) { + mavenDeployer { + configuration = configurations.wagon + repository(url: uri('scpexe://xbib.org/repository')) { + authentication(userName: xbibUsername, privateKey: xbibPrivateKey) + } + } + } + } +} + +task sonatypeUpload(type: Upload, dependsOn: build) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('ossrhUsername')) { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + repository(url: uri(ossrhReleaseUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + snapshotRepository(url: uri(ossrhSnapshotUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + pom.project { + groupId project.group + artifactId project.name + version project.version + name project.name + description projectDescription + packaging 'jar' + inceptionYear '2016' + url scmUrl + organization { + name 'xbib' + url 'http://xbib.org' + } + developers { + developer { + id user + name 'Jörg Prante' + email 'joergprante@gmail.com' + url 'https://github.com/jprante' + } + } + scm { + url scmUrl + connection scmConnection + developerConnection scmDeveloperConnection + } + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } + } +} diff --git a/gradle/sonarqube.gradle b/gradle/sonarqube.gradle new file mode 100644 index 0000000..6d4c3fa --- /dev/null +++ b/gradle/sonarqube.gradle @@ -0,0 +1,41 @@ +tasks.withType(FindBugs) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = false + } +} +tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} +tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} + +jacocoTestReport { + reports { + xml.enabled true + csv.enabled false + xml.destination "${buildDir}/reports/jacoco-xml" + html.destination "${buildDir}/reports/jacoco-html" + } +} + +sonarqube { + properties { + property "sonar.projectName", "${project.group} ${project.name}" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.java.coveragePlugin", "jacoco" + property "sonar.junit.reportsPath", "build/test-results/test/" + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ca78035 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..4464a72 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Nov 24 21:44:19 CET 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..27309d9 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f6d5974 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..a6041cf --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cql' diff --git a/src/main/jacc/org/xbib/cql/CQL.jacc b/src/main/jacc/org/xbib/cql/CQL.jacc new file mode 100644 index 0000000..205d6f2 --- /dev/null +++ b/src/main/jacc/org/xbib/cql/CQL.jacc @@ -0,0 +1 @@ +%{ import java.io.StringReader; %} %class CQLParser %interface CQLTokens %package org.xbib.cql %token NL %token LPAR RPAR SLASH %token AND OR NOT PROX %token SORTBY %token GE LE NE EXACT LT GT EQ NAMEDCOMPARITORS %token SIMPLESTRING QUOTEDSTRING %token INTEGER %token FLOAT %left OR %left AND PROX %left NOT %type sortedQuery %type cqlQuery %type sortSpec %type singleSpec %type prefixAssignment %type scopedClause %type booleanGroup %type searchClause %type comparitor %type relation %type modifier %type modifierList %type index %type term %type identifier %type simpleName %type quotedString %start cql %% /* CQL 1.2 */ cql: sortedQuery { this.cql = $1; $$ = this.cql; } ; /* sortedQuery ::= prefixAssignment sortedQuery | scopedClause 'sortby' sortSpec | scopedClause --> sortedQuey ::= cqlQuery 'sortby' sortSpec | cqlQuery */ sortedQuery: cqlQuery SORTBY sortSpec { $$ = new SortedQuery($1, $3); } | cqlQuery { $$ = new SortedQuery($1); } ; /* sortSpec ::= sortSpec singleSpec | singleSpec */ sortSpec: sortSpec singleSpec { $$ = new SortSpec($1, $2); } | singleSpec { $$ = new SortSpec($1); } ; /* singleSpec ::= index modifierList | index */ singleSpec: index modifierList { $$ = new SingleSpec($1, $2); } | index { $$ = new SingleSpec($1); } ; /* cqlQuery ::= prefixAssignment cqlQuery | scopedClause */ cqlQuery: prefixAssignment cqlQuery { $$ = new Query($1, $2); } | scopedClause { $$ = new Query($1); } ; /* prefixAssignment ::= '>' prefix '=' uri | '>' uri */ prefixAssignment: GT term EQ term { $$ = new PrefixAssignment($2, $4); } | GT term { $$ = new PrefixAssignment($2); } ; /* scopedClause ::= scopedClause booleanGroup searchClause | searchClause */ scopedClause: scopedClause booleanGroup searchClause { $$ = new ScopedClause($1, $2, $3 ); } | searchClause { $$ = new ScopedClause($1); } ; /* booleanGroup ::= boolean modifierList | boolean */ booleanGroup: boolean modifierList { $$ = new BooleanGroup(BooleanOperator.forToken($1), $2); } | boolean { $$ = new BooleanGroup(BooleanOperator.forToken($1)); } ; /* boolean ::= 'and' | 'or' | 'not' | 'prox' */ boolean: AND | OR | NOT | PROX ; /* searchClause ::= '(' cqlQuery ')' | index relation searchTerm | searchTerm */ searchClause: LPAR cqlQuery RPAR { $$ = new SearchClause($2); } | index relation term { $$ = new SearchClause($1, $2, $3); } | term { $$ = new SearchClause($1); } ; /* relation ::= comparitor modifierList | comparitor */ relation: comparitor modifierList { $$ = new Relation($1, $2); } | comparitor { $$ = new Relation($1); } ; /* comparitor ::= comparitorSymbol | namedComparitor */ comparitor: comparitorSymbol { $$ = Comparitor.forToken($1); } | namedComparitor { $$ = Comparitor.forToken($1); } ; comparitorSymbol: EQ | LT | GT | GE | LE | NE | EXACT ; namedComparitor: NAMEDCOMPARITORS ; /* modifierList ::= modifierList modifier | modifier */ modifierList: modifierList modifier { $$ = new ModifierList($1,$2); } | modifier { $$ = new ModifierList($1); } ; /* modifier ::= '/' modifierName [comparitorSymbol modifierValue] */ modifier: SLASH simpleName comparitorSymbol term { $$ = new Modifier($2, Comparitor.forToken($3), $4); } | SLASH simpleName { $$ = new Modifier($2); } ; index: simpleName { $$ = new Index($1); } ; /* term ::= identifier | 'and' | 'or' | 'not' | 'prox' */ term: identifier { $$ = new Term($1); } | boolean { $$ = new Term(BooleanOperator.forToken($1).getToken()); } | INTEGER { $$ = new Term($1); } | FLOAT { $$ = new Term($1); } ; /* identifier ::= simpleName | quotedString */ identifier: simpleName { $$ = new Identifier($1); } | quotedString { $$ = new Identifier($1); } ; simpleName: SIMPLESTRING { $$ = new SimpleName($1); } ; quotedString: QUOTEDSTRING { $$ = $1; } ; %% private CQLLexer lexer; private String input; private SortedQuery cql; public CQLParser(String input) { this.input = input; this.lexer = new CQLLexer(new StringReader(input)); lexer.nextToken(); } public void yyerror (String error) { throw new SyntaxException("CQL syntax error at " + "[" + lexer.getLine() + "," + lexer.getColumn() + "] in\"" + input + "\": " + (yyerrno >= 0 ? yyerrmsgs[yyerrno] : error) + ": " + lexer.getSemantic()); } public SortedQuery getCQLQuery() { return cql; } \ No newline at end of file diff --git a/src/main/java/org/xbib/cql/AbstractNode.java b/src/main/java/org/xbib/cql/AbstractNode.java new file mode 100644 index 0000000..715d662 --- /dev/null +++ b/src/main/java/org/xbib/cql/AbstractNode.java @@ -0,0 +1,26 @@ +package org.xbib.cql; + +/** + * This abstract node class is the base class for the CQL abstract syntax tree. + */ +public abstract class AbstractNode implements Node { + + /** + * Try to accept this node by a visitor. + * + * @param visitor the visitor + */ + @Override + public abstract void accept(Visitor visitor); + + /** + * Compare this node to another node. + */ + @Override + public int compareTo(Node object) { + if (this == object) { + return 0; + } + return toString().compareTo(object.toString()); + } +} diff --git a/src/main/java/org/xbib/cql/BooleanGroup.java b/src/main/java/org/xbib/cql/BooleanGroup.java new file mode 100644 index 0000000..b8bfe62 --- /dev/null +++ b/src/main/java/org/xbib/cql/BooleanGroup.java @@ -0,0 +1,38 @@ +package org.xbib.cql; + +/** + * Abstract syntax tree of CQL - Boolean Group. + */ +public class BooleanGroup extends AbstractNode { + + private BooleanOperator op; + private ModifierList modifiers; + + BooleanGroup(BooleanOperator op, ModifierList modifiers) { + this.op = op; + this.modifiers = modifiers; + } + + BooleanGroup(BooleanOperator op) { + this.op = op; + } + + public BooleanOperator getOperator() { + return op; + } + + public ModifierList getModifierList() { + return modifiers; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return op != null && modifiers != null ? op + modifiers.toString() + : op != null ? op.toString() : null; + } +} diff --git a/src/main/java/org/xbib/cql/BooleanOperator.java b/src/main/java/org/xbib/cql/BooleanOperator.java new file mode 100644 index 0000000..2585161 --- /dev/null +++ b/src/main/java/org/xbib/cql/BooleanOperator.java @@ -0,0 +1,80 @@ +package org.xbib.cql; + +import java.util.HashMap; +import java.util.Map; + +/** + * Abstract syntax tree of CQL - boolean operator enumeration. + */ +public enum BooleanOperator { + + AND("and"), + OR("or"), + NOT("not"), + PROX("prox"); + /** + * Token/operator map. + */ + private static Map tokenMap; + /** + * Operator/token map. + */ + private static Map opMap; + private String token; + + /** + * Creates a new Operator object. + * + * @param token the operator token + */ + BooleanOperator(String token) { + this.token = token; + map(token, this); + } + + /** + * Map token to operator. + * + * @param token the token + * @param op the operator + */ + private static void map(String token, BooleanOperator op) { + if (tokenMap == null) { + tokenMap = new HashMap<>(); + } + tokenMap.put(token, op); + if (opMap == null) { + opMap = new HashMap<>(); + } + opMap.put(op, token); + } + + /** + * Get token. + * + * @return the token + */ + public String getToken() { + return token; + } + + /** + * Get operator for token. + * + * @param token the token + * @return the operator + */ + static BooleanOperator forToken(Object token) { + return tokenMap.get(token.toString().toLowerCase()); + } + + /** + * Write operator representation. + * + * @return the operator token + */ + @Override + public String toString() { + return token; + } +} diff --git a/src/main/java/org/xbib/cql/CQLGenerator.java b/src/main/java/org/xbib/cql/CQLGenerator.java new file mode 100644 index 0000000..de0c477 --- /dev/null +++ b/src/main/java/org/xbib/cql/CQLGenerator.java @@ -0,0 +1,224 @@ +package org.xbib.cql; + +import org.xbib.cql.model.CQLQueryModel; +import org.xbib.cql.model.Facet; +import org.xbib.cql.model.Filter; +import org.xbib.cql.model.Option; + +/** + * This is a CQL abstract syntax tree generator useful for normalizing CQL queries. + */ +public final class CQLGenerator implements Visitor { + + /** + * helper for managing our CQL query model (facet/filter/option contexts, breadcrumb trails etc.). + */ + private CQLQueryModel model; + + /** + * A replacement string. + */ + private String replacementString; + + /** + * String to be replaced. + */ + private String stringToBeReplaced; + + public CQLGenerator() { + this.replacementString = null; + this.stringToBeReplaced = null; + this.model = new CQLQueryModel(); + } + + public CQLGenerator model(CQLQueryModel model) { + this.model = model; + return this; + } + + public CQLQueryModel getModel() { + return model; + } + + public String getResult() { + return model.getQuery(); + } + + @Override + public void visit(SortedQuery node) { + if (node.getSortSpec() != null) { + node.getSortSpec().accept(this); + } + if (node.getQuery() != null) { + node.getQuery().accept(this); + } + model.setQuery(node.toString()); + } + + @Override + public void visit(Query node) { + if (node.getPrefixAssignments() != null) { + for (PrefixAssignment assignment : node.getPrefixAssignments()) { + assignment.accept(this); + } + } + if (node.getQuery() != null) { + node.getQuery().accept(this); + } + if (node.getScopedClause() != null) { + node.getScopedClause().accept(this); + } + } + + @Override + public void visit(SortSpec node) { + if (node.getSingleSpec() != null) { + node.getSingleSpec().accept(this); + } + if (node.getSortSpec() != null) { + node.getSortSpec().accept(this); + } + } + + @Override + public void visit(SingleSpec node) { + if (node.getIndex() != null) { + node.getIndex().accept(this); + } + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + } + + @Override + public void visit(PrefixAssignment node) { + node.getPrefix().accept(this); + node.getURI().accept(this); + } + + @Override + public void visit(ScopedClause node) { + if (node.getScopedClause() != null) { + node.getScopedClause().accept(this); + } + node.getSearchClause().accept(this); + if (node.getBooleanGroup() != null) { + node.getBooleanGroup().accept(this); + BooleanOperator op = node.getBooleanGroup().getOperator(); + checkFilter(op, node); + checkFilter(op, node.getScopedClause()); + } + } + + @Override + public void visit(BooleanGroup node) { + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + } + + @Override + public void visit(SearchClause node) { + if (node.getQuery() != null) { + node.getQuery().accept(this); + } + if (node.getTerm() != null) { + node.getTerm().accept(this); + } + if (node.getIndex() != null) { + node.getIndex().accept(this); + String context = node.getIndex().getContext(); + if (CQLQueryModel.FACET_INDEX_NAME.equals(context)) { + Facet facet = new Facet<>(node.getIndex().getName()); + facet.setValue(node.getTerm()); + model.addFacet(facet); + } else if (CQLQueryModel.OPTION_INDEX_NAME.equals(context)) { + Option option = new Option<>(); + option.setName(node.getIndex().getName()); + option.setValue(node.getTerm()); + model.addOption(option); + } + } + if (node.getRelation() != null) { + node.getRelation().accept(this); + } + } + + @Override + public void visit(Relation node) { + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + } + + @Override + public void visit(Modifier node) { + if (node.getTerm() != null) { + node.getTerm().accept(this); + } + if (node.getName() != null) { + node.getName().accept(this); + } + } + + @Override + public void visit(ModifierList node) { + for (Modifier modifier : node.getModifierList()) { + modifier.accept(this); + } + } + + @Override + public void visit(Term node) { + if (replacementString != null && stringToBeReplaced.equals(node.getValue())) { + node.setValue(replacementString); + } + } + + @Override + public void visit(Identifier node) { + } + + @Override + public void visit(SimpleName node) { + } + + @Override + public void visit(Index node) { + } + + /** + * Write a substitution query, for example when a term has been + * suggested to be replaced by another term. + * + * @param oldTerm the term to be replaced + * @param newTerm the replacement term + * @return the new query with the term replaced + */ + public synchronized String writeSubstitutedForm(String oldTerm, String newTerm) { + this.stringToBeReplaced = oldTerm; + this.replacementString = newTerm; + CQLParser parser = new CQLParser(model.getQuery()); + parser.parse(); + parser.getCQLQuery().accept(this); + String result = model.getQuery(); + this.stringToBeReplaced = null; + this.replacementString = null; + return result; + } + + public String withBreadcrumbs() { + return model.toCQL(); + } + + private void checkFilter(BooleanOperator op, ScopedClause node) { + if (node.getSearchClause().getIndex() != null + && CQLQueryModel.FILTER_INDEX_NAME.equals(node.getSearchClause().getIndex().getContext())) { + String filtername = node.getSearchClause().getIndex().getName(); + Comparitor filterop = node.getSearchClause().getRelation().getComparitor(); + Term filterterm = node.getSearchClause().getTerm(); + Filter filter2 = new Filter<>(filtername, filterterm, filterop); + model.addFilter(op, filter2); + } + } +} diff --git a/src/main/java/org/xbib/cql/Comparitor.java b/src/main/java/org/xbib/cql/Comparitor.java new file mode 100644 index 0000000..7be6c3b --- /dev/null +++ b/src/main/java/org/xbib/cql/Comparitor.java @@ -0,0 +1,80 @@ +package org.xbib.cql; + +import java.util.HashMap; + +/** + * CQL operators. + */ +public enum Comparitor { + + EQUALS("="), + GREATER(">"), + GREATER_EQUALS(">="), + LESS("<"), + LESS_EQUALS("<="), + NOT_EQUALS("<>"), + WITHIN("within"), + CQLWITHIN("cql.within"), + ENCLOSES("encloses"), + CQLENCLOSES("cql.encloses"), + ADJ("adj"), + CQLADJ("cql.adj"), + ALL("all"), + CQLALL("cql.all"), + ANY("any"), + CQLANY("cql.any"); + private static HashMap tokenMap; + private String token; + + /** + * Creates a new Operator object. + * + * @param token the operator token + */ + private Comparitor(String token) { + this.token = token; + map(token, this); + } + + /** + * Map token to operator + * + * @param token the token + * @param op the operator + */ + private static void map(String token, Comparitor op) { + if (tokenMap == null) { + tokenMap = new HashMap<>(); + } + tokenMap.put(token, op); + } + + /** + * Get token. + * + * @return the token + */ + public String getToken() { + return token; + } + + /** + * Get operator for token. + * + * @param token the token + * @return the operator + */ + static Comparitor forToken(Object token) { + return tokenMap.get(token.toString()); + } + + /** + * Write operator representation. + * + * @return the operator token + */ + @Override + public String toString() { + return token; + } +} diff --git a/src/main/java/org/xbib/cql/Identifier.java b/src/main/java/org/xbib/cql/Identifier.java new file mode 100644 index 0000000..39be384 --- /dev/null +++ b/src/main/java/org/xbib/cql/Identifier.java @@ -0,0 +1,33 @@ +package org.xbib.cql; + +/** + * An Identifier is a SimpleName or a String in double quotes. + */ +public class Identifier extends AbstractNode { + + private String value; + private boolean quoted; + + public Identifier(String value) { + this.value = value; + this.quoted = true; + } + + public Identifier(SimpleName name) { + this.value = name.getName(); + this.quoted = false; + } + + public String getValue() { + return value; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return value != null && quoted ? "\"" + value.replaceAll("\"", "\\\\\"") + "\"" : value; + } +} diff --git a/src/main/java/org/xbib/cql/Index.java b/src/main/java/org/xbib/cql/Index.java new file mode 100644 index 0000000..16e22d9 --- /dev/null +++ b/src/main/java/org/xbib/cql/Index.java @@ -0,0 +1,51 @@ +package org.xbib.cql; + +/** + * Abstract syntax tree of CQL - Index. + * The Index consists of context and name + * The default context is "cql" and is of the same concept like a namespace. + */ +public class Index extends AbstractNode { + + private String context; + private String name; + + public Index(String name) { + this.name = name; + int pos = name.indexOf('.'); + if (pos > 0) { + this.context = name.substring(0, pos); + this.name = name.substring(pos + 1); + } + } + + public Index(SimpleName name) { + this(name.getName()); + } + + /** + * @return the context of the index + */ + public String getContext() { + return context; + } + + /** + * Get the name of the index + * + * @return the name of the index + */ + public String getName() { + return name; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return context != null ? context + "." + name : name; + } + +} diff --git a/src/main/java/org/xbib/cql/Modifier.java b/src/main/java/org/xbib/cql/Modifier.java new file mode 100644 index 0000000..9212862 --- /dev/null +++ b/src/main/java/org/xbib/cql/Modifier.java @@ -0,0 +1,45 @@ +package org.xbib.cql; + +/** + * Modifier. + */ +public class Modifier extends AbstractNode { + + private SimpleName name; + + private Comparitor op; + + private Term term; + + public Modifier(SimpleName name, Comparitor op, Term term) { + this.name = name; + this.op = op; + this.term = term; + } + + public Modifier(SimpleName name) { + this.name = name; + } + + public SimpleName getName() { + return name; + } + + public Comparitor getOperator() { + return op; + } + + public Term getTerm() { + return term; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return "/" + (term != null ? name.toString() + op + term : name.toString()); + } + +} diff --git a/src/main/java/org/xbib/cql/ModifierList.java b/src/main/java/org/xbib/cql/ModifierList.java new file mode 100644 index 0000000..1d1c102 --- /dev/null +++ b/src/main/java/org/xbib/cql/ModifierList.java @@ -0,0 +1,39 @@ +package org.xbib.cql; + +import java.util.LinkedList; +import java.util.List; + +/** + * Modifier list. This is a recursive data structure with a Modifier and optionally a ModifierList. + */ +public class ModifierList extends AbstractNode { + + private List modifierList = new LinkedList<>(); + + public ModifierList(ModifierList modifiers, Modifier modifier) { + modifierList.addAll(modifiers.modifierList); + modifierList.add(modifier); + } + + public ModifierList(Modifier modifier) { + modifierList.add(modifier); + } + + public List getModifierList() { + return modifierList; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Modifier m : modifierList) { + sb.append(m.toString()); + } + return sb.toString(); + } + +} diff --git a/src/main/java/org/xbib/cql/Node.java b/src/main/java/org/xbib/cql/Node.java new file mode 100644 index 0000000..67fb028 --- /dev/null +++ b/src/main/java/org/xbib/cql/Node.java @@ -0,0 +1,14 @@ +package org.xbib.cql; + +/** + * This is a node interface for the CQL abstract syntax tree. + */ +public interface Node extends Comparable { + + /** + * Accept a visitor on this node. + * + * @param visitor the visitor + */ + void accept(Visitor visitor); +} diff --git a/src/main/java/org/xbib/cql/PrefixAssignment.java b/src/main/java/org/xbib/cql/PrefixAssignment.java new file mode 100644 index 0000000..56ceb6a --- /dev/null +++ b/src/main/java/org/xbib/cql/PrefixAssignment.java @@ -0,0 +1,38 @@ +package org.xbib.cql; + +/** + * Prefix assignment. + */ +public class PrefixAssignment extends AbstractNode { + + private Term prefix; + + private Term uri; + + public PrefixAssignment(Term prefix, Term uri) { + this.prefix = prefix; + this.uri = uri; + } + + public PrefixAssignment(Term uri) { + this.uri = uri; + } + + public Term getPrefix() { + return prefix; + } + + public Term getURI() { + return uri; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return "> " + prefix + " = " + uri; + } + +} diff --git a/src/main/java/org/xbib/cql/Query.java b/src/main/java/org/xbib/cql/Query.java new file mode 100644 index 0000000..69f09a6 --- /dev/null +++ b/src/main/java/org/xbib/cql/Query.java @@ -0,0 +1,57 @@ +package org.xbib.cql; + +import java.util.LinkedList; +import java.util.List; + +/** + * CQL query. + */ +public class Query extends AbstractNode { + + private List prefixes = new LinkedList<>(); + + private Query query; + + private ScopedClause clause; + + Query(PrefixAssignment assignment, Query query) { + prefixes.add(assignment); + this.query = query; + } + + Query(ScopedClause clause) { + this.clause = clause; + } + + public List getPrefixAssignments() { + return prefixes; + } + + public Query getQuery() { + return query; + } + + public ScopedClause getScopedClause() { + return clause; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (PrefixAssignment assignment : prefixes) { + sb.append(assignment.toString()).append(' '); + } + if (query != null) { + sb.append(query); + } + if (clause != null) { + sb.append(clause); + } + return sb.toString(); + } + +} diff --git a/src/main/java/org/xbib/cql/QueryFacet.java b/src/main/java/org/xbib/cql/QueryFacet.java new file mode 100644 index 0000000..49fbf9a --- /dev/null +++ b/src/main/java/org/xbib/cql/QueryFacet.java @@ -0,0 +1,21 @@ +package org.xbib.cql; + +/** + * Query facet. + */ +public interface QueryFacet extends QueryOption { + /** + * The size of the facet. + * + * @return the facet size + */ + int getSize(); + + /** + * Get the filter name which must be used for filtering facet entries. + * + * @return the filter name + */ + String getFilterName(); + +} diff --git a/src/main/java/org/xbib/cql/QueryFilter.java b/src/main/java/org/xbib/cql/QueryFilter.java new file mode 100644 index 0000000..643797f --- /dev/null +++ b/src/main/java/org/xbib/cql/QueryFilter.java @@ -0,0 +1,7 @@ +package org.xbib.cql; + +/** + * A Filter for a query. + */ +public interface QueryFilter extends QueryOption { +} diff --git a/src/main/java/org/xbib/cql/QueryOption.java b/src/main/java/org/xbib/cql/QueryOption.java new file mode 100644 index 0000000..ef653db --- /dev/null +++ b/src/main/java/org/xbib/cql/QueryOption.java @@ -0,0 +1,16 @@ +package org.xbib.cql; + +/** + * Qery option. + * @param parameter type + */ +public interface QueryOption { + + void setName(String name); + + String getName(); + + void setValue(V value); + + V getValue(); +} diff --git a/src/main/java/org/xbib/cql/Relation.java b/src/main/java/org/xbib/cql/Relation.java new file mode 100644 index 0000000..b2becf8 --- /dev/null +++ b/src/main/java/org/xbib/cql/Relation.java @@ -0,0 +1,39 @@ +package org.xbib.cql; + +/** + * Relation to a ModifierList. + */ +public class Relation extends AbstractNode { + + private Comparitor comparitor; + private ModifierList modifiers; + + public Relation(Comparitor comparitor, ModifierList modifiers) { + this.comparitor = comparitor; + this.modifiers = modifiers; + } + + public Relation(Comparitor comparitor) { + this.comparitor = comparitor; + } + + public Comparitor getComparitor() { + return comparitor; + } + + public ModifierList getModifierList() { + return modifiers; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return modifiers != null ? comparitor + modifiers.toString() + : comparitor.toString(); + } + +} diff --git a/src/main/java/org/xbib/cql/ScopedClause.java b/src/main/java/org/xbib/cql/ScopedClause.java new file mode 100644 index 0000000..74c979c --- /dev/null +++ b/src/main/java/org/xbib/cql/ScopedClause.java @@ -0,0 +1,50 @@ +package org.xbib.cql; + +/** + * Scoped clause. This is a recursive data structure with a SearchClause and + * optionally a ScopedClause. + * SearchClause and ScopedClause are connected through a BooleanGroup. + */ +public class ScopedClause extends AbstractNode { + + private ScopedClause clause; + private BooleanGroup booleangroup; + private SearchClause search; + + ScopedClause(ScopedClause clause, BooleanGroup bg, SearchClause search) { + this.clause = clause; + this.booleangroup = bg; + this.search = search; + } + + ScopedClause(SearchClause search) { + this.search = search; + } + + public ScopedClause getScopedClause() { + return clause; + } + + public BooleanGroup getBooleanGroup() { + return booleangroup; + } + + public SearchClause getSearchClause() { + return search; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + String s = search.toString(); + boolean hasQuery = s.length() > 0; + return clause != null && hasQuery ? clause + " " + booleangroup + " " + search + : clause != null ? clause.toString() + : hasQuery ? search.toString() + : ""; + } +} diff --git a/src/main/java/org/xbib/cql/SearchClause.java b/src/main/java/org/xbib/cql/SearchClause.java new file mode 100644 index 0000000..eafcc96 --- /dev/null +++ b/src/main/java/org/xbib/cql/SearchClause.java @@ -0,0 +1,62 @@ +package org.xbib.cql; + +import org.xbib.cql.model.CQLQueryModel; + +/** + * Search clause. + */ +public class SearchClause extends AbstractNode { + + private Query query; + private Index index; + private Relation relation; + private Term term; + + SearchClause(Query query) { + this.query = query; + } + + SearchClause(Index index, Relation relation, Term term) { + this.index = index; + this.relation = relation; + this.term = term; + } + + SearchClause(Term term) { + this.term = term; + } + + public Query getQuery() { + return query; + } + + public Index getIndex() { + return index; + } + + public Term getTerm() { + return term; + } + + public Relation getRelation() { + return relation; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + /** + * @return CQL string + */ + @Override + public String toString() { + return query != null && query.toString().length() > 0 ? "(" + query + ")" + : query != null ? "" + : index != null && !CQLQueryModel.isVisible(index.getContext()) ? "" + : index != null ? index + " " + relation + " " + term + : term != null ? term.toString() + : null; + } + +} diff --git a/src/main/java/org/xbib/cql/SimpleName.java b/src/main/java/org/xbib/cql/SimpleName.java new file mode 100644 index 0000000..394e33e --- /dev/null +++ b/src/main/java/org/xbib/cql/SimpleName.java @@ -0,0 +1,28 @@ +package org.xbib.cql; + +/** + * A SimpleName consists of a String which is not surrounded by double quotes. + */ +public class SimpleName extends AbstractNode { + + private String name; + + public SimpleName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return name; + } + +} diff --git a/src/main/java/org/xbib/cql/SingleSpec.java b/src/main/java/org/xbib/cql/SingleSpec.java new file mode 100644 index 0000000..482b287 --- /dev/null +++ b/src/main/java/org/xbib/cql/SingleSpec.java @@ -0,0 +1,37 @@ +package org.xbib.cql; + +/** + * Single spec. + */ +public class SingleSpec extends AbstractNode { + + private Index index; + private ModifierList modifiers; + + public SingleSpec(Index index, ModifierList modifiers) { + this.index = index; + this.modifiers = modifiers; + } + + public SingleSpec(Index index) { + this.index = index; + } + + public Index getIndex() { + return index; + } + + public ModifierList getModifierList() { + return modifiers; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return index + (modifiers != null ? modifiers.toString() : ""); + } + +} diff --git a/src/main/java/org/xbib/cql/SortSpec.java b/src/main/java/org/xbib/cql/SortSpec.java new file mode 100644 index 0000000..afca6a1 --- /dev/null +++ b/src/main/java/org/xbib/cql/SortSpec.java @@ -0,0 +1,38 @@ +package org.xbib.cql; + +/** + * Abstract syntax tree of CQL, the sort specification. + */ +public class SortSpec extends AbstractNode { + + private SortSpec sortspec; + private SingleSpec spec; + + public SortSpec(SortSpec sortspec, SingleSpec spec) { + this.sortspec = sortspec; + this.spec = spec; + } + + public SortSpec(SingleSpec spec) { + this.spec = spec; + } + + public SortSpec getSortSpec() { + return sortspec; + } + + public SingleSpec getSingleSpec() { + return spec; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return (sortspec != null ? sortspec + " " : "") + spec; + } + +} diff --git a/src/main/java/org/xbib/cql/SortedQuery.java b/src/main/java/org/xbib/cql/SortedQuery.java new file mode 100644 index 0000000..570bf6f --- /dev/null +++ b/src/main/java/org/xbib/cql/SortedQuery.java @@ -0,0 +1,39 @@ +package org.xbib.cql; + +/** + * Sorted query. + */ +public class SortedQuery extends AbstractNode { + + private Query query; + + private SortSpec spec; + + SortedQuery(Query query, SortSpec spec) { + this.query = query; + this.spec = spec; + } + + SortedQuery(Query query) { + this.query = query; + } + + public Query getQuery() { + return query; + } + + public SortSpec getSortSpec() { + return spec; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return query != null && spec != null ? query + " sortby " + spec + : query != null ? query.toString() : ""; + } +} \ No newline at end of file diff --git a/src/main/java/org/xbib/cql/SyntaxException.java b/src/main/java/org/xbib/cql/SyntaxException.java new file mode 100644 index 0000000..f8e4ed5 --- /dev/null +++ b/src/main/java/org/xbib/cql/SyntaxException.java @@ -0,0 +1,25 @@ +package org.xbib.cql; + +/** + * CQL Syntax exception. + */ +public class SyntaxException extends RuntimeException { + /** + * Creates a new SyntaxException object. + * + * @param msg the message for this syntax exception + */ + public SyntaxException(String msg) { + super(msg); + } + + /** + * Creates a new SyntaxException object. + * + * @param msg the message for this syntax exception + * @param t the throwable for this syntax exception + */ + public SyntaxException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/src/main/java/org/xbib/cql/Term.java b/src/main/java/org/xbib/cql/Term.java new file mode 100644 index 0000000..a60cc2a --- /dev/null +++ b/src/main/java/org/xbib/cql/Term.java @@ -0,0 +1,147 @@ +package org.xbib.cql; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** + * A CQL Term. + */ +public class Term extends AbstractNode { + + private static final TimeZone tz = TimeZone.getTimeZone("GMT"); + private static final String ISO_FORMAT_SECONDS = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final String ISO_FORMAT_DAYS = "yyyy-MM-dd"; + + private String value; + private Long longvalue; + private Double doublevalue; + private Identifier identifier; + private Date datevalue; + private SimpleName name; + + public Term(String value) { + this.value = value; + try { + // check for hidden dates. CQL does not support ISO dates. + this.datevalue = parseDateISO(value); + this.value = null; + } catch (Exception e) { + + } + } + + public Term(Identifier identifier) { + this.identifier = identifier; + } + + public Term(SimpleName name) { + this.name = name; + } + + public Term(Long value) { + this.longvalue = value; + } + + public Term(Double value) { + this.doublevalue = value; + } + + /** + * Set value, useful for inline replacements + * in spellcheck suggestions + * + * @param value the value + */ + public void setValue(String value) { + this.value = value; + } + + /** + * If the value is a String it is embedded in quotation marks. + * If its a Integer or a Double it is returned without + * quotation marks. + * + * @return the value as String + */ + public String getValue() { + return longvalue != null ? Long.toString(longvalue) + : doublevalue != null ? Double.toString(doublevalue) + : value != null ? value + : identifier != null ? identifier.toString() + : name != null ? name.toString() + : null; + } + + public boolean isLong() { + return longvalue != null; + } + + public boolean isFloat() { + return doublevalue != null; + } + + public boolean isString() { + return value != null; + } + + public boolean isName() { + return name != null; + } + + public boolean isIdentifier() { + return identifier != null; + } + + public boolean isDate() { + return datevalue != null; + } + + public void accept(Visitor visitor) { + visitor.visit(this); + } + + private Date parseDateISO(String value) { + if (value == null) { + return null; + } + SimpleDateFormat sdf = new SimpleDateFormat(); + sdf.applyPattern(ISO_FORMAT_SECONDS); + sdf.setTimeZone(tz); + sdf.setLenient(true); + try { + return sdf.parse(value); + } catch (ParseException pe) { + // skip + } + sdf.applyPattern(ISO_FORMAT_DAYS); + try { + return sdf.parse(value); + } catch (ParseException pe) { + return null; + } + } + + private String formatDateISO(Date date) { + if (date == null) { + return null; + } + SimpleDateFormat sdf = new SimpleDateFormat(); + sdf.applyPattern(ISO_FORMAT_SECONDS); + sdf.setTimeZone(tz); + return sdf.format(date); + } + + @Override + public String toString() { + return longvalue != null ? Long.toString(longvalue) + : doublevalue != null ? Double.toString(doublevalue) + : datevalue != null ? formatDateISO(datevalue) + : value != null ? value.startsWith("\"") && value.endsWith("\"") ? value + : "\"" + value.replaceAll("\"", "\\\\\"") + "\"" + : identifier != null ? identifier.toString() + : name != null ? name.toString() + : null; + } +} diff --git a/src/main/java/org/xbib/cql/Visitor.java b/src/main/java/org/xbib/cql/Visitor.java new file mode 100644 index 0000000..228ad44 --- /dev/null +++ b/src/main/java/org/xbib/cql/Visitor.java @@ -0,0 +1,38 @@ +package org.xbib.cql; + +/** + * CQL abstract syntax tree visitor. + */ +public interface Visitor { + + void visit(SortedQuery node); + + void visit(Query node); + + void visit(PrefixAssignment node); + + void visit(ScopedClause node); + + void visit(BooleanGroup node); + + void visit(SearchClause node); + + void visit(Relation node); + + void visit(Modifier node); + + void visit(ModifierList node); + + void visit(Term node); + + void visit(Identifier node); + + void visit(Index node); + + void visit(SimpleName node); + + void visit(SortSpec node); + + void visit(SingleSpec node); + +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ElasticsearchFilterGenerator.java b/src/main/java/org/xbib/cql/elasticsearch/ElasticsearchFilterGenerator.java new file mode 100644 index 0000000..1543d5d --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ElasticsearchFilterGenerator.java @@ -0,0 +1,349 @@ +package org.xbib.cql.elasticsearch; + +import org.xbib.content.XContentBuilder; +import org.xbib.cql.util.DateUtil; +import org.xbib.cql.BooleanGroup; +import org.xbib.cql.BooleanOperator; +import org.xbib.cql.Comparitor; +import org.xbib.cql.Identifier; +import org.xbib.cql.Index; +import org.xbib.cql.ModifierList; +import org.xbib.cql.PrefixAssignment; +import org.xbib.cql.Query; +import org.xbib.cql.Relation; +import org.xbib.cql.ScopedClause; +import org.xbib.cql.SearchClause; +import org.xbib.cql.SimpleName; +import org.xbib.cql.SingleSpec; +import org.xbib.cql.SortSpec; +import org.xbib.cql.SortedQuery; +import org.xbib.cql.SyntaxException; +import org.xbib.cql.Term; +import org.xbib.cql.Visitor; +import org.xbib.cql.elasticsearch.ast.Expression; +import org.xbib.cql.elasticsearch.ast.Modifier; +import org.xbib.cql.elasticsearch.ast.Name; +import org.xbib.cql.elasticsearch.ast.Node; +import org.xbib.cql.elasticsearch.ast.Operator; +import org.xbib.cql.elasticsearch.ast.Token; +import org.xbib.cql.elasticsearch.ast.TokenType; +import org.xbib.cql.elasticsearch.model.ElasticsearchQueryModel; + +import java.io.IOException; +import java.util.Collection; +import java.util.Stack; + +/** + * Generate Elasticsearch filter query from CQL abstract syntax tree. + */ +public class ElasticsearchFilterGenerator implements Visitor { + + private final ElasticsearchQueryModel model; + + private Stack stack; + + private FilterGenerator filterGen; + + public ElasticsearchFilterGenerator() { + this(new ElasticsearchQueryModel()); + } + + public ElasticsearchFilterGenerator(ElasticsearchQueryModel model) { + this.model = model; + this.stack = new Stack<>(); + try { + this.filterGen = new FilterGenerator(); + } catch (IOException e) { + // ignore + } + } + + public void addOrFilter(String filterKey, Collection filterValues) { + for (String value : filterValues) { + model.addDisjunctiveFilter(filterKey, new Expression(Operator.OR_FILTER, new Name(filterKey), new Token(value)), Operator.OR); + } + } + + public void addAndFilter(String filterKey, Collection filterValues) { + for (String value : filterValues) { + model.addConjunctiveFilter(filterKey, new Expression(Operator.AND_FILTER, new Name(filterKey), new Token(value)), Operator.AND); + } + } + + public XContentBuilder getResult() throws IOException { + return filterGen.getResult(); + } + + @Override + public void visit(SortedQuery node) { + try { + filterGen.start(); + node.getQuery().accept(this); + Node querynode = stack.pop(); + if (querynode instanceof Token) { + filterGen.visit(new Expression(Operator.TERM_FILTER, new Name("cql.allIndexes"), querynode)); + } else if (querynode instanceof Expression) { + filterGen.visit(new Expression(Operator.QUERY_FILTER, (Expression) querynode)); + } + if (model.hasFilter()) { + filterGen.visit(model.getFilterExpression()); + } + filterGen.end(); + } catch (IOException e) { + throw new SyntaxException("unable to build a valid query from " + node + ", reason: " + e.getMessage(), e); + } + } + + @Override + public void visit(SortSpec node) { + if (node.getSingleSpec() != null) { + node.getSingleSpec().accept(this); + } + if (node.getSortSpec() != null) { + node.getSortSpec().accept(this); + } + } + + @Override + public void visit(SingleSpec node) { + if (node.getIndex() != null) { + node.getIndex().accept(this); + } + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + if (!stack.isEmpty()) { + model.setSort(stack); + } + } + + @Override + public void visit(Query node) { + for (PrefixAssignment assignment : node.getPrefixAssignments()) { + assignment.accept(this); + } + if (node.getScopedClause() != null) { + node.getScopedClause().accept(this); + } + } + + @Override + public void visit(PrefixAssignment node) { + node.getPrefix().accept(this); + node.getURI().accept(this); + } + + @Override + public void visit(ScopedClause node) { + if (node.getScopedClause() != null) { + node.getScopedClause().accept(this); + } + node.getSearchClause().accept(this); + if (node.getBooleanGroup() != null) { + node.getBooleanGroup().accept(this); + } + // evaluate expression + if (!stack.isEmpty() && stack.peek() instanceof Operator) { + Operator op = (Operator) stack.pop(); + if (!stack.isEmpty()) { + Node esnode = stack.pop(); + // add default context if node is a literal without a context + if (esnode instanceof Token && TokenType.STRING.equals(esnode.getType())) { + esnode = new Expression(Operator.ALL, new Name("cql.allIndexes"), esnode); + } + if (stack.isEmpty()) { + // unary expression + throw new IllegalArgumentException("unary expression not allowed, op=" + op + " node=" + esnode); + } else { + // binary expression + Node esnode2 = stack.pop(); + // add default context if node is a literal without context + if (esnode2 instanceof Token && TokenType.STRING.equals(esnode2.getType())) { + esnode2 = new Expression(Operator.ALL, new Name("cql.allIndexes"), esnode2); + } + esnode = new Expression(op, esnode2, esnode); + } + stack.push(esnode); + } + } + } + + @Override + public void visit(SearchClause node) { + if (node.getQuery() != null) { + // CQL query in parenthesis + node.getQuery().accept(this); + } + if (node.getTerm() != null) { + node.getTerm().accept(this); + } + if (node.getIndex() != null) { + node.getIndex().accept(this); + } + if (node.getRelation() != null) { + node.getRelation().accept(this); + if (node.getRelation().getModifierList() != null && node.getIndex() != null) { + // stack layout: op, list of modifiers, modifiable index + Node op = stack.pop(); + StringBuilder sb = new StringBuilder(); + Node modifier = stack.pop(); + while (modifier instanceof Modifier) { + if (sb.length() > 0) { + sb.append('.'); + } + sb.append(modifier.toString()); + modifier = stack.pop(); + } + String modifiable = sb.toString(); + stack.push(new Name(modifiable)); + stack.push(op); + } + } + // evaluate expression + if (!stack.isEmpty() && stack.peek() instanceof Operator) { + Operator op = (Operator) stack.pop(); + Node arg1 = stack.pop(); + Node arg2 = stack.pop(); + // fold two expressions if they have the same operator + boolean fold = arg1.isVisible() && arg2.isVisible() + && arg2 instanceof Expression + && ((Expression) arg2).getOperator().equals(op); + Expression expression = fold ? new Expression((Expression) arg2, arg1) : new Expression(op, arg1, arg2); + stack.push(expression); + } + } + + @Override + public void visit(BooleanGroup node) { + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + stack.push(booleanToES(node.getOperator())); + } + + @Override + public void visit(Relation node) { + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + stack.push(comparitorToES(node.getComparitor())); + } + + @Override + public void visit(ModifierList node) { + for (org.xbib.cql.Modifier modifier : node.getModifierList()) { + modifier.accept(this); + } + } + + @Override + public void visit(org.xbib.cql.Modifier node) { + Node term = null; + if (node.getTerm() != null) { + node.getTerm().accept(this); + term = stack.pop(); + } + node.getName().accept(this); + Node name = stack.pop(); + stack.push(new Modifier(name, term)); + } + + @Override + public void visit(Term node) { + stack.push(termToES(node)); + } + + @Override + public void visit(Identifier node) { + stack.push(new Name(node.getValue())); + } + + @Override + public void visit(Index node) { + String context = node.getContext(); + String name = context != null ? context + "." + node.getName() : node.getName(); + Name esname = new Name(name, model.getVisibility(context)); + esname.setType(model.getElasticsearchType(name)); + stack.push(esname); + } + + @Override + public void visit(SimpleName node) { + stack.push(new Name(node.getName())); + } + + private Node termToES(Term node) { + if (node.isLong()) { + return new Token(Long.parseLong(node.getValue())); + } else if (node.isFloat()) { + return new Token(Double.parseDouble(node.getValue())); + } else if (node.isIdentifier()) { + return new Token(node.getValue()); + } else if (node.isDate()) { + return new Token(DateUtil.parseDateISO(node.getValue())); + } else if (node.isString()) { + return new Token(node.getValue()); + } + return null; + } + + private Operator booleanToES(BooleanOperator bop) { + Operator op; + switch (bop) { + case AND: + op = Operator.AND; + break; + case OR: + op = Operator.OR; + break; + case NOT: + op = Operator.ANDNOT; + break; + case PROX: + op = Operator.PROX; + break; + default: + throw new IllegalArgumentException("unknown CQL operator: " + bop); + } + return op; + } + + private Operator comparitorToES(Comparitor op) { + Operator esop; + switch (op) { + case EQUALS: + esop = Operator.EQUALS; + break; + case GREATER: + esop = Operator.RANGE_GREATER_THAN; + break; + case GREATER_EQUALS: + esop = Operator.RANGE_GREATER_OR_EQUAL; + break; + case LESS: + esop = Operator.RANGE_LESS_THAN; + break; + case LESS_EQUALS: + esop = Operator.RANGE_LESS_OR_EQUALS; + break; + case NOT_EQUALS: + esop = Operator.NOT_EQUALS; + break; + case WITHIN: + esop = Operator.RANGE_WITHIN; + break; + case ADJ: + esop = Operator.PHRASE; + break; + case ALL: + esop = Operator.ALL; + break; + case ANY: + esop = Operator.ANY; + break; + default: + throw new IllegalArgumentException("unknown CQL comparitor: " + op); + } + return esop; + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ElasticsearchQueryGenerator.java b/src/main/java/org/xbib/cql/elasticsearch/ElasticsearchQueryGenerator.java new file mode 100644 index 0000000..3141d19 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ElasticsearchQueryGenerator.java @@ -0,0 +1,492 @@ +package org.xbib.cql.elasticsearch; + +import org.xbib.content.XContentBuilder; +import org.xbib.cql.BooleanGroup; +import org.xbib.cql.BooleanOperator; +import org.xbib.cql.CQLParser; +import org.xbib.cql.Comparitor; +import org.xbib.cql.Identifier; +import org.xbib.cql.Index; +import org.xbib.cql.ModifierList; +import org.xbib.cql.PrefixAssignment; +import org.xbib.cql.Query; +import org.xbib.cql.Relation; +import org.xbib.cql.ScopedClause; +import org.xbib.cql.SearchClause; +import org.xbib.cql.SimpleName; +import org.xbib.cql.SingleSpec; +import org.xbib.cql.SortSpec; +import org.xbib.cql.SortedQuery; +import org.xbib.cql.SyntaxException; +import org.xbib.cql.Term; +import org.xbib.cql.Visitor; +import org.xbib.cql.elasticsearch.ast.Expression; +import org.xbib.cql.elasticsearch.ast.Modifier; +import org.xbib.cql.elasticsearch.ast.Name; +import org.xbib.cql.elasticsearch.ast.Node; +import org.xbib.cql.elasticsearch.ast.Operator; +import org.xbib.cql.elasticsearch.ast.Token; +import org.xbib.cql.elasticsearch.ast.TokenType; +import org.xbib.cql.elasticsearch.model.ElasticsearchQueryModel; +import org.xbib.cql.util.DateUtil; + +import java.io.IOException; +import java.util.Collection; +import java.util.Stack; + +/** + * Generate Elasticsearch QueryModel DSL from CQL abstract syntax tree + */ +public class ElasticsearchQueryGenerator implements Visitor { + + private ElasticsearchQueryModel model; + + private ElasticsearchFilterGenerator filterGenerator; + + private Stack stack; + + private int from; + + private int size; + + private String boostField; + + private String modifier; + + private Float factor; + + private String boostMode; + + private SourceGenerator sourceGen; + + private QueryGenerator queryGen; + + private FilterGenerator filterGen; + + private FacetsGenerator facetGen; + + private XContentBuilder sort; + + public ElasticsearchQueryGenerator() { + this.from = 0; + this.size = 10; + this.model = new ElasticsearchQueryModel(); + this.filterGenerator = new ElasticsearchFilterGenerator(model); + this.stack = new Stack<>(); + try { + this.sourceGen = new SourceGenerator(); + this.queryGen = new QueryGenerator(); + this.filterGen = new FilterGenerator(); + this.facetGen = new FacetsGenerator(); + } catch (IOException e) { + // ignore + } + } + + public ElasticsearchQueryModel getModel() { + return model; + } + + public ElasticsearchQueryGenerator setFrom(int from) { + this.from = from; + return this; + } + + public ElasticsearchQueryGenerator setSize(int size) { + this.size = size; + return this; + } + + public ElasticsearchQueryGenerator setSort(XContentBuilder sort) { + this.sort = sort; + return this; + } + + public ElasticsearchQueryGenerator setBoostParams(String boostField, String modifier, Float factor, String boostMode) { + this.boostField = boostField; + this.modifier = modifier; + this.factor = factor; + this.boostMode = boostMode; + return this; + } + + public ElasticsearchQueryGenerator filter(String filter) { + CQLParser parser = new CQLParser(filter); + parser.parse(); + parser.getCQLQuery().accept(filterGenerator); + return this; + } + + public ElasticsearchQueryGenerator andfilter(String filterKey, Collection filterValues) { + filterGenerator.addAndFilter(filterKey, filterValues); + return this; + } + + public ElasticsearchQueryGenerator orfilter(String filterKey, Collection filterValues) { + filterGenerator.addOrFilter(filterKey, filterValues); + return this; + } + + public ElasticsearchQueryGenerator facet(String facetLimit, String facetSort) { + try { + facetGen.facet(facetLimit, facetSort); + } catch (IOException e) { + // ignore + } + return this; + } + + public String getQueryResult() { + return queryGen.getResult().string(); + } + + + public String getFacetResult() { + try { + return facetGen.getResult().string(); + } catch (IOException e) { + return e.getMessage(); + } + } + + public String getSourceResult() { + return sourceGen.getResult().string(); + } + + @Override + public void visit(SortedQuery node) { + try { + if (node.getSortSpec() != null) { + node.getSortSpec().accept(this); + } + queryGen.start(); + node.getQuery().accept(this); + if (boostField != null) { + queryGen.startBoost(boostField, modifier, factor, boostMode); + } + if (model.hasFilter()) { + queryGen.startFiltered(); + } else if (filterGenerator.getResult().bytes().length() > 0) { + queryGen.startFiltered(); + } + Node querynode = stack.pop(); + if (querynode instanceof Token) { + Token token = (Token) querynode; + querynode = ".".equals(token.getString()) ? + new Expression(Operator.MATCH_ALL) : + new Expression(Operator.EQUALS, new Name("cql.allIndexes"), querynode); + } + queryGen.visit((Expression) querynode); + if (model.hasFilter()) { + queryGen.end(); + filterGen = new FilterGenerator(queryGen); + filterGen.startFilter(); + filterGen.visit(model.getFilterExpression()); + filterGen.endFilter(); + queryGen.end(); + } else if (filterGenerator.getResult().bytes().length() > 0) { + queryGen.end(); + queryGen.getResult().rawField("filter", filterGenerator.getResult().bytes().toBytes()); + queryGen.endFiltered(); + } + if (boostField != null) { + queryGen.endBoost(); + } + if (model.hasFacets()) { + facetGen = new FacetsGenerator(); + facetGen.visit(model.getFacetExpression()); + } + queryGen.end(); + Expression sortnode = model.getSort(); + SortGenerator sortGen = new SortGenerator(); + if (sortnode != null) { + sortGen.start(); + sortGen.visit(sortnode); + sortGen.end(); + sort = sortGen.getResult(); + } + sourceGen.build(queryGen, from, size, sort, facetGen.getResult()); + } catch (IOException e) { + throw new SyntaxException("unable to build a valid query from " + node + " , reason: " + e.getMessage(), e); + } + } + + @Override + public void visit(SortSpec node) { + if (node.getSingleSpec() != null) { + node.getSingleSpec().accept(this); + } + if (node.getSortSpec() != null) { + node.getSortSpec().accept(this); + } + } + + @Override + public void visit(SingleSpec node) { + if (node.getIndex() != null) { + node.getIndex().accept(this); + } + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + if (!stack.isEmpty()) { + model.setSort(stack); + } + } + + @Override + public void visit(Query node) { + for (PrefixAssignment assignment : node.getPrefixAssignments()) { + assignment.accept(this); + } + if (node.getScopedClause() != null) { + node.getScopedClause().accept(this); + } + } + + @Override + public void visit(PrefixAssignment node) { + node.getPrefix().accept(this); + node.getURI().accept(this); + } + + @Override + public void visit(ScopedClause node) { + if (node.getScopedClause() != null) { + node.getScopedClause().accept(this); + } + node.getSearchClause().accept(this); + if (node.getBooleanGroup() != null) { + node.getBooleanGroup().accept(this); + } + // format disjunctive or conjunctive filters + if (node.getSearchClause().getIndex() != null + && model.isFilterContext(node.getSearchClause().getIndex().getContext())) { + // assume that each operator-less filter is a conjunctive filter + BooleanOperator op = node.getBooleanGroup() != null + ? node.getBooleanGroup().getOperator() : BooleanOperator.AND; + String filtername = node.getSearchClause().getIndex().getName(); + Operator filterop = comparitorToES(node.getSearchClause().getRelation().getComparitor()); + Node filterterm = termToESwithoutWildCard(node.getSearchClause().getTerm()); + if (op == BooleanOperator.AND) { + model.addConjunctiveFilter(filtername, filterterm, filterop); + } else if (op == BooleanOperator.OR) { + model.addDisjunctiveFilter(filtername, filterterm, filterop); + } + } + // evaluate expression + if (!stack.isEmpty() && stack.peek() instanceof Operator) { + Operator op = (Operator) stack.pop(); + if (!stack.isEmpty()) { + Node esnode = stack.pop(); + // add default context if node is a literal without a context + if (esnode instanceof Token && TokenType.STRING.equals(esnode.getType())) { + esnode = new Expression(Operator.EQUALS, new Name("cql.allIndexes"), esnode); + } + if (stack.isEmpty()) { + // unary expression + throw new IllegalArgumentException("unary expression not allowed, op=" + op + " node=" + esnode); + } else { + // binary expression + Node esnode2 = stack.pop(); + // add default context if node is a literal without context + if (esnode2 instanceof Token && TokenType.STRING.equals(esnode2.getType())) { + esnode2 = new Expression(Operator.EQUALS, new Name("cql.allIndexes"), esnode2); + } + esnode = new Expression(op, esnode2, esnode); + } + stack.push(esnode); + } + } + } + + @Override + public void visit(SearchClause node) { + if (node.getQuery() != null) { + // CQL query in parenthesis + node.getQuery().accept(this); + } + if (node.getTerm() != null) { + node.getTerm().accept(this); + } + if (node.getIndex() != null) { + node.getIndex().accept(this); + String context = node.getIndex().getContext(); + // format facets + if (model.isFacetContext(context)) { + model.addFacet(node.getIndex().getName(), node.getTerm().getValue()); + } + } + if (node.getRelation() != null) { + node.getRelation().accept(this); + if (node.getRelation().getModifierList() != null && node.getIndex() != null) { + // stack layout: op, list of modifiers, modifiable index + Node op = stack.pop(); + StringBuilder sb = new StringBuilder(); + Node modifier = stack.pop(); + while (modifier instanceof Modifier) { + if (sb.length() > 0) { + sb.append('.'); + } + sb.append(modifier.toString()); + modifier = stack.pop(); + } + String modifiable = sb.toString(); + stack.push(new Name(modifiable)); + stack.push(op); + } + } + // evaluate expression + if (!stack.isEmpty() && stack.peek() instanceof Operator) { + Operator op = (Operator) stack.pop(); + Node arg1 = stack.pop(); + Node arg2 = stack.pop(); + // fold two expressions if they have the same operator + boolean fold = arg1.isVisible() && arg2.isVisible() + && arg2 instanceof Expression + && ((Expression) arg2).getOperator().equals(op); + Expression expression = fold ? new Expression((Expression) arg2, arg1) : new Expression(op, arg1, arg2); + stack.push(expression); + } + } + + @Override + public void visit(BooleanGroup node) { + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + stack.push(booleanToES(node.getOperator())); + } + + @Override + public void visit(Relation node) { + if (node.getModifierList() != null) { + node.getModifierList().accept(this); + } + stack.push(comparitorToES(node.getComparitor())); + } + + @Override + public void visit(ModifierList node) { + for (org.xbib.cql.Modifier modifier : node.getModifierList()) { + modifier.accept(this); + } + } + + @Override + public void visit(org.xbib.cql.Modifier node) { + Node term = null; + if (node.getTerm() != null) { + node.getTerm().accept(this); + term = stack.pop(); + } + node.getName().accept(this); + Node name = stack.pop(); + stack.push(new Modifier(name, term)); + } + + @Override + public void visit(Term node) { + stack.push(termToES(node)); + } + + @Override + public void visit(Identifier node) { + stack.push(new Name(node.getValue())); + } + + @Override + public void visit(Index node) { + String context = node.getContext(); + String name = context != null ? context + "." + node.getName() : node.getName(); + Name esname = new Name(name, model.getVisibility(context)); + esname.setType(model.getElasticsearchType(name)); + stack.push(esname); + } + + @Override + public void visit(SimpleName node) { + stack.push(new Name(node.getName())); + } + + private Node termToES(Term node) { + if (node.isLong()) { + return new Token(Long.parseLong(node.getValue())); + } else if (node.isFloat()) { + return new Token(Double.parseDouble(node.getValue())); + } else if (node.isIdentifier()) { + return new Token(node.getValue()); + } else if (node.isDate()) { + return new Token(DateUtil.parseDateISO(node.getValue())); + } else if (node.isString()) { + return new Token(node.getValue()); + } + return null; + } + + private Node termToESwithoutWildCard(Term node) { + return node.isString() || node.isIdentifier() + ? new Token(node.getValue().replaceAll("\\*", "")) + : termToES(node); + } + + private Operator booleanToES(BooleanOperator bop) { + Operator op; + switch (bop) { + case AND: + op = Operator.AND; + break; + case OR: + op = Operator.OR; + break; + case NOT: + op = Operator.ANDNOT; + break; + case PROX: + op = Operator.PROX; + break; + default: + throw new IllegalArgumentException("unknown CQL operator: " + bop); + } + return op; + } + + private Operator comparitorToES(Comparitor op) { + Operator esop; + switch (op) { + case EQUALS: + esop = Operator.EQUALS; + break; + case GREATER: + esop = Operator.RANGE_GREATER_THAN; + break; + case GREATER_EQUALS: + esop = Operator.RANGE_GREATER_OR_EQUAL; + break; + case LESS: + esop = Operator.RANGE_LESS_THAN; + break; + case LESS_EQUALS: + esop = Operator.RANGE_LESS_OR_EQUALS; + break; + case NOT_EQUALS: + esop = Operator.NOT_EQUALS; + break; + case WITHIN: + esop = Operator.RANGE_WITHIN; + break; + case ADJ: + esop = Operator.PHRASE; + break; + case ALL: + esop = Operator.ALL; + break; + case ANY: + esop = Operator.ANY; + break; + default: + throw new IllegalArgumentException("unknown CQL comparitor: " + op); + } + return esop; + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/FacetsGenerator.java b/src/main/java/org/xbib/cql/elasticsearch/FacetsGenerator.java new file mode 100644 index 0000000..300da93 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/FacetsGenerator.java @@ -0,0 +1,177 @@ +package org.xbib.cql.elasticsearch; + +import static org.xbib.content.json.JsonXContent.contentBuilder; + +import org.xbib.content.XContentBuilder; +import org.xbib.cql.SyntaxException; +import org.xbib.cql.elasticsearch.ast.Expression; +import org.xbib.cql.elasticsearch.ast.Modifier; +import org.xbib.cql.elasticsearch.ast.Name; +import org.xbib.cql.elasticsearch.ast.Operator; +import org.xbib.cql.elasticsearch.ast.Token; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Build facet from abstract syntax tree + */ +public class FacetsGenerator implements Visitor { + + private int facetlength = 10; + + private final XContentBuilder builder; + + public FacetsGenerator() throws IOException { + this.builder = contentBuilder(); + } + + public void start() throws IOException { + builder.startObject(); + } + + public void end() throws IOException { + builder.endObject(); + } + + public void startFacets() throws IOException { + builder.startObject("aggregations"); + } + + public void endFacets() throws IOException { + builder.endObject(); + } + + public XContentBuilder getResult() throws IOException { + return builder; + } + + @Override + public void visit(Token node) { + try { + builder.value(node.toString().getBytes()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Name node) { + try { + builder.value(node.toString().getBytes()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Modifier node) { + try { + builder.value(node.toString().getBytes()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Operator node) { + try { + builder.value(node.toString().getBytes()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Expression node) { + try { + Operator op = node.getOperator(); + switch (op) { + case TERMS_FACET: { + builder.startObject().field("myfacet", "myvalue") + .endObject(); + break; + } + default: + throw new IllegalArgumentException( + "unable to translate operator while building elasticsearch facet: " + op); + } + } catch (IOException e) { + throw new SyntaxException("internal error while building elasticsearch query", e); + } + } + + public FacetsGenerator facet(String facetLimit, String facetSort) throws IOException { + if (facetLimit == null) { + return this; + } + Map facetMap = parseFacet(facetLimit); + String[] sortSpec = facetSort != null ? facetSort.split(",") : new String[]{"recordCount", "descending"}; + String order = "_count"; + String dir = "desc"; + for (String s : sortSpec) { + switch (s) { + case "recordCount": + order = "_count"; + break; + case "alphanumeric": + order = "_term"; + break; + case "ascending": + dir = "asc"; + break; + } + } + builder.startObject(); + + for (String index : facetMap.keySet()) { + if ("*".equals(index)) { + continue; + } + // TODO range aggregations etc. + String facetType = "terms"; + Integer size = facetMap.get(index); + builder.field(index) + .startObject() + .field(facetType).startObject() + .field("field", index) + .field("size", size > 0 ? size : 10) + .startObject("order") + .field(order, dir) + .endObject() + .endObject(); + builder.endObject(); + } + builder.endObject(); + return this; + } + + private Map parseFacet(String spec) { + Map m = new HashMap(); + m.put("*", facetlength); + if (spec == null || spec.length() == 0) { + return m; + } + String[] params = spec.split(","); + for (String param : params) { + int pos = param.indexOf(':'); + if (pos > 0) { + int n = parseInt(param.substring(0, pos), facetlength); + m.put(param.substring(pos + 1), n); + } else if (param.length() > 0) { + int n = parseInt(param, facetlength); + m.put("*", n); + } + } + return m; + } + + private int parseInt(String s, int defaultValue) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/FilterGenerator.java b/src/main/java/org/xbib/cql/elasticsearch/FilterGenerator.java new file mode 100644 index 0000000..cad6100 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/FilterGenerator.java @@ -0,0 +1,338 @@ +package org.xbib.cql.elasticsearch; + +import static org.xbib.content.json.JsonXContent.contentBuilder; + +import org.xbib.content.XContentBuilder; +import org.xbib.cql.SyntaxException; +import org.xbib.cql.elasticsearch.ast.Expression; +import org.xbib.cql.elasticsearch.ast.Modifier; +import org.xbib.cql.elasticsearch.ast.Name; +import org.xbib.cql.elasticsearch.ast.Node; +import org.xbib.cql.elasticsearch.ast.Operator; +import org.xbib.cql.elasticsearch.ast.Token; +import org.xbib.cql.util.QuotedStringTokenizer; + +import java.io.IOException; + +/** + * Build query filter in Elasticsearch JSON syntax from abstract syntax tree + */ +public class FilterGenerator implements Visitor { + + private XContentBuilder builder; + + public FilterGenerator() throws IOException { + this.builder = contentBuilder(); + } + + public FilterGenerator(QueryGenerator queryGenerator) throws IOException { + this.builder = queryGenerator.getResult(); + } + + public FilterGenerator start() throws IOException { + builder.startObject(); + return this; + } + + public FilterGenerator end() throws IOException { + builder.endObject(); + return this; + } + + public FilterGenerator startFilter() throws IOException { + builder.startObject("filter"); + return this; + } + + public FilterGenerator endFilter() throws IOException { + builder.endObject(); + return this; + } + + public XContentBuilder getResult() throws IOException { + return builder; + } + + @Override + public void visit(Token node) { + try { + builder.value(node.getString()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Name node) { + try { + builder.field(node.toString()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Modifier node) { + try { + builder.value(node.toString()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Operator node) { + try { + builder.value(node.toString()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Expression node) { + if (!node.isVisible()) { + return; + } + try { + Operator op = node.getOperator(); + switch (op.getArity()) { + case 2: { + Node arg1 = node.getArg1(); + Node arg2 = node.getArgs().length > 1 ? node.getArg2() : null; + boolean visible = false; + for (Node arg : node.getArgs()) { + visible = visible || arg.isVisible(); + } + if (!visible) { + return; + } + Token tok2 = arg2 instanceof Token ? (Token) arg2 : null; + switch (op) { + case EQUALS: { + String field = arg1.toString(); + String value = tok2 != null ? tok2.getString() : ""; + builder.startObject(tok2 != null && tok2.isBoundary() ? "prefix" : "term"); + builder.field(field, value).endObject(); + break; + } + case NOT_EQUALS: { + String field = arg1.toString(); + String value = tok2 != null ? tok2.getString() : ""; + builder.startObject("not") + .startObject(tok2 != null && tok2.isBoundary() ? "prefix" : "term") + .field(field, value) + .endObject().endObject(); + break; + } + case ALL: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + boolean phrase = arg2 instanceof Token && ((Token) arg2).isProtected(); + if (phrase) { + builder.startArray("and"); + QuotedStringTokenizer qst = new QuotedStringTokenizer(value); + while (qst.hasMoreTokens()) { + builder.startObject().startObject("term").field(field, qst.nextToken()).endObject().endObject(); + } + builder.endArray(); + } else { + builder.startObject(tok2 != null && tok2.isBoundary() ? "prefix" : "term") + .field(field, value) + .endObject(); + } + break; + } + case ANY: { + boolean phrase = arg2 instanceof Token && ((Token) arg2).isProtected(); + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + if (phrase) { + builder.startArray("or"); + QuotedStringTokenizer qst = new QuotedStringTokenizer(value); + while (qst.hasMoreTokens()) { + builder.startObject().startObject("term") + .field(field, qst.nextToken()).endObject().endObject(); + } + builder.endArray(); + } else { + builder.startObject(tok2 != null && tok2.isBoundary() ? "prefix" : "term") + .field(field, value) + .endObject(); + } + break; + } + case RANGE_GREATER_THAN: { + String field = arg1.toString(); + String value = tok2 != null ? tok2.getString() : ""; + builder.startObject("range").startObject(field) + .field("from", value) + .field("include_lower", false) + .endObject().endObject(); + break; + } + case RANGE_GREATER_OR_EQUAL: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("range").startObject(field) + .field("from", value) + .field("include_lower", true) + .endObject().endObject(); + break; + } + case RANGE_LESS_THAN: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("range").startObject(field) + .field("to", value) + .field("include_upper", false) + .endObject().endObject(); + break; + } + case RANGE_LESS_OR_EQUALS: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("range").startObject(field) + .field("to", value) + .field("include_upper", true) + .endObject().endObject(); + break; + } + case RANGE_WITHIN: { + String field = arg1.toString(); + String value = tok2 != null ? tok2.getString() : ""; + String[] s = value.split(" "); + builder.startObject("range").startObject(field). + field("from", s[0]) + .field("to", s[1]) + .field("include_lower", true) + .field("include_upper", true) + .endObject().endObject(); + break; + } + case AND: { + if (arg2 == null) { + if (arg1.isVisible()) { + arg1.accept(this); + } + } else { + builder.startObject("bool"); + builder.startArray("must"); + Node[] args = node.getArgs(); + for (int i = 0; i < node.getArgs().length; i++) { + if (args[i].isVisible()) { + builder.startObject(); + args[i].accept(this); + builder.endObject(); + } + } + builder.endArray(); + builder.endObject(); + } + break; + } + case OR: { + if (arg2 == null) { + if (arg1.isVisible()) { + arg1.accept(this); + } + } else { + builder.startObject("bool"); + builder.startArray("should"); + Node[] args = node.getArgs(); + for (int i = 0; i < node.getArgs().length; i++) { + if (args[i].isVisible()) { + builder.startObject(); + args[i].accept(this); + builder.endObject(); + } + } + builder.endArray(); + builder.endObject(); + } + break; + } + case OR_FILTER: { + builder.startObject("bool"); + builder.startArray("should"); + Node[] args = node.getArgs(); + for (int i = 0; i < args.length; i += 2) { + if (args[i].isVisible()) { + builder.startObject().startObject("term"); + args[i].accept(this); + args[i + 1].accept(this); + builder.endObject().endObject(); + } + } + builder.endArray(); + builder.endObject(); + break; + } + case AND_FILTER: { + builder.startObject("bool"); + builder.startArray("must"); + Node[] args = node.getArgs(); + for (int i = 0; i < args.length; i += 2) { + if (args[i].isVisible()) { + builder.startObject().startObject("term"); + args[i].accept(this); + args[i + 1].accept(this); + builder.endObject().endObject(); + } + } + builder.endArray(); + builder.endObject(); + break; + } + case ANDNOT: { + if (arg2 == null) { + if (arg1.isVisible()) { + arg1.accept(this); + } + } else { + builder.startObject("bool"); + builder.startArray("must_not"); + Node[] args = node.getArgs(); + for (int i = 0; i < node.getArgs().length; i++) { + if (args[i].isVisible()) { + builder.startObject(); + args[i].accept(this); + builder.endObject(); + } + } + builder.endArray(); + builder.endObject(); + } + break; + } + case PROX: { + String field = arg1.toString(); + // we assume a default of 10 words is enough for proximity + String value = arg2 != null ? arg2.toString() + "~10" : ""; + builder.startObject("field").field(field, value).endObject(); + break; + } + case TERM_FILTER: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("term").field(field, value).endObject(); + break; + } + case QUERY_FILTER: { + builder.startObject("query"); + arg1.accept(this); + builder.endObject(); + break; + } + default: + throw new IllegalArgumentException("unable to translate operator while building elasticsearch query filter: " + op); + } + break; + } + } + } catch (IOException e) { + throw new SyntaxException("internal error while building elasticsearch query filter", e); + } + } + +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/QueryGenerator.java b/src/main/java/org/xbib/cql/elasticsearch/QueryGenerator.java new file mode 100644 index 0000000..8d6bcb4 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/QueryGenerator.java @@ -0,0 +1,381 @@ +package org.xbib.cql.elasticsearch; + +import static org.xbib.content.json.JsonXContent.contentBuilder; + +import org.xbib.content.XContentBuilder; +import org.xbib.cql.SyntaxException; +import org.xbib.cql.elasticsearch.ast.Expression; +import org.xbib.cql.elasticsearch.ast.Modifier; +import org.xbib.cql.elasticsearch.ast.Name; +import org.xbib.cql.elasticsearch.ast.Node; +import org.xbib.cql.elasticsearch.ast.Operator; +import org.xbib.cql.elasticsearch.ast.Token; + +import java.io.IOException; + +/** + * Build Elasticsearch query from abstract syntax tree + */ +public class QueryGenerator implements Visitor { + + private final XContentBuilder builder; + + public QueryGenerator() throws IOException { + this.builder = contentBuilder(); + } + + public void start() throws IOException { + builder.startObject(); + } + + public void end() throws IOException { + builder.endObject(); + } + + public void startFiltered() throws IOException { + builder.startObject("filtered").startObject("query"); + } + + public void endFiltered() throws IOException { + builder.endObject(); + } + + public void startBoost(String boostField, String modifier, Float factor, String boostMode) throws IOException { + builder.startObject("function_score") + .startObject("field_value_factor") + .field("field", boostField) + .field("modifier", modifier != null ? modifier : "log1p") + .field("factor", factor != null ? factor : 1.0f) + .endObject() + .field("boost_mode", boostMode != null ? boostMode : "multiply") + .startObject("query"); + } + + public void endBoost() throws IOException { + builder.endObject().endObject(); + } + + public XContentBuilder getResult() { + return builder; + } + + @Override + public void visit(Token token) { + try { + switch (token.getType()) { + case BOOL: + builder.value(token.getBoolean()); + break; + case INT: + builder.value(token.getInteger()); + break; + case FLOAT: + builder.value(token.getFloat()); + break; + case DATETIME: + builder.value(token.getDate()); + break; + case STRING: + builder.value(token.getString()); + break; + default: + throw new IOException("unknown token type: " + token); + } + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Name node) { + try { + builder.field(node.toString()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Modifier node) { + try { + builder.value(node.toString()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Operator node) { + try { + builder.value(node.toString()); + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Expression node) { + if (!node.isVisible()) { + return; + } + try { + Operator op = node.getOperator(); + switch (op.getArity()) { + case 0: { + switch (op) { + case MATCH_ALL: { + builder.startObject("match_all").endObject(); + break; + } + } + break; + } + case 1: { + // unary operators, anyone? + break; + } + case 2: { + // binary operators + Node arg1 = node.getArg1(); + Node arg2 = node.getArgs().length > 1 ? node.getArg2() : null; + Token tok2 = arg2 instanceof Token ? (Token) arg2 : null; + boolean visible = false; + for (Node arg : node.getArgs()) { + visible = visible || arg.isVisible(); + } + if (!visible) { + return; + } + switch (op) { + case EQUALS: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("simple_query_string") + .field("query", value) + .field("fields", new String[]{field}) + .field("analyze_wildcard", true) + .field("default_operator", "and") + .endObject(); + break; + } + case NOT_EQUALS: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("bool").startObject("must_not"); + builder.startObject("simple_query_string") + .field("query", value) + .field("fields", new String[]{field}) + .field("analyze_wildcard", true) + .field("default_operator", "and") + .endObject(); + builder.endObject().endObject(); + break; + } + case ALL: { + String field = arg1.toString(); + String value = tok2 != null ? tok2.getString() : ""; + builder.startObject("simple_query_string") + .field("query", value) + .field("fields", new String[]{field}) + .field("analyze_wildcard", true) + .field("default_operator", "and") + .endObject(); + break; + } + case ANY: { + String field = arg1.toString(); + String value = tok2 != null ? tok2.getString() : ""; + builder.startObject("simple_query_string") + .field("query", value) + .field("fields", new String[]{field}) + .field("analyze_wildcard", true) + .field("default_operator", "or") + .endObject(); + break; + } + case PHRASE: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + if (tok2 != null) { + if (tok2.isProtected()) { + builder.startObject("match_phrase") + .startObject(field) + .field("query", tok2.getString()) + .field("slop", 0) + .endObject() + .endObject(); + } else if (tok2.isAll()) { + builder.startObject("match_all").endObject(); + } else if (tok2.isWildcard()) { + builder.startObject("wildcard").field(field, value).endObject(); + } else if (tok2.isBoundary()) { + builder.startObject("prefix").field(field, value).endObject(); + } else { + builder.startObject("match_phrase") + .startObject(field) + .field("query", value) + .field("slop", 0) + .endObject() + .endObject(); + } + } + break; + } + case RANGE_GREATER_THAN: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("range").startObject(field) + .field("from", value) + .field("include_lower", false) + .endObject().endObject(); + break; + } + case RANGE_GREATER_OR_EQUAL: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("range").startObject(field) + .field("from", value) + .field("include_lower", true) + .endObject().endObject(); + break; + } + case RANGE_LESS_THAN: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("range").startObject(field) + .field("to", value) + .field("include_upper", false) + .endObject().endObject(); + break; + } + case RANGE_LESS_OR_EQUALS: { + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + builder.startObject("range").startObject(field) + .field("to", value) + .field("include_upper", true) + .endObject().endObject(); + break; + } + case RANGE_WITHIN: { + // borders are inclusive + String field = arg1.toString(); + String value = arg2 != null ? arg2.toString() : ""; + String from = null; + String to = null; + if (tok2 != null) { + if (!tok2.isProtected()) { + throw new IllegalArgumentException("range within: unable to derive range from a non-phrase: " + value); + } + if (tok2.getStringList().size() != 2) { + throw new IllegalArgumentException("range within: unable to derive range from a phrase of length not equals to 2: " + tok2.getStringList()); + } + from = tok2.getStringList().get(0); + to = tok2.getStringList().get(1); + } + builder.startObject("range").startObject(field) + .field("from", from) + .field("to", to) + .field("include_lower", true) + .field("include_upper", true) + .endObject().endObject(); + break; + } + case AND: { + if (arg2 == null) { + if (arg1.isVisible()) { + arg1.accept(this); + } + } else { + builder.startObject("bool"); + if (arg1.isVisible() && arg2.isVisible()) { + builder.startArray("must").startObject(); + arg1.accept(this); + builder.endObject().startObject(); + arg2.accept(this); + builder.endObject().endArray(); + } else if (arg1.isVisible()) { + builder.startObject("must"); + arg1.accept(this); + builder.endObject(); + } else if (arg2.isVisible()) { + builder.startObject("must"); + arg2.accept(this); + builder.endObject(); + } + builder.endObject(); + } + break; + } + case OR: { + // short expression + if (arg2 == null) { + if (arg1.isVisible()) { + arg1.accept(this); + } + } else { + builder.startObject("bool"); + if (arg1.isVisible() && arg2.isVisible()) { + builder.startArray("should").startObject(); + arg1.accept(this); + builder.endObject().startObject(); + arg2.accept(this); + builder.endObject().endArray(); + } else if (arg1.isVisible()) { + builder.startObject("should"); + arg1.accept(this); + builder.endObject(); + } else if (arg2.isVisible()) { + builder.startObject("should"); + arg2.accept(this); + builder.endObject(); + } + builder.endObject(); + } + break; + } + case ANDNOT: { + if (arg2 == null) { + if (arg1.isVisible()) { + arg1.accept(this); + } + } else { + builder.startObject("bool"); + if (arg1.isVisible() && arg2.isVisible()) { + builder.startArray("must_not").startObject(); + arg1.accept(this); + builder.endObject().startObject(); + arg2.accept(this); + builder.endObject().endArray(); + } else if (arg1.isVisible()) { + builder.startObject("must_not"); + arg1.accept(this); + builder.endObject(); + } else if (arg2.isVisible()) { + builder.startObject("must_not"); + arg2.accept(this); + builder.endObject(); + } + builder.endObject(); + } + break; + } + case PROX: { + String field = arg1.toString(); + // we assume a default of 10 words is enough for proximity + String value = arg2 != null ? arg2.toString() + "~10" : ""; + builder.startObject("field").field(field, value).endObject(); + break; + } + default: + throw new IllegalArgumentException("unable to translate operator while building elasticsearch query: " + op); + } + break; + } + } + } catch (IOException e) { + throw new SyntaxException("internal error while building elasticsearch query", e); + } + } + +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/SortGenerator.java b/src/main/java/org/xbib/cql/elasticsearch/SortGenerator.java new file mode 100644 index 0000000..6527a34 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/SortGenerator.java @@ -0,0 +1,109 @@ +package org.xbib.cql.elasticsearch; + +import static org.xbib.content.json.JsonXContent.contentBuilder; + +import org.xbib.content.XContentBuilder; +import org.xbib.cql.SyntaxException; +import org.xbib.cql.elasticsearch.ast.Expression; +import org.xbib.cql.elasticsearch.ast.Modifier; +import org.xbib.cql.elasticsearch.ast.Name; +import org.xbib.cql.elasticsearch.ast.Node; +import org.xbib.cql.elasticsearch.ast.Operator; +import org.xbib.cql.elasticsearch.ast.Token; + +import java.io.IOException; +import java.util.Stack; + +/** + * Build sort in Elasticsearch JSON syntax from abstract syntax tree + */ +public class SortGenerator implements Visitor { + + private final XContentBuilder builder; + + private final Stack modifiers; + + public SortGenerator() throws IOException { + this.builder = contentBuilder(); + this.modifiers = new Stack<>(); + } + + public void start() throws IOException { + builder.startArray(); + } + + public void end() throws IOException { + builder.endArray(); + } + + public XContentBuilder getResult() { + return builder; + } + + @Override + public void visit(Token node) { + } + + @Override + public void visit(Name node) { + try { + if (modifiers.isEmpty()) { + builder.startObject() + .field(node.getName()) + .startObject() + .field("unmapped_type", "string") + .field("missing", "_last") + .endObject() + .endObject(); + } else { + builder.startObject().field(node.getName()).startObject(); + while (!modifiers.isEmpty()) { + Modifier mod = modifiers.pop(); + String s = mod.getName().toString(); + switch (s) { + case "ascending": + case "sort.ascending": { + builder.field("order", "asc"); + break; + } + case "descending": + case "sort.descending": { + builder.field("order", "desc"); + break; + } + default: { + builder.field(s, mod.getTerm()); + break; + } + } + } + builder.field("unmapped_type", "string"); + builder.field("missing", "_last"); + builder.endObject(); + builder.endObject(); + } + } catch (IOException e) { + throw new SyntaxException(e.getMessage(), e); + } + } + + @Override + public void visit(Modifier node) { + modifiers.push(node); + } + + @Override + public void visit(Operator node) { + } + + @Override + public void visit(Expression node) { + Operator op = node.getOperator(); + if (op == Operator.SORT) { + for (Node arg : node.getArgs()) { + arg.accept(this); + } + } + } + +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/SourceGenerator.java b/src/main/java/org/xbib/cql/elasticsearch/SourceGenerator.java new file mode 100644 index 0000000..9cc295a --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/SourceGenerator.java @@ -0,0 +1,43 @@ +package org.xbib.cql.elasticsearch; + +import static org.xbib.content.json.JsonXContent.contentBuilder; + +import org.xbib.content.XContentBuilder; + +import java.io.IOException; + +/** + * + */ +public class SourceGenerator { + + private final XContentBuilder builder; + + public SourceGenerator() throws IOException { + this.builder = contentBuilder(); + } + + public void build(QueryGenerator query, + int from, int size) throws IOException { + build(query, from, size, null, null); + } + + public void build(QueryGenerator query, int from, int size, XContentBuilder sort, XContentBuilder facets) throws IOException { + builder.startObject(); + builder.field("from", from); + builder.field("size", size); + builder.rawField("query", query.getResult().bytes().toBytes() ); + if (sort != null && sort.bytes().length() > 0) { + builder.rawField("sort", sort.bytes().toBytes()); + } + if (facets != null && facets.bytes().length() > 0) { + builder.rawField("aggregations", facets.bytes().toBytes()); + } + builder.endObject(); + builder.close(); + } + + public XContentBuilder getResult() { + return builder; + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/Visitor.java b/src/main/java/org/xbib/cql/elasticsearch/Visitor.java new file mode 100644 index 0000000..9e13868 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/Visitor.java @@ -0,0 +1,24 @@ +package org.xbib.cql.elasticsearch; + +import org.xbib.cql.elasticsearch.ast.Expression; +import org.xbib.cql.elasticsearch.ast.Modifier; +import org.xbib.cql.elasticsearch.ast.Name; +import org.xbib.cql.elasticsearch.ast.Operator; +import org.xbib.cql.elasticsearch.ast.Token; + +/** + * + */ +public interface Visitor { + + void visit(Token node); + + void visit(Name node); + + void visit(Modifier node); + + void visit(Operator node); + + void visit(Expression node); + +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ast/Expression.java b/src/main/java/org/xbib/cql/elasticsearch/ast/Expression.java new file mode 100644 index 0000000..48a2c8d --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ast/Expression.java @@ -0,0 +1,110 @@ +package org.xbib.cql.elasticsearch.ast; + +import org.xbib.cql.elasticsearch.Visitor; + +/** + * Elasticsearch expression + */ +public class Expression implements Node { + + private Operator op; + + private Node[] args; + + private TokenType type; + + private boolean visible; + + /** + * Constructor for folding nodes. + * + * @param expr the expression + * @param arg the new argument + */ + public Expression(Expression expr, Node arg) { + this.type = TokenType.EXPRESSION; + this.op = expr.getOperator(); + if (arg instanceof Expression) { + Expression expr2 = (Expression) arg; + this.args = new Node[expr.getArgs().length + expr2.getArgs().length]; + System.arraycopy(expr.getArgs(), 0, this.args, 0, expr.getArgs().length); + System.arraycopy(expr2.getArgs(), 0, this.args, expr.getArgs().length, expr2.getArgs().length); + } else { + Node[] exprargs = expr.getArgs(); + this.args = new Node[exprargs.length + 1]; + // to avoid copy, organization of the argument list is reverse, the most recent arg is at position 0 + this.args[0] = arg; + System.arraycopy(exprargs, 0, this.args, 1, exprargs.length); + } + this.visible = false; + for (Node node : args) { + if (node instanceof Name || node instanceof Expression) { + this.visible = visible || arg.isVisible(); + } + } + } + + public Expression(Operator op, Node... args) { + this.op = op; + this.type = TokenType.EXPRESSION; + this.args = args; + if (args != null && args.length > 0) { + this.visible = false; + for (Node arg : args) { + if (arg instanceof Name || arg instanceof Expression) { + this.visible = visible || arg.isVisible(); + } + } + } else { + this.visible = true; + } + } + + public Operator getOperator() { + return op; + } + + public Node[] getArgs() { + return args; + } + + public Node getArg1() { + return args[0]; + } + + public Node getArg2() { + return args[1]; + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public TokenType getType() { + return type; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + if (!visible) { + return ""; + } + StringBuilder sb = new StringBuilder(op.toString()); + sb.append('('); + for (int i = 0; i < args.length; i++) { + sb.append(args[i]); + if (i < args.length - 1) { + sb.append(','); + } + } + sb.append(')'); + return sb.toString(); + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ast/Modifier.java b/src/main/java/org/xbib/cql/elasticsearch/ast/Modifier.java new file mode 100644 index 0000000..434b268 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ast/Modifier.java @@ -0,0 +1,49 @@ +package org.xbib.cql.elasticsearch.ast; + +import org.xbib.cql.elasticsearch.Visitor; + +/** + * This is a modifier node for Elasticsearch query language + */ +public class Modifier implements Node { + + private Node name; + private Node term; + + public Modifier(Node name, Node term) { + this.name = name; + this.term = term; + } + + public Modifier(Node name) { + this.name = name; + } + + public Node getName() { + return name; + } + + public Node getTerm() { + return term; + } + + @Override + public boolean isVisible() { + return true; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public TokenType getType() { + return TokenType.OPERATOR; + } + + @Override + public String toString() { + return name + "=" + term; + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ast/Name.java b/src/main/java/org/xbib/cql/elasticsearch/ast/Name.java new file mode 100644 index 0000000..c2cd1ef --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ast/Name.java @@ -0,0 +1,52 @@ +package org.xbib.cql.elasticsearch.ast; + +import org.xbib.cql.elasticsearch.Visitor; + +/** + * A name for Elasticsearch fields + */ +public class Name implements Node { + + private String name; + + private TokenType type; + + private boolean visible; + + public Name(String name) { + this(name, true); + } + + public Name(String name, boolean visible) { + this.name = name; + this.visible = visible; + } + + public String getName() { + return name; + } + + public void setType(TokenType type) { + this.type = type; + } + + @Override + public TokenType getType() { + return type; + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ast/Node.java b/src/main/java/org/xbib/cql/elasticsearch/ast/Node.java new file mode 100644 index 0000000..fdd05ca --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ast/Node.java @@ -0,0 +1,16 @@ +package org.xbib.cql.elasticsearch.ast; + +import org.xbib.cql.elasticsearch.Visitor; + +/** + * This node class is the base class for the Elasticsearch Query Lange abstract syntax tree + */ +public interface Node { + + void accept(Visitor visitor); + + boolean isVisible(); + + TokenType getType(); + +} \ No newline at end of file diff --git a/src/main/java/org/xbib/cql/elasticsearch/ast/Operator.java b/src/main/java/org/xbib/cql/elasticsearch/ast/Operator.java new file mode 100644 index 0000000..45ff772 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ast/Operator.java @@ -0,0 +1,62 @@ +package org.xbib.cql.elasticsearch.ast; + +import org.xbib.cql.elasticsearch.Visitor; + +/** + * Elasticsearch operators + */ +public enum Operator implements Node { + EQUALS(2), + NOT_EQUALS(2), + RANGE_LESS_THAN(2), + RANGE_LESS_OR_EQUALS(2), + RANGE_GREATER_THAN(2), + RANGE_GREATER_OR_EQUAL(2), + RANGE_WITHIN(2), + AND(2), + ANDNOT(2), + OR(2), + PROX(2), + ALL(2), + ANY(2), + PHRASE(2), + TERM_FILTER(2), + QUERY_FILTER(2), + SORT(0), + TERMS_FACET(0), + OR_FILTER(2), + AND_FILTER(2), + MATCH_ALL(0); + + + private final int arity; + + Operator(int arity) { + this.arity = arity; + } + + @Override + public boolean isVisible() { + return true; + } + + @Override + public TokenType getType() { + return TokenType.OPERATOR; + } + + public int getArity() { + return arity; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return this.name(); + } + +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ast/Token.java b/src/main/java/org/xbib/cql/elasticsearch/ast/Token.java new file mode 100644 index 0000000..9ec3576 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ast/Token.java @@ -0,0 +1,213 @@ +package org.xbib.cql.elasticsearch.ast; + +import static java.util.stream.Collectors.toList; + +import org.xbib.cql.elasticsearch.Visitor; +import org.xbib.cql.util.DateUtil; +import org.xbib.cql.util.QuotedStringTokenizer; +import org.xbib.cql.util.UnterminatedQuotedStringException; + +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Elasticsearch query tokens. + */ +public class Token implements Node { + + public enum TokenClass { + + NORMAL, ALL, WILDCARD, BOUNDARY, PROTECTED + } + + private TokenType type; + + private String value; + + private String stringvalue; + + private Boolean booleanvalue; + + private Long longvalue; + + private Double doublevalue; + + private Date datevalue; + + private List values; + + private final EnumSet tokenClass; + + public Token(String value) { + this.value = value; + this.tokenClass = EnumSet.of(TokenClass.NORMAL); + this.type = TokenType.STRING; + // if this string is equal to true/false or on/off or yes/no, convert silently to bool + if (value.equals("true") || value.equals("yes") || value.equals("on")) { + this.booleanvalue = true; + this.value = null; + this.type = TokenType.BOOL; + + } else if (value.equals("false") || value.equals("no") || value.equals("off")) { + this.booleanvalue = false; + this.value = null; + this.type = TokenType.BOOL; + + } + if (this.value != null) { + // protected? + if (value.startsWith("\"") && value.endsWith("\"")) { + this.stringvalue = value; + this.value = value.substring(1, value.length() - 1).replaceAll("\\\\\"", "\""); + this.values = parseQuot(this.value); + tokenClass.add(TokenClass.PROTECTED); + } + // wildcard? + if (this.value.indexOf('*') >= 0 || this.value.indexOf('?') >= 0) { + tokenClass.add(TokenClass.WILDCARD); + // all? + if (this.value.length() == 1) { + tokenClass.add(TokenClass.ALL); + } + } + // prefix? + if (this.value.length() > 0 && this.value.charAt(0) == '^') { + tokenClass.add(TokenClass.BOUNDARY); + this.value = this.value.substring(1); + } + } + } + + public Token(Boolean value) { + this.booleanvalue = value; + this.type = TokenType.BOOL; + this.tokenClass = EnumSet.of(TokenClass.NORMAL); + } + + public Token(Long value) { + this.longvalue = value; + this.type = TokenType.INT; + this.tokenClass = EnumSet.of(TokenClass.NORMAL); + } + + public Token(Double value) { + this.doublevalue = value; + this.type = TokenType.FLOAT; + this.tokenClass = EnumSet.of(TokenClass.NORMAL); + } + + public Token(Date value) { + this.datevalue = value; + // this will enforce dates to get formatted as long values (years) + this.longvalue = Long.parseLong(DateUtil.formatDate(datevalue, "yyyy")); + this.type = TokenType.DATETIME; + this.tokenClass = EnumSet.of(TokenClass.NORMAL); + } + + /** + * Same as toString(), but ignore stringvalue. + */ + public String getString() { + StringBuilder sb = new StringBuilder(); + if (booleanvalue != null) { + sb.append(booleanvalue); + } else if (longvalue != null) { + sb.append(longvalue); + } else if (doublevalue != null) { + sb.append(doublevalue); + } else if (datevalue != null) { + sb.append(DateUtil.formatDateISO(datevalue)); + } else if (value != null) { + sb.append(value); + } + return sb.toString(); + } + + public Boolean getBoolean() { + return booleanvalue; + } + + public Long getInteger() { + return longvalue; + } + + public Double getFloat() { + return doublevalue; + } + + public Date getDate() { + return datevalue; + } + + public List getStringList() { + return values; + } + + @Override + public TokenType getType() { + return type; + } + + @Override + public boolean isVisible() { + return true; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (booleanvalue != null) { + sb.append(booleanvalue); + } else if (longvalue != null) { + sb.append(longvalue); + } else if (doublevalue != null) { + sb.append(doublevalue); + } else if (datevalue != null) { + sb.append(DateUtil.formatDateISO(datevalue)); + } else if (stringvalue != null) { + sb.append(stringvalue); + } else if (value != null) { + sb.append(value); + } + return sb.toString(); + } + + public boolean isProtected() { + return tokenClass.contains(TokenClass.PROTECTED); + } + + public boolean isBoundary() { + return tokenClass.contains(TokenClass.BOUNDARY); + } + + public boolean isWildcard() { + return tokenClass.contains(TokenClass.WILDCARD); + } + + public boolean isAll() { + return tokenClass.contains(TokenClass.ALL); + } + + private List parseQuot(String s) { + try { + QuotedStringTokenizer qst = new QuotedStringTokenizer(s, " \t\n\r\f", "\"", '\\', false); + Iterable iterable = () -> qst; + Stream stream = StreamSupport.stream(iterable.spliterator(), false); + return stream.filter(str -> !word.matcher(str).matches()).collect(toList()); + } catch (UnterminatedQuotedStringException e) { + return Collections.singletonList(s); + } + } + + private final static Pattern word = Pattern.compile("[\\P{IsWord}]"); +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ast/TokenType.java b/src/main/java/org/xbib/cql/elasticsearch/ast/TokenType.java new file mode 100644 index 0000000..3d08da8 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ast/TokenType.java @@ -0,0 +1,9 @@ +package org.xbib.cql.elasticsearch.ast; + +/** + * Elasticsearch query language token types. + */ +public enum TokenType { + + STRING, BOOL, INT, FLOAT, DATETIME, NAME, OPERATOR, EXPRESSION +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/ast/package-info.java b/src/main/java/org/xbib/cql/elasticsearch/ast/package-info.java new file mode 100644 index 0000000..a378a60 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/ast/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for abstract syntax tree construction for Elasticsearch query generation. + */ +package org.xbib.cql.elasticsearch.ast; diff --git a/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchFacet.java b/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchFacet.java new file mode 100644 index 0000000..a0be1b2 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchFacet.java @@ -0,0 +1,93 @@ +package org.xbib.cql.elasticsearch.model; + +import org.xbib.cql.QueryFacet; + +/** + * Elasticsearch facet. + * + * @param parameter type + */ +public final class ElasticsearchFacet implements QueryFacet, Comparable> { + + public enum Type { + TERMS, + RANGE, + HISTOGRAM, + DATEHISTOGRAM, + FILTER, + QUERY, + STATISTICAL, + TERMS_STATS, + GEO_DISTANCE + } + + public static int DEFAULT_FACET_SIZE = 10; + + private Type type; + + private String name; + + private V value; + + private int size; + + public ElasticsearchFacet(Type type, String name, V value) { + this(type, name, value, DEFAULT_FACET_SIZE); + } + + public ElasticsearchFacet(Type type, String name, V value, int size) { + this.type = type; + this.name = name; + this.value = value; + this.size = size; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public void setType(Type type) { + this.type = type; + } + + public Type getType() { + return type; + } + + @Override + public void setValue(V value) { + this.value = value; + } + + @Override + public V getValue() { + return value; + } + + @Override + public int getSize() { + return size; + } + + @Override + public String getFilterName() { + return name; + } + + @Override + public int compareTo(ElasticsearchFacet o) { + return name.compareTo(((ElasticsearchFacet) o).getName()); + } + + @Override + public String toString() { + return "facet [name=" + name + ",value=" + value + ",size=" + size + "]"; + } + +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchFilter.java b/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchFilter.java new file mode 100644 index 0000000..b57502a --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchFilter.java @@ -0,0 +1,53 @@ +package org.xbib.cql.elasticsearch.model; + +import org.xbib.cql.QueryFilter; +import org.xbib.cql.elasticsearch.ast.Operator; + +/** + * Elasticsearch filter. + * @param parameter type + */ +public class ElasticsearchFilter implements QueryFilter, Comparable> { + + private String name; + + private V value; + + private Operator op; + + public ElasticsearchFilter(String name, V value, Operator op) { + this.name = name; + this.op = op; + this.value = value; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setValue(V value) { + this.value = value; + } + + public V getValue() { + return value; + } + + public Operator getFilterOperation() { + return op; + } + + @Override + public int compareTo(ElasticsearchFilter o) { + return toString().compareTo(o.toString()); + } + + @Override + public String toString() { + return name + " " + op + " " + value; + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchQueryModel.java b/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchQueryModel.java new file mode 100644 index 0000000..518e0fa --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/model/ElasticsearchQueryModel.java @@ -0,0 +1,200 @@ +package org.xbib.cql.elasticsearch.model; + +import org.xbib.cql.elasticsearch.ast.Expression; +import org.xbib.cql.elasticsearch.ast.Name; +import org.xbib.cql.elasticsearch.ast.Node; +import org.xbib.cql.elasticsearch.ast.Operator; +import org.xbib.cql.elasticsearch.ast.Token; +import org.xbib.cql.elasticsearch.ast.TokenType; +import org.xbib.cql.model.CQLQueryModel; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +/** + * Elasticsearch query model. + */ +public final class ElasticsearchQueryModel { + + private final Map conjunctivefilters; + + private final Map disjunctivefilters; + + private final Map facets; + + private Expression sortexpr; + + public ElasticsearchQueryModel() { + this.conjunctivefilters = new HashMap<>(); + this.disjunctivefilters = new HashMap<>(); + this.facets = new HashMap<>(); + } + + /** + * Determine if the key has a type. Default type is string. + * + * @param key the key to check + * @return the type of the key + */ + public TokenType getElasticsearchType(String key) { + if ("datetime".equals(key)) { + return TokenType.DATETIME; + } + if ("int".equals(key)) { + return TokenType.INT; + } + if ("long".equals(key)) { + return TokenType.INT; + } + if ("float".equals(key)) { + return TokenType.FLOAT; + } + return TokenType.STRING; + } + + /** + * Get expression visibility of a given context. + * + * @param context the context + * @return true if visible + */ + public boolean getVisibility(String context) { + return !CQLQueryModel.isFacetContext(context) + && !CQLQueryModel.isFilterContext(context) + && !CQLQueryModel.isOptionContext(context); + } + + /** + * Check if this context is the facet context. + * + * @param context the context + * @return true if facet context + */ + public boolean isFacetContext(String context) { + return CQLQueryModel.isFacetContext(context); + } + + /** + * Check if this context is the filter context. + * + * @param context the context + * @return true if filter context + */ + public boolean isFilterContext(String context) { + return CQLQueryModel.isFilterContext(context); + } + + + public boolean hasFacets() { + return !facets.isEmpty(); + } + + public void addFacet(String key, String value) { + ElasticsearchFacet facet = new ElasticsearchFacet(ElasticsearchFacet.Type.TERMS, key, new Name(value)); + facets.put(facet.getName(), new Expression(Operator.TERMS_FACET, facet.getValue())); + } + + public Expression getFacetExpression() { + return new Expression(Operator.TERMS_FACET, facets.values().toArray(new Node[facets.size()])); + } + + public void addConjunctiveFilter(String name, Node value, Operator op) { + addFilter(conjunctivefilters, new ElasticsearchFilter<>(name, value, op)); + } + + public void addDisjunctiveFilter(String name, Node value, Operator op) { + addFilter(disjunctivefilters, new ElasticsearchFilter<>(name, value, op)); + } + + public boolean hasFilter() { + return !conjunctivefilters.isEmpty() || !disjunctivefilters.isEmpty(); + } + + /** + * Get filter expression. + * Only one filter expression is allowed per query. + * First, build conjunctive and disjunctive filter terms. + * If both are null, there is no filter at all. + * Otherwise, combine conjunctive and disjunctive filter terms with a + * disjunction, and apply filter function, and return this expression. + * + * @return a single filter expression or null if there are no filter terms + */ + public Expression getFilterExpression() { + if (!hasFilter()) { + return null; + } + Expression conjunctiveclause = null; + if (!conjunctivefilters.isEmpty()) { + conjunctiveclause = new Expression(Operator.AND, + conjunctivefilters.values().toArray(new Node[conjunctivefilters.size()])); + } + Expression disjunctiveclause = null; + if (!disjunctivefilters.isEmpty()) { + disjunctiveclause = new Expression(Operator.OR, + disjunctivefilters.values().toArray(new Node[disjunctivefilters.size()])); + } + if (conjunctiveclause != null && disjunctiveclause == null) { + return conjunctiveclause; + } else if (conjunctiveclause == null && disjunctiveclause != null) { + return disjunctiveclause; + } else { + return new Expression(Operator.OR, conjunctiveclause, disjunctiveclause); + } + } + + /** + * Add sort expression. + * + * @param indexAndModifier the index with modifiers + */ + public void setSort(Stack indexAndModifier) { + this.sortexpr = new Expression(Operator.SORT, reverse(indexAndModifier).toArray(new Node[indexAndModifier.size()])); + } + + /** + * Get sort expression. + * + * @return the sort expression + */ + public Expression getSort() { + return sortexpr; + } + + /** + * Helper method to add a filter. + * + * @param filters the filter list + * @param filter the filter to add + */ + private void addFilter(Map filters, ElasticsearchFilter filter) { + Name name = new Name(filter.getName()); + name.setType(getElasticsearchType(filter.getName())); + Node value = filter.getValue(); + if (value instanceof Token) { + value = new Expression(filter.getFilterOperation(), name, value); + } + if (filters.containsKey(filter.getName())) { + Expression expression = filters.get(filter.getName()); + expression = new Expression(expression, value); + filters.put(filter.getName(), expression); + } else { + filters.put(filter.getName(), (Expression) value); + } + } + + /** + * Helper method to reverse an expression stack. + * + * @param in the stack to reverse + * @return the reversed stack + */ + private Stack reverse(Stack in) { + Stack out = new Stack(); + while (!in.empty()) { + out.push(in.pop()); + } + return out; + } +} diff --git a/src/main/java/org/xbib/cql/elasticsearch/model/package-info.java b/src/main/java/org/xbib/cql/elasticsearch/model/package-info.java new file mode 100644 index 0000000..e9b7c89 --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/model/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for Elasticsearch query model. + */ +package org.xbib.cql.elasticsearch.model; \ No newline at end of file diff --git a/src/main/java/org/xbib/cql/elasticsearch/package-info.java b/src/main/java/org/xbib/cql/elasticsearch/package-info.java new file mode 100644 index 0000000..f37279f --- /dev/null +++ b/src/main/java/org/xbib/cql/elasticsearch/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for compiling CQL to Elasticsearch queries. + */ +package org.xbib.cql.elasticsearch; \ No newline at end of file diff --git a/src/main/java/org/xbib/cql/model/CQLQueryModel.java b/src/main/java/org/xbib/cql/model/CQLQueryModel.java new file mode 100644 index 0000000..657868d --- /dev/null +++ b/src/main/java/org/xbib/cql/model/CQLQueryModel.java @@ -0,0 +1,230 @@ +package org.xbib.cql.model; + +import org.xbib.cql.AbstractNode; +import org.xbib.cql.BooleanOperator; +import org.xbib.cql.Term; +import org.xbib.cql.model.breadcrumb.FacetBreadcrumbTrail; +import org.xbib.cql.model.breadcrumb.FilterBreadcrumbTrail; +import org.xbib.cql.model.breadcrumb.OptionBreadcrumbTrail; + +/** + * A CQL query model. + * Special contexts are facet, filter, + * and option. + * These contexts form breadcrumb trails. + * Bread crumbs provide a means for a server to track an chronologically + * ordered set of client actions. Bread crumbs are typically rendered as a + * user-driven constructed list of links, and are useful when + * users select them to drill down and up in a structure, + * so that they can find their way and have a notion of where they + * currently are. + * Bread crumbs in the original sense just represent where users are + * situated in a site hierarchy. For example, when browsing a + * library catalog, bread crumbs could look like this: + *
+ *  Home > Scientific literature > Arts & Human > Philosophy
+ * 
+ * or + *
+ *   Main library > Branch library > First floor > Rare book room
+ * 
+ * These items would be rendered as links to the corresponding location. + * Classes that implement this interface are responsible for managing + * such a bread crumb structure. A typical implementation regards + * bread crumbs as a set of elements. + * When a bread crumb is activated that was not in the set yet, + * it would add it to the set, or when a bread crumb is activated + * that is already on the set, it would roll back to the corresponding depth. + * In this model, multiple bread crumb trails may exist side by side. They are + * separate and do not depend on each other. There is a list of bread crumb + * trails, and the notion of a currently active bread crumb within a trail. + * This model does not make any presumptions on how it should interact with + * breadcrumbs except that a breadcrumb model should be serializable into + * a writer. + */ +public final class CQLQueryModel { + + /** + * Contexts 'facet', 'filter', and 'option'. + */ + public static final String FACET_INDEX_NAME = "facet"; + + public static final String FILTER_INDEX_NAME = "filter"; + + public static final String OPTION_INDEX_NAME = "option"; + + private static final String AND_OP = " and "; + + private static final String OR_OP = " or "; + + /** + * the CQL query string. + */ + private String query; + + /** + * breadcrumb trail for facets. + */ + private FacetBreadcrumbTrail facetTrail; + + /** + * breadcrumb trail for conjunctive filters. + */ + private FilterBreadcrumbTrail conjunctivefilterTrail; + + /** + * breadcrumb trail for disjunctive filters. + */ + private FilterBreadcrumbTrail disjunctivefilterTrail; + + /** + * breadcrumb trail for options. + */ + private OptionBreadcrumbTrail optionTrail; + + public CQLQueryModel() { + this.facetTrail = new FacetBreadcrumbTrail(); + this.conjunctivefilterTrail = new FilterBreadcrumbTrail(BooleanOperator.AND); + this.disjunctivefilterTrail = new FilterBreadcrumbTrail(BooleanOperator.OR); + this.optionTrail = new OptionBreadcrumbTrail(); + } + + public void setQuery(String query) { + this.query = query; + } + + public String getQuery() { + return query; + } + + public void addFacet(Facet facet) { + facetTrail.add(facet); + } + + public void removeFacet(Facet facet) { + facetTrail.remove(facet); + } + + /** + * Add CQL filter. + * + * @param op boolean operator, AND for conjunctive filter, OR for disjunctive filter + * @param filter the filter to add + */ + public void addFilter(BooleanOperator op, Filter filter) { + if (op == BooleanOperator.AND && !disjunctivefilterTrail.contains(filter)) { + conjunctivefilterTrail.add(filter); + } + if (op == BooleanOperator.OR && !conjunctivefilterTrail.contains(filter)) { + disjunctivefilterTrail.add(filter); + } + } + + /** + * Remove CQL filter. + * + * @param filter the filter to remove + */ + public void removeFilter(Filter filter) { + conjunctivefilterTrail.remove(filter); + disjunctivefilterTrail.remove(filter); + } + + public void addOption(Option option) { + optionTrail.add(option); + } + + public void removeOption(Option option) { + optionTrail.remove(option); + } + + public FacetBreadcrumbTrail getFacetTrail() { + return facetTrail; + } + + public String getFilterTrail() { + StringBuilder sb = new StringBuilder(); + if (!conjunctivefilterTrail.isEmpty()) { + sb.append(AND_OP).append(conjunctivefilterTrail.toString()); + } + if (disjunctivefilterTrail.size() == 1) { + sb.append(OR_OP).append(disjunctivefilterTrail.toString()); + } else if (disjunctivefilterTrail.size() > 1) { + sb.append(AND_OP).append(disjunctivefilterTrail.toString()); + } + return sb.toString(); + } + + /** + * Get the option breadcrumb trail. + * + * @return the option breadcrumb trail + */ + public OptionBreadcrumbTrail getOptionTrail() { + return optionTrail; + } + + /** + * Get query of a given context. + * + * @param context the context + * @return true if visible, false if not + */ + public static boolean isVisible(String context) { + return !isFacetContext(context) + && !isFilterContext(context) + && !isOptionContext(context); + } + + /** + * Check if this context is the facet context. + * + * @param context the context + * @return true if facet contet + */ + public static boolean isFacetContext(String context) { + return FACET_INDEX_NAME.equals(context); + } + + /** + * Check if this context is the filter context. + * + * @param context the context + * @return true if filter context + */ + public static boolean isFilterContext(String context) { + return FILTER_INDEX_NAME.equals(context); + } + + /** + * Check if this context is the option context + * + * @param context the context + * @return true if option context + */ + public static boolean isOptionContext(String context) { + return OPTION_INDEX_NAME.equals(context); + } + + /** + * Write the CQL query model as CQL string. + * + * @return the query model as CQL + */ + public String toCQL() { + StringBuilder sb = new StringBuilder(query); + String facets = getFacetTrail().toCQL(); + if (facets.length() > 0) { + sb.append(AND_OP).append(facets); + } + String filters = getFilterTrail(); + if (filters.length() > 0) { + sb.append(filters); + } + String options = getOptionTrail().toCQL(); + if (options.length() > 0) { + sb.append(AND_OP).append(options); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/xbib/cql/model/Facet.java b/src/main/java/org/xbib/cql/model/Facet.java new file mode 100644 index 0000000..88fc168 --- /dev/null +++ b/src/main/java/org/xbib/cql/model/Facet.java @@ -0,0 +1,70 @@ +package org.xbib.cql.model; + +import org.xbib.cql.QueryFacet; + +/** + * Facet. + * + * @param parameter type + */ +public final class Facet implements QueryFacet, Comparable> { + + private int size; + private String filterName; + private String name; + private V value; + + public Facet(String name) { + this.name = name; + } + + public Facet(String name, String filterName, int size) { + this.name = name; + this.filterName = filterName; + this.size = size; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setValue(V value) { + this.value = value; + } + + @Override + public V getValue() { + return value; + } + + @Override + public int getSize() { + return size; + } + + @Override + public String getFilterName() { + return filterName; + } + + public String toCQL() { + return CQLQueryModel.FACET_INDEX_NAME + "." + name + " = " + value; + } + + @Override + public int compareTo(Facet o) { + return name.compareTo((o).getName()); + } + + @Override + public String toString() { + return toCQL(); + } +} diff --git a/src/main/java/org/xbib/cql/model/Filter.java b/src/main/java/org/xbib/cql/model/Filter.java new file mode 100644 index 0000000..c7a9862 --- /dev/null +++ b/src/main/java/org/xbib/cql/model/Filter.java @@ -0,0 +1,68 @@ +package org.xbib.cql.model; + +import org.xbib.cql.QueryFilter; +import org.xbib.cql.Comparitor; + +/** + * Filter. + * @param filter parameter type + */ +public class Filter implements QueryFilter, Comparable> { + + private String name; + private V value; + private Comparitor op; + private String label; + + public Filter(String name, V value, Comparitor op) { + this.name = name; + this.op = op; + this.value = value; + } + + public Filter(String name, V value, Comparitor op, String label) { + this.name = name; + this.op = op; + this.value = value; + this.label = label; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setValue(V value) { + this.value = value; + } + + public V getValue() { + return value; + } + + public Comparitor getFilterOperation() { + return op; + } + + public String getLabel() { + return label; + } + + public String toCQL() { + return CQLQueryModel.FILTER_INDEX_NAME + "." + name + " " + op.getToken() + " " + value; + } + + @Override + public int compareTo(Filter o) { + return toString().compareTo((o).toString()); + } + + @Override + public String toString() { + return name + " " + op + " " + value; + } + +} diff --git a/src/main/java/org/xbib/cql/model/Option.java b/src/main/java/org/xbib/cql/model/Option.java new file mode 100644 index 0000000..e31e981 --- /dev/null +++ b/src/main/java/org/xbib/cql/model/Option.java @@ -0,0 +1,48 @@ +package org.xbib.cql.model; + +import org.xbib.cql.QueryOption; + +/** + * Option. + * @param parameter type + */ +public class Option implements QueryOption, Comparable> { + + private String name; + private V value; + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setValue(V value) { + this.value = value; + } + + @Override + public V getValue() { + return value; + } + + public String toCQL() { + return CQLQueryModel.OPTION_INDEX_NAME + "." + name + " = " + value; + } + + @Override + public int compareTo(Option o) { + return name.compareTo((o).getName()); + } + + @Override + public String toString() { + return toCQL(); + } + +} diff --git a/src/main/java/org/xbib/cql/model/breadcrumb/FacetBreadcrumbTrail.java b/src/main/java/org/xbib/cql/model/breadcrumb/FacetBreadcrumbTrail.java new file mode 100644 index 0000000..3cba055 --- /dev/null +++ b/src/main/java/org/xbib/cql/model/breadcrumb/FacetBreadcrumbTrail.java @@ -0,0 +1,32 @@ +package org.xbib.cql.model.breadcrumb; + +import org.xbib.cql.model.Facet; + +import java.util.Iterator; +import java.util.TreeSet; + +/** + * Facet breadcrumb trail. + */ +public class FacetBreadcrumbTrail extends TreeSet { + + @Override + public String toString() { + return toCQL(); + } + + public String toCQL() { + StringBuilder sb = new StringBuilder(); + if (isEmpty()) { + return sb.toString(); + } + Iterator it = iterator(); + if (it.hasNext()) { + sb.append(it.next().toCQL()); + } + while (it.hasNext()) { + sb.append(" and ").append(it.next().toCQL()); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/xbib/cql/model/breadcrumb/FilterBreadcrumbTrail.java b/src/main/java/org/xbib/cql/model/breadcrumb/FilterBreadcrumbTrail.java new file mode 100644 index 0000000..4c88b13 --- /dev/null +++ b/src/main/java/org/xbib/cql/model/breadcrumb/FilterBreadcrumbTrail.java @@ -0,0 +1,44 @@ +package org.xbib.cql.model.breadcrumb; + +import org.xbib.cql.BooleanOperator; +import org.xbib.cql.model.Filter; + +import java.util.Iterator; +import java.util.TreeSet; + +/** + * Filter breadcrumbs. + */ +public class FilterBreadcrumbTrail extends TreeSet { + + private BooleanOperator op; + + public FilterBreadcrumbTrail(BooleanOperator op) { + super(); + this.op = op; + } + + @Override + public String toString() { + return toCQL(); + } + + public String toCQL() { + StringBuilder sb = new StringBuilder(); + if (isEmpty()) { + return sb.toString(); + } + if (op == BooleanOperator.OR && size() > 1) { + sb.append('('); + } + Iterator it = this.iterator(); + sb.append(it.next().toCQL()); + while (it.hasNext()) { + sb.append(' ').append(op).append(' ').append(it.next().toCQL()); + } + if (op == BooleanOperator.OR && size() > 1) { + sb.append(')'); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/xbib/cql/model/breadcrumb/OptionBreadcrumbTrail.java b/src/main/java/org/xbib/cql/model/breadcrumb/OptionBreadcrumbTrail.java new file mode 100644 index 0000000..931bc9b --- /dev/null +++ b/src/main/java/org/xbib/cql/model/breadcrumb/OptionBreadcrumbTrail.java @@ -0,0 +1,39 @@ +package org.xbib.cql.model.breadcrumb; + +import org.xbib.cql.model.Option; + +import java.util.Iterator; +import java.util.TreeSet; + +/** + * An Option breadcrumb trail is a trail of attributes (key/value pairs). + * There is no interdependency between attributes; all values are allowed, + * even if they interfere with each other, the trail does not resolve it. + */ +public class OptionBreadcrumbTrail extends TreeSet