diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..8630e46c0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +/build +/jbake-dist/build +.gradle diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..a3b32ad0f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,50 @@ +name: Build + +on: + push: + branches: + - 'master' + - 'chore/**' + - 'feat/**' + - 'fix/**' + pull_request: + branches: + - 'master' + +jobs: + build: + name: Build + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v5 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + java-version: 21 + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Build-win + if: runner.os == 'Windows' + shell: cmd + run: gradlew.bat -Dfile.encoding=UTF-8 build -S + - name: Build-nix + if: runner.os != 'Windows' + run: ./gradlew build -S + + - name: Upload Reports + uses: actions/upload-artifact@v4 + if: failure() + with: + name: reports-${{ runner.os }} + path: | + jbake-core/build + jbake-dist/build diff --git a/.github/workflows/early-access.yml b/.github/workflows/early-access.yml new file mode 100644 index 000000000..460dcd09c --- /dev/null +++ b/.github/workflows/early-access.yml @@ -0,0 +1,48 @@ +name: EarlyAccess + +on: + push: + branches: [ master ] + +jobs: + earlyaccess: + name: EarlyAccess + if: github.repository == 'jbake-org/jbake' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + java-version: 21 + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Build + run: ./gradlew build -S + + - name: Release + run: ./gradlew jreleaserRelease -S + env: + JRELEASER_GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }} + JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} + JRELEASER_SDKMAN_CONSUMER_KEY: ${{ secrets.SDKMAN_CONSUMER_KEY }} + JRELEASER_SDKMAN_CONSUMER_TOKEN: ${{ secrets.SDKMAN_CONSUMER_TOKEN }} + + - name: JReleaser output + if: always() + uses: actions/upload-artifact@v4 + with: + name: jreleaser-logs + path: | + jbake-dist/build/jreleaser/trace.log + jbake-dist/build/jreleaser/output.properties diff --git a/.travis.yml b/.travis.yml index 859438b56..0d8f20a36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,33 +16,36 @@ install: true script: - "./gradlew check --stacktrace" -jdk: - - openjdk15 - - openjdk11 - jobs: include: - - jdk: openjdk15 - os: osx + - jdk: openjdk21 + + - jdk: openjdk17 - jdk: openjdk11 - os: osx - - stage: Quality Check - name: sonarqube - jdk: openjdk11 - script: - - "./gradlew jacocoRootReport sonarqube -i --stacktrace" + - os: osx + osx_image: xcode13.2 # defaults to openjdk17 + +# below config fails +# - os: osx +# osx_image: xcode9.4 +# jdk: openjdk11 - - stage: Quality Check - name: coveralls - jdk: openjdk11 - script: - - "./gradlew -PskipSigning jacocoRootReport coveralls -i --stacktrace" +# - stage: Quality Check +# name: sonarqube +# jdk: openjdk11 +# script: +# - "./gradlew jacocoRootReport sonarqube -i --stacktrace" +# +# - stage: Quality Check +# name: coveralls +# jdk: openjdk11 +# script: +# - "./gradlew -PskipSigning jacocoRootReport coveralls -i --stacktrace" notifications: - irc: "irc.freenode.org#jbake" webhooks: urls: - https://webhooks.gitter.im/e/2d332fabb02dba68a36b diff --git a/BUILD.adoc b/BUILD.adoc index 603edb689..dc4f0ffea 100644 --- a/BUILD.adoc +++ b/BUILD.adoc @@ -25,7 +25,7 @@ To get an overview of all available tasks with a short description run `./gradle == Structure -There are 3 projects +There are 4 projects: root aka. jbake-base:: configures subprojects, jacoco execution aggregation and coveralls @@ -34,6 +34,9 @@ jbake-core:: - the core library. produces jbake-core-{version}.jar (`build/libs`) - publishes to bintray maven repository jbake-core +jbake-maven-plugin:: + - the JBake maven plugin, build by Gradle too + jbake-dist:: - bundles the cli to an distribution (`build/distribution`) - publishes to bintray binary repository jbake @@ -161,36 +164,62 @@ plugin:: https://docs.gradle.org/current/userguide/application_plugin.html WARNING: Never add credentials to the repository -=== publish to bintray +=== github release + +Bump desired project version in the projects `gradle.properties` file. -You can publish to bintray with +Like ---- -./gradlew bintrayUpload +version = 2.7.0 ---- -If you want to see what's going on without publishing +Commit and push to origin at github. ---- -./gradlew -PbintrayDryRun=true bU --info +./gradlew signArchives +./gradlew githubRelease ---- -You need to add two properties to your local gradle.properties file (_~/.gradle/gradle.properties_). +The task will create a tag for you and create a release. Additionaly it uploads the binary distribution and the corresponding signature to the release. - bintrayUsername=username - bintrayKey=secret +[NOTE] +==== +You need to add some properties to your local gradle.properties file (_~/.gradle/gradle.properties_) -It's possible to change the organization and repository too. -The properties are called _bintrayOrg_ and _bintrayRepo_. -To publish to your private repository in an example repository run +---- +github.token= +github.release.owner=jbake-org +github.release.repo=jbake +---- + +It's also possible to dry-run this task. Execute `export GITHUB_RELEASE_DRY_RUN=true` in your terminal. +==== + +plugin:: https://github.com/BreadMoirai/github-release-gradle-plugin + +=== publish to nexus sonatype + +You can publish to nexus with ---- -gradle -PbintrayOrg='' -PbintrayRepo=example bU +./gradlew publishToSonatype ---- -The default values can be found in the _gradle.properties_ file at the root of this repository. +The task will create a staging repository. You need to close and publish it manually. +You can automate this process with the other tasks like `closeSonatypeStagingRepository` and `closeAndReleaseSonatypeStagingRepository`. + +For more information see: + +* https://github.com/gradle-nexus/publish-plugin +* https://central.sonatype.org/pages/ossrh-guide.html + +You need to add two properties to your local gradle.properties file (_~/.gradle/gradle.properties_). + + sonatypeUsername=username + sonatypePassword=secret -plugin:: https://plugins.gradle.org/plugin/com.jfrog.bintray +plugin:: https://plugins.gradle.org/plugin/io.github.gradle-nexus.publish-plugin === publish to sdkman diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index a22c2253b..e3ba82bd3 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -96,5 +96,5 @@ review the JBake documentation and make... === Supported Java Versions -At present JBake supports Java 7 and above and any contributions shouldn't use Java 8+ syntax, please see -the {uri-issues}[roadmap] for when support for Java 8 is planned. \ No newline at end of file +At present JBake supports Java 8 and above so any contributions shouldn't use Java 9+ syntax, please see +the {uri-issues}[roadmap] for when support for later Java versions is planned. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..e17527c5c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +FROM gradle:6.8-jdk11 as builder + +LABEL maintainer="https://jbake.org/community/team.html" + +ENV JBAKE_HOME=/opt/jbake + +RUN mkdir -p ${JBAKE_HOME} +COPY . /usr/src/jbake + +RUN set -o errexit -o nounset \ + && echo "Building JBake" \ + && cd /usr/src/jbake \ + && gradle --no-daemon installDist \ + && cp -r jbake-dist/build/install/jbake/* $JBAKE_HOME \ + && rm -r ~/.gradle /usr/src/jbake + +FROM eclipse-temurin:11-jre-alpine + +ENV JBAKE_USER=jbake +ENV JBAKE_HOME=/opt/jbake +ENV PATH ${JBAKE_HOME}/bin:$PATH +ENV TZ=UTC + +RUN apk --no-cache update && \ + apk --no-cache upgrade && \ + apk add --update tzdata && \ + rm -rf /var/cache/apk/* + +RUN echo ${TZ} > /etc/timezone + +RUN adduser -D -u 1000 -g "" ${JBAKE_USER} ${JBAKE_USER} + +USER ${JBAKE_USER} + +COPY --from=builder /opt/jbake /opt/jbake + +WORKDIR /mnt/site + +VOLUME ["/mnt/site"] + +ENTRYPOINT ["jbake"] +CMD ["-b"] + +EXPOSE 8820 diff --git a/README.asciidoc b/README.asciidoc index f52683339..5e5383d65 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -1,18 +1,17 @@ = JBake Jonathan Bullock -2017-05-11 +2021-04-13 :idprefix: -== JBake - http://jbake.org[JBake] is a Java based open source static site/blog generator for developers. +image:https://img.shields.io/github/v/release/jbake-org/jbake?label=download&sort=semver["GitHub Release Download", link="https://github.com/jbake-org/jbake/releases/latest"] +image:https://img.shields.io/maven-central/v/org.jbake/jbake-core.svg["Maven Download", link="http://jbake.org/download.html#maven"] +image:https://img.shields.io/homebrew/v/jbake.svg["Homebrew Download", link="http://jbake.org/download.html#homebrew"] + image:https://img.shields.io/travis/com/jbake-org/jbake/master.svg["Build Status", link="https://travis-ci.com/github/jbake-org/jbake"] image:https://ci.appveyor.com/api/projects/status/2q7hvg03wsjx953b?svg=true["Appveyor Status", link="https://ci.appveyor.com/project/jbake-org/jbake"] image:https://img.shields.io/coveralls/jbake-org/jbake/master.svg["Coverage Status", link="https://coveralls.io/r/jbake-org/jbake"] -image:https://img.shields.io/maven-central/v/org.jbake/jbake-core.svg["Maven Download", link="http://jbake.org/download.html#maven"] -image:https://api.bintray.com/packages/jbake/maven/jbake-core/images/download.svg["Bintray Download", link="https://bintray.com/jbake/maven/jbake-core/_latestVersion"] -image:https://img.shields.io/homebrew/v/jbake.svg["Homebrew Download", link="http://jbake.org/download.html#homebrew"] image:https://badges.gitter.im/jbake-org/jbake.svg["Gitter Chat", link="https://gitter.im/jbake-org/jbake"] @@ -55,6 +54,46 @@ Talk to other users of JBake on the forum: * http://groups.google.com/group/jbake-user[Forum] +== Docker Image + +The image uses the official https://hub.docker.com/r/adoptopenjdk/openjdk11/[adoptopenjdk/openjdk11:alpine] image +for building a distribution of JBake and +https://hub.docker.com/r/adoptopenjdk/openjdk11/[adoptopenjdk/openjdk11:alpine-jre] for runtime. + +=== Build + +To build the Docker image: + +---- +$ docker build -t jbake/jbake:latest . +---- + +=== Usage + +To execute JBake via Docker run this from project directory: + +---- +$ docker run --rm -u jbake -v "$PWD":/mnt/site jbake/jbake:latest +---- + +This command will execute using the jbake user to avoid running as root and will mount the current working directory as `/mnt/site` +in the Docker container where JBake is expecting your project files to be. By default the Docker image will execute a bake `-b` only. + +If you want to bake and serve your project using the Docker image then you'll need to override the default command: + +---- +$ docker run --rm -u jbake -v "$PWD":/mnt/site -p 8820:8820 jbake/jbake:latest -b -s +---- + +This command will also expose port 8820 from the container, you'll also need to set the following option in your `jbake.properties` file: + +---- +server.hostname=0.0.0.0 +---- + +NOTE: Docker image timezone is _UTC_. This may affect the date and time expected in output content. To set different timezone, add `TZ` environment variable and set value to required https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[timezone^]. Example - `docker run --rm -u jbake -e TZ=America/New_York -v "$PWD":/mnt/site jbake/jbake:latest` + + == Build System The project uses http://gradle.org[Gradle] 4.9+ as the build system. diff --git a/appveyor.yml b/appveyor.yml index b6ef6ed20..2875b1793 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,9 +8,8 @@ clone_depth: 10 environment: TERM: dumb matrix: - - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 - JAVA_HOME: C:\Program Files\Java\jdk11 - - JAVA_HOME: C:\Program Files\Java\jdk15 + - JAVA_HOME: C:\Program Files\Java\jdk17 install: - SET PATH=%JAVA_HOME%\bin;%PATH% @@ -20,6 +19,6 @@ install: - gradlew.bat --version - file C:\projects\jbake\jbake-core\src\test\resources\fixture\jbake.properties build_script: - - gradlew.bat -i assemble + - gradlew.bat -Dfile.encoding=UTF-8 -i assemble test_script: - - gradlew.bat -i -S check + - gradlew.bat -Dfile.encoding=UTF-8 -i -S check diff --git a/build.gradle b/build.gradle index f8c7dbde2..248290743 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,9 @@ plugins { + id "io.github.gradle-nexus.publish-plugin" version "$nexusPublishPluginVersion" + id 'com.github.ben-manes.versions' version "$versionsPluginVersion" + id 'org.jreleaser' version "$jreleaserVersion" apply false id "eclipse" id "idea" - id "io.sdkman.vendors" version "2.0.0" apply false - id "com.jfrog.bintray" version "1.8.5" apply false - id "com.github.kt3k.coveralls" version "2.10.2" apply false - id "org.sonarqube" version "3.1.1" apply false - id 'com.github.ben-manes.versions' version '0.38.0' - id "nebula.optional-base" version "5.0.3" apply false } // common variables @@ -21,168 +18,24 @@ ext { sonarDefaultProjectKey = "org.jbake:jbake-base:jbake-core" sonarURL = System.getenv("SONARHOST") ?: sonarDefaultURL sonarProjectKey = System.getenv("SONARPROJECTKEY") ?: sonarDefaultProjectKey + isReleaseVersion = !version.endsWith("SNAPSHOT") } -/** - * Apply coveralls to the root project as we just need it here to send the - * aggregated coverage execution data from the jacocoRootReport task - */ -apply plugin: 'com.github.kt3k.coveralls' - -/** - * Apply jacoco plugin to all projects and add jcenter as default repository - */ -allprojects { - apply plugin: 'jacoco' - - if ( JavaVersion.current().isJava8Compatible() ) { - - apply plugin: 'checkstyle' - - tasks.withType(Checkstyle) { - reports { - xml.enabled false - html.enabled true - } - } - - } - +nexusPublishing { repositories { - jcenter() - } - - jacoco { - toolVersion = jacocoVersion + sonatype() } - } -/** - * Common setup for all subprojects - */ -subprojects { - - apply plugin: 'java' - apply plugin: 'nebula.optional-base' - - // We do not publish any jars from the jbake-dist project - if ( project.name != "jbake-dist" ) { - apply from: "$rootDir/gradle/maven-publishing.gradle" - apply from: "$rootDir/gradle/signing.gradle" - apply from: "$rootDir/gradle/publishing.gradle" - } - - // add source and target compatibility for all JavaCompile tasks - tasks.withType(JavaCompile) { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - } - - test { - useJUnitPlatform() - - testLogging { - events "passed", "skipped", "failed" - exceptionFormat "full" - } - } - - dependencies { - implementation "org.slf4j:slf4j-api:$slf4jVersion" - implementation "org.slf4j:jul-to-slf4j:$slf4jVersion" - implementation "org.slf4j:jcl-over-slf4j:$slf4jVersion" - implementation "ch.qos.logback:logback-classic:$logbackVersion", optional - implementation "ch.qos.logback:logback-core:$logbackVersion", optional - - testImplementation "org.junit-pioneer:junit-pioneer:$junitPioneer" - testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5Version" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5Version" - // compatibility for Junit 4 test - testCompileOnly "junit:junit:$junit4Version" - testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit5Version" - - testImplementation "org.assertj:assertj-core:$assertjCoreVersion" - testImplementation "org.mockito:mockito-core:$mockitoVersion" - testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion" - testImplementation 'org.itsallcode:junit5-system-extensions:1.1.0' - } - - dependencyUpdates.resolutionStrategy { - componentSelection { rules -> - rules.all { ComponentSelection selection -> - boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm'].any { qualifier -> - selection.candidate.version ==~ /(?i).*[.-]?${qualifier}[.\d-]*/ - } - if (rejected) { - selection.reject('Release candidate') - } +dependencyUpdates.resolutionStrategy { + componentSelection { rules -> + rules.all { ComponentSelection selection -> + boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm'].any { qualifier -> + selection.candidate.version ==~ /(?i).*[.-]?${qualifier}[.\d-]*/ + } + if (rejected) { + selection.reject('Release candidate') } } } - - //set jvm for all Test tasks (like test and smokeTest) - tasks.withType(Test) { - - def args = ['-Xms512m', '-Xmx3g', '-Dorientdb.installCustomFormatter=false=false','-Djna.nosys=true'] - - /** - * jdk9 build is unable to determine the amount of MaxDirectMemorySize - * See https://pastebin.com/ECvQeHx0 - */ - if ( JavaVersion.current().java9Compatible ) { - args << '-XX:MaxDirectMemorySize=2g' - } - jvmArgs args - } - - jacocoTestReport { - reports { - xml.enabled = true // coveralls plugin depends on xml format report - html.enabled = true - } - } - - jacocoTestReport.dependsOn test -} - -task jacocoMerge(type: JacocoMerge) { - description 'Merge all testreport execution data from subprojects excluding jbake-dist' - dependsOn subprojects.test - executionData subprojects.findAll{it.name!="jbake-dist"}.jacocoTestReport.executionData -} - -task jacocoRootReport(type: JacocoReport, group: 'Coverage reports') { - description = 'Generates an aggregate report from all subprojects' - dependsOn jacocoMerge - - sourceDirectories.from files(subprojects.sourceSets.main.allSource.srcDirs) - classDirectories.from files(subprojects.sourceSets.main.output) - executionData.from jacocoMerge.executionData - - reports { - html.enabled = true - xml.enabled = true - } -} - -task testReport(type: TestReport) { - description "Generate an aggregated Testreport for all projects" - - destinationDir = file("$buildDir/reports/allTests") - // Include the results from the `test` task in all subprojects - reportOn subprojects*.test -} - -coveralls { - jacocoReportPath = "${buildDir}/reports/jacoco/jacocoRootReport/jacocoRootReport.xml" -} - -tasks.coveralls { - group = 'Coverage reports' - description = 'Uploads the aggregated coverage report to Coveralls' - - dependsOn jacocoRootReport - // Skip Task if not run on CI Server - onlyIf { System.env.'CI' } } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 000000000..678405245 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'groovy-gradle-plugin' +} diff --git a/jbake-core/src/test/resources/fixture/ignorablefolder/.jbakeignore b/buildSrc/settings.gradle similarity index 100% rename from jbake-core/src/test/resources/fixture/ignorablefolder/.jbakeignore rename to buildSrc/settings.gradle diff --git a/buildSrc/src/main/groovy/org/jbake/convention/org.jbake.convention.java-common.gradle b/buildSrc/src/main/groovy/org/jbake/convention/org.jbake.convention.java-common.gradle new file mode 100644 index 000000000..d7cbd3a4d --- /dev/null +++ b/buildSrc/src/main/groovy/org/jbake/convention/org.jbake.convention.java-common.gradle @@ -0,0 +1,142 @@ +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +plugins { + id 'java' + id 'jacoco' + id 'checkstyle' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.slf4j:slf4j-api:$slf4jVersion" + implementation "org.slf4j:jul-to-slf4j:$slf4jVersion" + implementation "org.slf4j:jcl-over-slf4j:$slf4jVersion" + implementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation "ch.qos.logback:logback-core:$logbackVersion" + + testImplementation "org.junit-pioneer:junit-pioneer:$junitPioneer" + testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5Version" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5Version" + // compatibility for Junit 4 test + testCompileOnly "junit:junit:$junit4Version" + testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit5Version" + + testImplementation "org.assertj:assertj-core:$assertjCoreVersion" + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion" +} + +tasks.withType(JavaCompile).configureEach { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +//set jvm for all Test tasks (like test and smokeTest) +tasks.withType(Test).configureEach { + + def args = ['-Xms512m', '-Xmx3g', '-Dorientdb.installCustomFormatter=false=false', '-Djna.nosys=true'] + + /** + * jdk9 build is unable to determine the amount of MaxDirectMemorySize + * See https://pastebin.com/ECvQeHx0 + */ + if (JavaVersion.current().java9Compatible) { + args << '-XX:MaxDirectMemorySize=2g' + } + jvmArgs = args +} + +tasks.register("javadocJar", Jar) { + archiveClassifier.set('javadoc') + from javadoc +} + +tasks.register("sourcesJar", Jar) { + + it.archiveClassifier.set('sources') + from sourceSets.main.allSource +} + +tasks.withType(AbstractArchiveTask).configureEach { + preserveFileTimestamps = false + reproducibleFileOrder = true +} + +tasks.register("commitBuildAndDate", Exec) { + group = "build" + description = "determine build date and time from last commit" + + executable "git" + args "show", "-s", "--format=%cI", "HEAD" + + standardOutput = new ByteArrayOutputStream() + + ext.commitDate = { + def zonedDateTime = ZonedDateTime.parse(standardOutput.toString().trim()) + return zonedDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss'['VV']'")) + } + + ext.date = { + def zonedDateTime = ZonedDateTime.parse(standardOutput.toString().trim()) + return zonedDateTime.format(DateTimeFormatter.ofPattern('yyyy-MM-dd')) + } + + ext.time = { + def zonedDateTime = ZonedDateTime.parse(standardOutput.toString().trim()) + return zonedDateTime.format(DateTimeFormatter.ofPattern('HH:mm:ss.SSSZ')) + } +} + +tasks.register("commitHash", Exec) { + group = "build" + description = "determine build commit hash for last commit" + + executable "git" + args "show", "-s", "--format=%h", "HEAD" + + standardOutput = new ByteArrayOutputStream() + + ext.abbreviated = { + return standardOutput.toString().trim() + } +} + +processResources.dependsOn(tasks.commitBuildAndDate) +processResources.dependsOn(tasks.commitHash) + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + exceptionFormat = "full" + } + + jacoco { + excludes = ["**/*OrientSqlTokenManager*"] + } +} + +jacoco { + toolVersion = jacocoVersion +} + +jacocoTestReport { + reports { + xml.required.set true // coveralls plugin depends on xml format report + html.required.set true + } +} + +jacocoTestReport.dependsOn test + +tasks.withType(Checkstyle).configureEach { + reports { + xml.required.set false + html.required.set true + } +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 63e20f426..560f649a5 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -20,7 +20,9 @@ - + + + diff --git a/gradle.properties b/gradle.properties index 325bdaae0..bee94706e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group = org.jbake -version = 2.6.6 +version = 2.7.0-SNAPSHOT description = JBake is a Java based open source static site/blog generator for developers. website = http://jbake.org @@ -7,42 +7,50 @@ issues = https://github.com/jbake-org/jbake/issues vcs = https://github.com/jbake-org/jbake/ # runtime dependencies -asciidoctorjVersion = 2.4.3 -asciidoctorjDiagramVersion = 2.1.0 +asciidoctorjVersion = 3.0.1 +asciidoctorjDiagramVersion = 3.0.1 args4jVersion = 2.33 -commonsIoVersion = 2.8.0 -commonsConfigurationVersion = 1.10 -commonsLangVersion = 3.12.0 -commonsVfs2Version = 2.7.0 -freemarkerVersion = 2.3.31 -flexmarkVersion = 0.62.2 -groovyVersion = 3.0.7 -jettyServerVersion = 9.4.36.v20210114 +commonsIoVersion = 2.21.0 +commonsConfigurationVersion = 2.12.0 +commonsBeanutilsVersion = 1.11.0 +commonsLangVersion = 3.19.0 +commonsVfs2Version = 2.10.0 +freemarkerVersion = 2.3.34 +flexmarkVersion = 0.64.8 +groovyVersion = 3.0.25 +jettyServerVersion = 11.0.26 jsonSimpleVersion = 1.1.1 jade4jVersion = 1.3.2 -jsoupVersion = 1.13.1 -jgitVersion = 5.10.0.202012080955-r -logbackVersion = 1.2.3 -orientDbVersion = 3.0.37 -pebbleVersion = 3.1.5 -slf4jVersion = 1.7.30 -thymeleafVersion = 3.0.12.RELEASE +jsoupVersion = 1.21.2 +jgitVersion = 7.4.0.202509020913-r +logbackVersion = 1.5.21 +orientDbVersion = 3.1.20 +pebbleVersion = 3.2.4 +slf4jVersion = 2.0.17 +snakeYamlVersion = 2.5 +thymeleafVersion = 3.1.3.RELEASE +picocli = 4.7.7 + # testing dependencies junit4Version = 4.13.2 -junit5Version = 5.7.1 -junitPioneer = 1.3.8 -assertjCoreVersion = 3.19.0 -mockitoVersion = 3.8.0 +junit5Version = 5.14.1 +junitPioneer = 2.3.0 +assertjCoreVersion = 3.27.6 +mockitoVersion = 5.20.0 # build dependencies -jacocoVersion = 0.8.6 -grgitVersion = 1.6.0 +jacocoVersion = 0.8.13 +nexusPublishPluginVersion = 2.0.0 +versionsPluginVersion = 0.53.0 +optionalBaseVersion = 10.0.1 +jreleaserVersion = 1.21.0 +mavenPluginDevVersion = 1.0.3 -bintrayDryRun = false -bintrayOrg = jbake -bintrayRepo = maven -bintrayBinaryRepo = binary +# jbake-maven-plugin dependencies +mavenVersion = 3.9.11 +mavenAnnotationsVersion = 3.15.2 +sparkVersion = 2.9.4 org.gradle.caching=true -org.gradle.parallel=true +org.gradle.vfs.watch = true diff --git a/gradle/application.gradle b/gradle/application.gradle index ed28737e0..714a77f92 100644 --- a/gradle/application.gradle +++ b/gradle/application.gradle @@ -1,66 +1,37 @@ -import org.ajoberstar.grgit.Grgit - -import java.text.SimpleDateFormat - -buildscript { - repositories { - jcenter() - } - dependencies { - classpath "org.ajoberstar:grgit:$grgitVersion" - } +application { + mainClass = "org.jbake.launcher.Main" + applicationName = "jbake" } -apply plugin: 'application' - -mainClassName = "org.jbake.launcher.Main" -applicationName = "jbake" - - -def examplesBase = "$project.buildDir/examples" - def exampleRepositories = [ - "example_project_freemarker": "git://github.com/jbake-org/jbake-example-project-freemarker.git", - "example_project_groovy" : "git://github.com/jbake-org/jbake-example-project-groovy.git", - "example_project_thymeleaf" : "git://github.com/jbake-org/jbake-example-project-thymeleaf.git", - "example_project_groovy-mte": "git://github.com/jbake-org/jbake-example-project-groovy-mte.git", - "example_project_jade" : "git://github.com/jbake-org/jbake-example-project-jade.git" + "example_project_freemarker": "https://github.com/jbake-org/jbake-example-project-freemarker.git", + "example_project_groovy" : "https://github.com/jbake-org/jbake-example-project-groovy.git", + "example_project_thymeleaf" : "https://github.com/jbake-org/jbake-example-project-thymeleaf.git", + "example_project_groovy-mte": "https://github.com/jbake-org/jbake-example-project-groovy-mte.git", + "example_project_jade" : "https://github.com/jbake-org/jbake-example-project-jade.git" ] -processResources { - from("src/main/resources"){ - include 'default.properties' - expand jbakeVersion: project.version, - timestamp: new SimpleDateFormat("yyyy-MM-dd HH:mm:ssa").format( new Date() ) - } -} - - - //create clone and Zip Task for each repository exampleRepositories.each { name, repository -> - task "clone_${name}Repository"() { + tasks.register("clone_${name}Repository", Exec) { group = "distribution" - description "Clone jbake ${name} example project repository" - - def repositoryName = "$examplesBase/$name" + description = "Clone jbake ${name} example project repository" + def repositoryName = "${project.layout.buildDirectory.asFile.get()}/examples/$name" - outputs.dir repositoryName + outputs.dir(repositoryName) - doLast { - Grgit.clone(dir: repositoryName, uri: repository) - } + executable "git" + args "clone", "$repository", "$repositoryName" } - task "${name}Zip"(type: Zip) { - group "distribution" - description "Zip $name repository" + tasks.register("${name}Zip", Zip) { + group = "distribution" + description = "Zip $name repository" - archiveBaseName = name - archiveFileName = "${archiveBaseName.get()}.zip" + archiveFileName = "${name}.zip" - from project.tasks.getByName("clone_${name}Repository").outputs + from project.tasks.named("clone_${name}Repository").get().outputs exclude 'README.md' exclude 'LICENSE' exclude '.git' diff --git a/gradle/maven-publishing.gradle b/gradle/maven-publishing.gradle index a1a518be0..e0bb12f09 100644 --- a/gradle/maven-publishing.gradle +++ b/gradle/maven-publishing.gradle @@ -1,108 +1,85 @@ -import java.text.SimpleDateFormat - apply plugin: 'maven-publish' -Date buildTimeAndDate = new Date() -ext { - buildDate = new SimpleDateFormat('yyyy-MM-dd').format(buildTimeAndDate) - buildTime = new SimpleDateFormat('HH:mm:ss.SSSZ').format(buildTimeAndDate) - isReleaseVersion = !version.endsWith("SNAPSHOT") -} - -task javadocJar(type: Jar) { - archiveClassifier.set('javadoc') - from javadoc -} - -task sourcesJar(type: Jar) { - archiveClassifier.set('sources') - from sourceSets.main.allSource -} - publishing { publications { - mavenJava(MavenPublication) { - from components.java + mavenJava(MavenPublication) { + from components.java - artifact javadocJar - artifact sourcesJar + artifact javadocJar + artifact sourcesJar - pom { - name = "jbake" - description = project.description - url = project.website - developers { - developer { - id = 'jonbullock' - name = 'Jonathan Bullock' - email = 'jonbullock@gmail.com' - url = 'http://jonathanbullock.com' - timezone = 'Europe/London' - } - developer { - id = 'ancho' - name = 'Frank Becker' - email = 'frank@calmdevelopment.de' - timezone = 'Europe/Berlin' - } - developer { - id = 'manikmagar' - name = 'Manik Magar' - url = 'https://manik.magar.me/' - timezone = 'America/New_York' - } + pom { + name = "jbake" + description = project.description + url = project.website + developers { + developer { + id = 'jonbullock' + name = 'Jonathan Bullock' + email = 'jonbullock@gmail.com' + url = 'http://jonathanbullock.com' + timezone = 'Europe/London' } - scm { - url = project.vcs - connection = 'scm:git:git@github.com:jbake-org/jbake.git' - developerConnection = 'scm:git:https://github.com/jbake-org/jbake.git' + developer { + id = 'ancho' + name = 'Frank Becker' + email = 'frank@calmdevelopment.de' + timezone = 'Europe/Berlin' } - issueManagement { - system = 'GitHub Issues' - url = project.issues + developer { + id = 'manikmagar' + name = 'Manik Magar' + url = 'https://manik.magar.me/' + timezone = 'America/New_York' } - mailingLists { - - mailingList { - name = 'jbake-dev' - subscribe = 'jbake-dev@googlegroups.com' - unsubscribe = 'jbake-dev+unsubscribe@googlegroups.com' - archive = 'http://groups.google.com/group/jbake-dev' - } - mailingList { - name = 'jbake-user' - subscribe = 'jbake-user@googlegroups.com' - unsubscribe = 'jbake-user+unsubscribe@googlegroups.com' - archive = 'http://groups.google.com/group/jbake-user' - } + developer { + id = 'aldrinleal' + name = 'Aldrin Leal' + timezone = 'America/Bogota' } + } + scm { + url = project.vcs + connection = 'scm:git:git@github.com:jbake-org/jbake.git' + developerConnection = 'scm:git:https://github.com/jbake-org/jbake.git' + } + issueManagement { + system = 'GitHub Issues' + url = project.issues + } + mailingLists { - licenses { - license { - name = 'The MIT License (MIT)' - url = 'http://opensource.org/licenses/MIT' - } + mailingList { + name = 'jbake-dev' + subscribe = 'jbake-dev@googlegroups.com' + unsubscribe = 'jbake-dev+unsubscribe@googlegroups.com' + archive = 'http://groups.google.com/group/jbake-dev' + } + mailingList { + name = 'jbake-user' + subscribe = 'jbake-user@googlegroups.com' + unsubscribe = 'jbake-user+unsubscribe@googlegroups.com' + archive = 'http://groups.google.com/group/jbake-user' } } } } } +} jar { manifest { - attributes( - 'Built-By': System.properties['user.name'], - 'Created-By': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})".toString(), - 'Build-Date': buildDate, - 'Build-Time': buildTime, - 'Specification-Title': project.name, - 'Specification-Version': project.version, - 'Specification-Vendor': project.name, - 'Implementation-Title': project.name, - 'Implementation-Version': project.version, - 'Implementation-Vendor': project.name - ) + attributes.putIfAbsent('Built-By', System.properties['user.name']) + attributes.putIfAbsent('Created-By', "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})") + attributes.putIfAbsent('Specification-Title', project.name) + attributes.putIfAbsent('Specification-Version', project.version) + attributes.putIfAbsent('Specification-Vendor', project.name) + attributes.putIfAbsent('Implementation-Title', project.name) + attributes.putIfAbsent('Implementation-Version', project.version) + attributes.putIfAbsent('Implementation-Vendor', project.name) + } + doFirst { + manifest.attributes.putIfAbsent('Build-Date',commitBuildAndDate.date()) + manifest.attributes.putIfAbsent('Build-Time',commitBuildAndDate.time()) } } - - diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle deleted file mode 100644 index 38135d261..000000000 --- a/gradle/publishing.gradle +++ /dev/null @@ -1,103 +0,0 @@ -apply plugin: 'com.jfrog.bintray' - -ext.bintrayUsername = project.hasProperty('bintrayUsername') ? bintrayUsername : '' -ext.bintrayKey = project.hasProperty('bintrayKey') ? bintrayKey : '' - -/* - UGLY HACK to workaround gradle-bintray-plugin compatibility with Gradle 5+ - https://github.com/asciidoctor/asciidoctorj/issues/861 - https://github.com/bintray/gradle-bintray-plugin/issues/300 - WARNING: since the Groovy Gradle API is modified, this breaks build isolation when sharing a common Gradle daemon instance - This works because gradle-bintray-plugin is using Groovy dynamic compilation hence it is affected by Groovy runtime meta-programming - Tested with: Gradle 5.6.3 / gradle-bintray-plugin 1.8.4 - TODO: remove as soon as bintray/gradle-bintray-plugin#300 is fixed and integrated -*/ -Signature.metaClass.getToSignArtifact = { -> - return (delegate as Signature).source -} - -if (project.name == "jbake-dist") { - - /** - * jbake-dist specific bintray configuration. - * - * We just need the distribution packages from the project with signatures if they are present. - * Notice the repository. It's a generic one. You can configure it with -PbintrayBinaryRepo=whatever - */ - bintray { - user = bintrayUsername - key = bintrayKey - - if (!project.hasProperty('skipSigning')) { - filesSpec { - from distZip - from distTar - from signArchives.signatureFiles.filter { !it.name.endsWith(".jar.asc") } - - into "." - } - - _bintrayRecordingCopy.dependsOn signArchives - - } else { - filesSpec { - from distZip - from distTar - - into "." - } - } - - dryRun = bintrayDryRun.toBoolean() - - pkg { - userOrg = bintrayOrg - repo = bintrayBinaryRepo - name = applicationName - desc = project.description - licenses = ['MIT'] - labels = ['jbake', 'site-generator'] - websiteUrl = project.website - issueTrackerUrl = project.issues - vcsUrl = project.vcs - publicDownloadNumbers = true - } - } - -} else { - - bintray { - user = bintrayUsername - key = bintrayKey - publications = ['mavenJava'] - - if (!project.hasProperty('skipSigning')) { - filesSpec { - from("${buildDir}/libs") { - include '*.jar.asc' - } - from("${buildDir}/publications/mavenJava") { - include 'pom-default.xml.asc' - rename 'pom-default.xml.asc', "${project.name}-${project.version}.pom.asc" - } - into "." - } - } - - dryRun = bintrayDryRun.toBoolean() - pkg { - userOrg = bintrayOrg - repo = bintrayRepo - name = project.name - desc = project.description - licenses = ['MIT'] - labels = ['jbake', 'site-generator'] - websiteUrl = project.website - issueTrackerUrl = project.issues - vcsUrl = project.vcs - publicDownloadNumbers = true - } - } - -} - diff --git a/gradle/release.gradle b/gradle/release.gradle new file mode 100644 index 000000000..474481d4e --- /dev/null +++ b/gradle/release.gradle @@ -0,0 +1,60 @@ +apply plugin: 'org.jreleaser' + +// cannot reference project. properties inside jreleaser config block +// as that collides with the "project" DSL element of the config extension +ext.releaseIsDryrun = (project.rootProject.findProperty('dryrun') ?: false).toBoolean() +ext.projectWebsite = project.website + +jreleaser { + dryrun = releaseIsDryrun + gitRootSearch = true + + project { + website = projectWebsite + authors = ['Jonathan Bullock'] + license = 'MIT' + extraProperties.put('inceptionYear', '2012') + } + + release { + github { + overwrite = true + branch = 'master' + changelog { + formatted = 'always' + format = '- {{commitShortHash}} {{commitTitle}}' + contributors { + format = '- {{contributorName}}{{#contributorUsernameAsLink}} ({{.}}){{/contributorUsernameAsLink}}' + } + hide { + contributors = ['GitHub'] + } + } + } + } + + signing { + active = 'always' + armored = true + } + + checksum { + individual = true + } + + distributions { + jbake { + sdkman { + active = 'release' + } + artifact { + path = 'build/distributions/{{distributionName}}-{{projectVersion}}-bin.zip' + transform = '{{distributionName}}/{{distributionName}}-{{projectEffectiveVersion}}-bin.zip' + } + artifact { + path = 'build/distributions/{{distributionName}}-{{projectVersion}}.tar' + transform = '{{distributionName}}/{{distributionName}}-{{projectEffectiveVersion}}.tar' + } + } + } +} diff --git a/gradle/sdkman.gradle b/gradle/sdkman.gradle deleted file mode 100644 index 54d72c756..000000000 --- a/gradle/sdkman.gradle +++ /dev/null @@ -1,34 +0,0 @@ -apply plugin: "io.sdkman.vendors" - -ext.sdkman_consumer_key = project.hasProperty('sdkman_consumer_key')?sdkman_consumer_key:'' -ext.sdkman_consumer_token = project.hasProperty('sdkman_consumer_token')?sdkman_consumer_token:'' - -sdkman { - consumerKey = project.sdkman_consumer_key - consumerToken = project.sdkman_consumer_token - candidate = "jbake" - version = rootProject.version - hashtag = "#JBake" - url = "https://dl.bintray.com/jbake/binary/${distZip.archiveName}" -} - -task distributionAvailable() { - group = "Verification" - description = "Check if distribution is available on bintray" - - doLast { - String errorMsg = "Distribution is not available at $sdkman.url" - - HttpURLConnection connection = sdkman.url.toURL().openConnection() - connection.setRequestMethod("HEAD") - connection.connect() - - if ( connection.responseCode != 200 ){ - throw new GradleException(errorMsg) - } - } -} - -tasks.findAll{ it.name ==~ /sdk.*(Release|Version)/ }.each { - it.dependsOn distributionAvailable -} \ No newline at end of file diff --git a/gradle/signing.gradle b/gradle/signing.gradle index 72cc887bb..a8b0abece 100644 --- a/gradle/signing.gradle +++ b/gradle/signing.gradle @@ -11,10 +11,5 @@ if ( !project.hasProperty('skipSigning') ) { sign publishing.publications.mavenJava } } - else { - signing { - sign configurations.archives - } - } } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c02..8bdaf60c7 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 442d9132e..bad7c2462 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c8..ef07e0162 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,81 +15,115 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # 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 +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac 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='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # 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 - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +132,120 @@ 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. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + 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 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 +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac 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 +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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 +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # 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\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg 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 -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32c..5eed7ee84 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@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 Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@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="-Xmx64m" "-Xms64m" - -@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 execute - -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 execute - -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 - -: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 %* - -: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 +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jbake-core/build.gradle b/jbake-core/build.gradle index d7f886ee7..789e84417 100644 --- a/jbake-core/build.gradle +++ b/jbake-core/build.gradle @@ -1,42 +1,64 @@ -import java.text.SimpleDateFormat - -apply from: "$rootDir/gradle/sonarqube.gradle" -apply plugin: 'java-library' +plugins { + id "org.jbake.convention.java-common" + id 'java-library' +} +apply from: "$rootDir/gradle/maven-publishing.gradle" +apply from: "$rootDir/gradle/signing.gradle" description = "The core library of JBake" +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = "jbake-core" + + licenses { + license { + name = 'The MIT License (MIT)' + url = 'http://opensource.org/licenses/MIT' + } + } + } + } + } +} + + dependencies { - implementation "commons-io:commons-io:$commonsIoVersion" - implementation "commons-configuration:commons-configuration:$commonsConfigurationVersion" - implementation "org.apache.commons:commons-vfs2:$commonsVfs2Version", optional + api "commons-io:commons-io:$commonsIoVersion" + api "org.apache.commons:commons-configuration2:$commonsConfigurationVersion" + implementation "commons-beanutils:commons-beanutils:$commonsBeanutilsVersion" + implementation "org.apache.commons:commons-vfs2:$commonsVfs2Version" implementation "org.apache.commons:commons-lang3:$commonsLangVersion" implementation("com.googlecode.json-simple:json-simple:$jsonSimpleVersion") { exclude group: "junit", module: "junit" } implementation "com.orientechnologies:orientdb-core:$orientDbVersion" - implementation "org.asciidoctor:asciidoctorj:$asciidoctorjVersion", optional - implementation "org.codehaus.groovy:groovy:$groovyVersion", optional - implementation "org.codehaus.groovy:groovy-templates:$groovyVersion", optional - implementation "org.codehaus.groovy:groovy-dateutil:$groovyVersion", optional - implementation "org.freemarker:freemarker:$freemarkerVersion", optional - implementation "org.thymeleaf:thymeleaf:$thymeleafVersion", optional - implementation "de.neuland-bfi:jade4j:$jade4jVersion", optional - implementation "com.vladsch.flexmark:flexmark:$flexmarkVersion", optional - implementation "com.vladsch.flexmark:flexmark-profile-pegdown:$flexmarkVersion", optional + api "org.asciidoctor:asciidoctorj:$asciidoctorjVersion" + api "org.codehaus.groovy:groovy:$groovyVersion" + api "org.codehaus.groovy:groovy-templates:$groovyVersion" + api "org.codehaus.groovy:groovy-dateutil:$groovyVersion" + api "org.freemarker:freemarker:$freemarkerVersion" + api "org.thymeleaf:thymeleaf:$thymeleafVersion" + api "de.neuland-bfi:jade4j:$jade4jVersion" + api "com.vladsch.flexmark:flexmark:$flexmarkVersion" + api "com.vladsch.flexmark:flexmark-profile-pegdown:$flexmarkVersion" + api "io.pebbletemplates:pebble:$pebbleVersion" implementation "org.jsoup:jsoup:$jsoupVersion" - implementation "io.pebbletemplates:pebble:$pebbleVersion", optional + implementation "org.yaml:snakeyaml:$snakeYamlVersion" // cli specific dependencies - implementation "org.eclipse.jetty:jetty-server:$jettyServerVersion", optional - implementation "args4j:args4j:$args4jVersion", optional + implementation "org.eclipse.jetty:jetty-server:$jettyServerVersion" + implementation "info.picocli:picocli:$picocli" } processResources { - from("src/main/resources") { - include 'default.properties' - expand jbakeVersion: project.version, - timestamp: new SimpleDateFormat("yyyy-MM-dd HH:mm:ssa").format(new Date()) - } + inputs.property("jbake.version", project.version) + filesMatching("default.properties") { + expand jbakeVersion: inputs.properties.get("jbake.version"), + timestamp: () -> commitBuildAndDate.date(), + gitHash: () -> commitHash.abbreviated() + } } - diff --git a/jbake-core/src/main/java/org/jbake/app/Asset.java b/jbake-core/src/main/java/org/jbake/app/Asset.java index cd9148cc0..7eefa962f 100644 --- a/jbake-core/src/main/java/org/jbake/app/Asset.java +++ b/jbake-core/src/main/java/org/jbake/app/Asset.java @@ -1,6 +1,6 @@ package org.jbake.app; -import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.configuration2.CompositeConfiguration; import org.apache.commons.io.FileUtils; import org.jbake.app.configuration.JBakeConfiguration; import org.jbake.app.configuration.JBakeConfigurationFactory; @@ -24,7 +24,7 @@ public class Asset { private static final Logger LOGGER = LoggerFactory.getLogger(Asset.class); private final List errors = new LinkedList<>(); - private JBakeConfiguration config; + private final JBakeConfiguration config; /** * @param source Source file for the asset @@ -65,7 +65,7 @@ public void copy(File path) { FileFilter filter = new FileFilter() { @Override public boolean accept(File file) { - return (!config.getAssetIgnoreHidden() || !file.isHidden()) && (file.isFile() || FileUtil.directoryOnlyIfNotIgnored(file)); + return (!config.getAssetIgnoreHidden() || !file.isHidden()) && (file.isFile() || FileUtil.directoryOnlyIfNotIgnored(file, config)); } }; copy(path, config.getDestinationFolder(), filter); @@ -99,11 +99,11 @@ public boolean isAssetFile(File path) { boolean isAsset = false; try { - if(FileUtil.directoryOnlyIfNotIgnored(path.getParentFile())) { + if(FileUtil.directoryOnlyIfNotIgnored(path.getParentFile(), config)) { if (FileUtil.isFileInDirectory(path, config.getAssetFolder())) { isAsset = true; } else if (FileUtil.isFileInDirectory(path, config.getContentFolder()) - && FileUtil.getNotContentFileFilter().accept(path)) { + && FileUtil.getNotContentFileFilter(config).accept(path)) { isAsset = true; } } @@ -119,7 +119,7 @@ public boolean isAssetFile(File path) { * @param path of the content directory */ public void copyAssetsFromContent(File path) { - copy(path, config.getDestinationFolder(), FileUtil.getNotContentFileFilter()); + copy(path, config.getDestinationFolder(), FileUtil.getNotContentFileFilter(config)); } /** @@ -158,7 +158,7 @@ private void copyFile(File asset, File targetFolder) { try { FileUtils.copyFile(asset, targetFolder); LOGGER.info("Copying [{}]... done!", asset.getPath()); - } catch (IOException e) { + } catch (IOException|IllegalArgumentException e) { LOGGER.error("Copying [{}]... failed!", asset.getPath(), e); errors.add(e); } diff --git a/jbake-core/src/main/java/org/jbake/app/ContentStore.java b/jbake-core/src/main/java/org/jbake/app/ContentStore.java index 7ebce3433..352e97ae5 100644 --- a/jbake-core/src/main/java/org/jbake/app/ContentStore.java +++ b/jbake-core/src/main/java/org/jbake/app/ContentStore.java @@ -32,44 +32,40 @@ import com.orientechnologies.orient.core.metadata.schema.OClass; import com.orientechnologies.orient.core.metadata.schema.OSchema; import com.orientechnologies.orient.core.metadata.schema.OType; -import com.orientechnologies.orient.core.record.impl.ODocument; -import com.orientechnologies.orient.core.sql.OCommandSQL; +import com.orientechnologies.orient.core.record.OElement; import com.orientechnologies.orient.core.sql.executor.OResultSet; -import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery; -import org.jbake.model.DocumentAttributes; +import org.jbake.model.DocumentModel; import org.jbake.model.DocumentTypes; +import org.jbake.model.ModelAttributes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.Collections; import java.util.HashSet; -import java.util.List; -import java.util.Map; import java.util.Set; - /** * @author jdlee */ public class ContentStore { - private static final String STATEMENT_GET_PUBLISHED_POST_BY_TYPE_AND_TAG = "select * from %s where status='published' and ? in tags order by date desc"; - private static final String STATEMENT_GET_POST_BY_TYPE_AND_URI = "select * from %s where sourceuri=?"; - private static final String STATEMENT_GET_DOCUMENT_STATUS_BY_DOCTYPE_AND_URI = "select sha1,rendered from %s where sourceuri=?"; - private static final String STATEMENT_GET_PUBLISHED_COUNT = "select count(*) as count from %s where status='published'"; - private static final String STATEMENT_MARK_CONTENT_AS_RENDERD = "update %s set rendered=true where rendered=false and cached=true"; - private static final String STATEMENT_DELETE_DOCTYPE_BY_SOURCEURI = "delete from %s where sourceuri=?"; - private static final String STATEMENT_GET_UNDRENDERED_CONTENT = "select * from %s where rendered=false order by date desc"; + private static final String STATEMENT_GET_PUBLISHED_POST_BY_TYPE_AND_TAG = "select * from Documents where status='published' and type='%s' and ? in tags order by date desc"; + private static final String STATEMENT_GET_DOCUMENT_STATUS_BY_DOCTYPE_AND_URI = "select sha1,rendered from Documents where sourceuri=?"; + private static final String STATEMENT_GET_PUBLISHED_COUNT = "select count(*) as count from Documents where status='published' and type='%s'"; + private static final String STATEMENT_MARK_CONTENT_AS_RENDERD = "update Documents set rendered=true where rendered=false and type='%s' and sourceuri='%s' and cached=true"; + private static final String STATEMENT_DELETE_DOCTYPE_BY_SOURCEURI = "delete from Documents where sourceuri=?"; + private static final String STATEMENT_GET_UNDRENDERED_CONTENT = "select * from Documents where rendered=false order by date desc"; private static final String STATEMENT_GET_SIGNATURE_FOR_TEMPLATES = "select sha1 from Signatures where key='templates'"; - private static final String STATEMENT_GET_TAGS_FROM_PUBLISHED_POSTS = "select tags from post where status='published'"; - private static final String STATEMENT_GET_ALL_CONTENT_BY_DOCTYPE = "select * from %s order by date desc"; - private static final String STATEMENT_GET_PUBLISHED_CONTENT_BY_DOCTYPE = "select * from %s where status='published' order by date desc"; - private static final String STATEMENT_GET_PUBLISHED_POSTS_BY_TAG = "select * from post where status='published' and ? in tags order by date desc"; - private static final String STATEMENT_GET_TAGS_BY_DOCTYPE = "select tags from %s where status='published'"; + private static final String STATEMENT_GET_TAGS_FROM_PUBLISHED_POSTS = "select tags from Documents where status='published' and type='post'"; + private static final String STATEMENT_GET_ALL_CONTENT_BY_DOCTYPE = "select * from Documents where type='%s' order by date desc"; + private static final String STATEMENT_GET_PUBLISHED_CONTENT_BY_DOCTYPE = "select * from Documents where status='published' and type='%s' order by date desc"; + private static final String STATEMENT_GET_PUBLISHED_POSTS_BY_TAG = "select * from Documents where status='published' and type='post' and ? in tags order by date desc"; + private static final String STATEMENT_GET_TAGS_BY_DOCTYPE = "select tags from Documents where status='published' and type='%s'"; private static final String STATEMENT_INSERT_TEMPLATES_SIGNATURE = "insert into Signatures(key,sha1) values('templates',?)"; - private static final String STATEMENT_DELETE_ALL = "delete from %s"; + private static final String STATEMENT_DELETE_ALL = "delete from Documents where type='%s'"; private static final String STATEMENT_UPDATE_TEMPLATE_SIGNATURE = "update Signatures set sha1=? where key='templates'"; + private static final String STATEMENT_GET_DOCUMENT_COUNT_BY_TYPE = "select count(*) as count from Documents where type='%s'"; private final Logger logger = LoggerFactory.getLogger(ContentStore.class); private final String type; @@ -130,12 +126,10 @@ public final void updateSchema() { OSchema schema = db.getMetadata().getSchema(); - for (String docType : DocumentTypes.getDocumentTypes()) { - if (!schema.existsClass(docType)) { - createDocType(schema, docType); - } + if (!schema.existsClass(Schema.DOCUMENTS)) { + createDocType(schema); } - if (!schema.existsClass("Signatures")) { + if (!schema.existsClass(Schema.SIGNATURES)) { createSignatureType(schema); } } @@ -186,109 +180,70 @@ private void activateOnCurrentThread() { } } - - /** - * Get a document by sourceUri and update it from the given map. - * @param incomingDocMap The document's db columns. - * @return The saved document. - * @throws IllegalArgumentException if sourceUri or docType are null, or if the document doesn't exist. - */ - public ODocument mergeDocument(Map incomingDocMap) { - String sourceUri = (String) incomingDocMap.get(DocumentAttributes.SOURCE_URI.toString()); - - if (null == sourceUri) { - throw new IllegalArgumentException("Document sourceUri is null."); - } - - String docType = (String) incomingDocMap.get(Crawler.Attributes.TYPE); - - if (null == docType) { - throw new IllegalArgumentException("Document docType is null."); - } - - // Get a document by sourceUri - String sql = String.format(STATEMENT_GET_POST_BY_TYPE_AND_URI, quoteIdentifier(docType)); - activateOnCurrentThread(); - List results = db.command(new OSQLSynchQuery(sql)).execute(sourceUri); - if (results.isEmpty()) { - throw new JBakeException("No document with sourceUri '" + sourceUri + "'."); - } - - // Update it from the given map. - ODocument incomingDoc = new ODocument(docType); - incomingDoc.fromMap(incomingDocMap); - ODocument merged = results.get(0).merge(incomingDoc, true, false); - return merged; - } - - public long getDocumentCount(String docType) { activateOnCurrentThread(); - return db.countClass(docType); + String statement = String.format(STATEMENT_GET_DOCUMENT_COUNT_BY_TYPE, docType); + return (long) query(statement).get(0).get("count"); } public long getPublishedCount(String docType) { - String statement = String.format(STATEMENT_GET_PUBLISHED_COUNT, quoteIdentifier(docType)); - return (Long) query(statement).get(0).get("count"); + String statement = String.format(STATEMENT_GET_PUBLISHED_COUNT, docType); + return (long) query(statement).get(0).get("count"); } - /* - * In fact, the URI should be the only input as there can only be one document at given URI; but the DB is split per document type for some reason. - */ - public DocumentList getDocumentByUri(String docType, String uri) { - return query(String.format(STATEMENT_GET_POST_BY_TYPE_AND_URI, quoteIdentifier(docType)), uri); + public DocumentList getDocumentByUri(String uri) { + return query("select * from Documents where sourceuri=?", uri); } - public DocumentList getDocumentStatus(String docType, String uri) { - String statement = String.format(STATEMENT_GET_DOCUMENT_STATUS_BY_DOCTYPE_AND_URI, quoteIdentifier(docType)); - return query(statement, uri); + public DocumentList getDocumentStatus(String uri) { + return query(STATEMENT_GET_DOCUMENT_STATUS_BY_DOCTYPE_AND_URI, uri); } - public DocumentList getPublishedPosts() { + public DocumentList getPublishedPosts() { return getPublishedContent("post"); } - public DocumentList getPublishedPosts(boolean applyPaging) { + public DocumentList getPublishedPosts(boolean applyPaging) { return getPublishedContent("post", applyPaging); } - public DocumentList getPublishedPostsByTag(String tag) { + public DocumentList getPublishedPostsByTag(String tag) { return query(STATEMENT_GET_PUBLISHED_POSTS_BY_TAG, tag); } - public DocumentList getPublishedDocumentsByTag(String tag) { - final DocumentList documents = new DocumentList(); + public DocumentList getPublishedDocumentsByTag(String tag) { + final DocumentList documents = new DocumentList<>(); for (final String docType : DocumentTypes.getDocumentTypes()) { - String statement = String.format(STATEMENT_GET_PUBLISHED_POST_BY_TYPE_AND_TAG, quoteIdentifier(docType)); - DocumentList documentsByTag = query(statement, tag); + String statement = String.format(STATEMENT_GET_PUBLISHED_POST_BY_TYPE_AND_TAG, docType); + DocumentList documentsByTag = query(statement, tag); documents.addAll(documentsByTag); } return documents; } - public DocumentList getPublishedPages() { + public DocumentList getPublishedPages() { return getPublishedContent("page"); } - public DocumentList getPublishedContent(String docType) { + public DocumentList getPublishedContent(String docType) { return getPublishedContent(docType, false); } - private DocumentList getPublishedContent(String docType, boolean applyPaging) { - String query = String.format(STATEMENT_GET_PUBLISHED_CONTENT_BY_DOCTYPE, quoteIdentifier(docType)); + private DocumentList getPublishedContent(String docType, boolean applyPaging) { + String query = String.format(STATEMENT_GET_PUBLISHED_CONTENT_BY_DOCTYPE, docType); if (applyPaging && hasStartAndLimitBoundary()) { query += " SKIP " + start + " LIMIT " + limit; } return query(query); } - public DocumentList getAllContent(String docType) { + public DocumentList getAllContent(String docType) { return getAllContent(docType, false); } - public DocumentList getAllContent(String docType, boolean applyPaging) { - String query = String.format(STATEMENT_GET_ALL_CONTENT_BY_DOCTYPE, quoteIdentifier(docType)); + public DocumentList getAllContent(String docType, boolean applyPaging) { + String query = String.format(STATEMENT_GET_ALL_CONTENT_BY_DOCTYPE, docType); if (applyPaging && hasStartAndLimitBoundary()) { query += " SKIP " + start + " LIMIT " + limit; } @@ -299,26 +254,24 @@ private boolean hasStartAndLimitBoundary() { return (start >= 0) && (limit > -1); } - private DocumentList getAllTagsFromPublishedPosts() { + private DocumentList getAllTagsFromPublishedPosts() { return query(STATEMENT_GET_TAGS_FROM_PUBLISHED_POSTS); } - private DocumentList getSignaturesForTemplates() { + private DocumentList getSignaturesForTemplates() { return query(STATEMENT_GET_SIGNATURE_FOR_TEMPLATES); } - public DocumentList getUnrenderedContent(String docType) { - String statement = String.format(STATEMENT_GET_UNDRENDERED_CONTENT, quoteIdentifier(docType)); - return query(statement); + public DocumentList getUnrenderedContent() { + return query(STATEMENT_GET_UNDRENDERED_CONTENT); } - public void deleteContent(String docType, String uri) { - String statement = String.format(STATEMENT_DELETE_DOCTYPE_BY_SOURCEURI, quoteIdentifier(docType)); - executeCommand(statement, uri); + public void deleteContent(String uri) { + executeCommand(STATEMENT_DELETE_DOCTYPE_BY_SOURCEURI, uri); } - public void markContentAsRendered(String docType) { - String statement = String.format(STATEMENT_MARK_CONTENT_AS_RENDERD, quoteIdentifier(docType)); + public void markContentAsRendered(DocumentModel document) { + String statement = String.format(STATEMENT_MARK_CONTENT_AS_RENDERD, document.getType(), document.getSourceuri()); executeCommand(statement); } @@ -327,7 +280,7 @@ private void updateSignatures(String currentTemplatesSignature) { } public void deleteAllByDocType(String docType) { - String statement = String.format(STATEMENT_DELETE_ALL, quoteIdentifier(docType)); + String statement = String.format(STATEMENT_DELETE_ALL, docType); executeCommand(statement); } @@ -335,13 +288,13 @@ private void insertTemplatesSignature(String currentTemplatesSignature) { executeCommand(STATEMENT_INSERT_TEMPLATES_SIGNATURE, currentTemplatesSignature); } - private DocumentList query(String sql) { + private DocumentList query(String sql) { activateOnCurrentThread(); OResultSet results = db.query(sql); return DocumentList.wrap(results); } - private DocumentList query(String sql, Object... args) { + private DocumentList query(String sql, Object... args) { activateOnCurrentThread(); OResultSet results = db.command(sql, args); return DocumentList.wrap(results); @@ -349,14 +302,14 @@ private DocumentList query(String sql, Object... args) { private void executeCommand(String query, Object... args) { activateOnCurrentThread(); - db.command(new OCommandSQL(query)).execute(args); + db.command(query, args); } public Set getTags() { - DocumentList docs = this.getAllTagsFromPublishedPosts(); + DocumentList docs = this.getAllTagsFromPublishedPosts(); Set result = new HashSet<>(); - for (Map document : docs) { - String[] tags = DBUtil.toStringArray(document.get(Crawler.Attributes.TAGS)); + for (DocumentModel document : docs) { + String[] tags = document.getTags(); Collections.addAll(result, tags); } return result; @@ -365,48 +318,39 @@ public Set getTags() { public Set getAllTags() { Set result = new HashSet<>(); for (String docType : DocumentTypes.getDocumentTypes()) { - String statement = String.format(STATEMENT_GET_TAGS_BY_DOCTYPE, quoteIdentifier(docType)); - DocumentList docs = query(statement); - for (Map document : docs) { - String[] tags = DBUtil.toStringArray(document.get(Crawler.Attributes.TAGS)); + String statement = String.format(STATEMENT_GET_TAGS_BY_DOCTYPE, docType); + DocumentList docs = query(statement); + for (DocumentModel document : docs) { + String[] tags = document.getTags(); Collections.addAll(result, tags); } } return result; } - private void createDocType(final OSchema schema, final String docType) { - logger.debug("Create document class '{}'", docType); - - - OClass page = schema.createClass(docType); - - // Primary key - String attribName = DocumentAttributes.SOURCE_URI.toString(); - page.createProperty(attribName, OType.STRING).setNotNull(true); - page.createIndex(docType + "sourceUriIndex", OClass.INDEX_TYPE.UNIQUE, attribName); - - attribName = DocumentAttributes.SHA1.toString(); - page.createProperty(attribName, OType.STRING).setNotNull(true); - page.createIndex(docType + "sha1Index", OClass.INDEX_TYPE.NOTUNIQUE, attribName); + private void createDocType(final OSchema schema) { + logger.debug("Create document class"); - attribName = DocumentAttributes.CACHED.toString(); - page.createProperty(attribName, OType.BOOLEAN).setNotNull(true); - page.createIndex(docType + "cachedIndex", OClass.INDEX_TYPE.NOTUNIQUE, attribName); + OClass page = schema.createClass(Schema.DOCUMENTS); + page.createProperty(ModelAttributes.SHA1, OType.STRING).setNotNull(true); + page.createIndex(Schema.DOCUMENTS + "sha1Index", OClass.INDEX_TYPE.NOTUNIQUE, ModelAttributes.SHA1); + page.createProperty(ModelAttributes.SOURCE_URI, OType.STRING).setNotNull(true); + page.createIndex(Schema.DOCUMENTS + "sourceUriIndex", OClass.INDEX_TYPE.UNIQUE, ModelAttributes.SOURCE_URI); + page.createProperty(ModelAttributes.CACHED, OType.BOOLEAN).setNotNull(true); + page.createIndex(Schema.DOCUMENTS + "cachedIndex", OClass.INDEX_TYPE.NOTUNIQUE, ModelAttributes.CACHED); + page.createProperty(ModelAttributes.RENDERED, OType.BOOLEAN).setNotNull(true); + page.createIndex(Schema.DOCUMENTS + "renderedIndex", OClass.INDEX_TYPE.NOTUNIQUE, ModelAttributes.RENDERED); + page.createProperty(ModelAttributes.STATUS, OType.STRING).setNotNull(true); + page.createIndex(Schema.DOCUMENTS + "statusIndex", OClass.INDEX_TYPE.NOTUNIQUE, ModelAttributes.STATUS); + page.createProperty(ModelAttributes.TYPE, OType.STRING).setNotNull(true); + page.createIndex(Schema.DOCUMENTS + "typeIndex", OClass.INDEX_TYPE.NOTUNIQUE, ModelAttributes.TYPE); - attribName = DocumentAttributes.RENDERED.toString(); - page.createProperty(attribName, OType.BOOLEAN).setNotNull(true); - page.createIndex(docType + "renderedIndex", OClass.INDEX_TYPE.NOTUNIQUE, attribName); - - attribName = DocumentAttributes.STATUS.toString(); - page.createProperty(attribName, OType.STRING).setNotNull(true); - page.createIndex(docType + "statusIndex", OClass.INDEX_TYPE.NOTUNIQUE, attribName); } private void createSignatureType(OSchema schema) { - OClass signatures = schema.createClass("Signatures"); - signatures.createProperty(String.valueOf(DocumentAttributes.SHA1), OType.STRING).setNotNull(true); - signatures.createIndex("sha1Idx", OClass.INDEX_TYPE.UNIQUE, DocumentAttributes.SHA1.toString()); + OClass signatures = schema.createClass(Schema.SIGNATURES); + signatures.createProperty(ModelAttributes.SHA1, OType.STRING).setNotNull(true); + signatures.createIndex("sha1Idx", OClass.INDEX_TYPE.UNIQUE, ModelAttributes.SHA1); } public void updateAndClearCacheIfNeeded(boolean needed, File templateFolder) { @@ -426,7 +370,7 @@ public void updateAndClearCacheIfNeeded(boolean needed, File templateFolder) { private boolean updateTemplateSignatureIfChanged(File templateFolder) { boolean templateSignatureChanged = false; - DocumentList docs = this.getSignaturesForTemplates(); + DocumentList docs = this.getSignaturesForTemplates(); String currentTemplatesSignature; try { currentTemplatesSignature = FileUtil.sha1(templateFolder); @@ -434,7 +378,7 @@ private boolean updateTemplateSignatureIfChanged(File templateFolder) { currentTemplatesSignature = ""; } if (!docs.isEmpty()) { - String sha1 = (String) docs.get(0).get(String.valueOf(DocumentAttributes.SHA1)); + String sha1 = docs.get(0).getSha1(); if (!sha1.equals(currentTemplatesSignature)) { this.updateSignatures(currentTemplatesSignature); templateSignatureChanged = true; @@ -461,11 +405,15 @@ public boolean isActive() { return db.isActiveOnCurrentThread(); } - static String quoteIdentifier(String input) { - if(input == null) { - return input; - } else { - return "`" + input.replaceAll("([\\\\`])", "\\\\$1") + "`"; - } + public void addDocument(DocumentModel document) { + OElement element = db.newElement(Schema.DOCUMENTS); + document.forEach((k, v) -> element.setProperty(k, v, OType.ANY)); + element.save(); } + + protected abstract class Schema { + static final String DOCUMENTS = "Documents"; + static final String SIGNATURES = "Signatures"; + } + } diff --git a/jbake-core/src/main/java/org/jbake/app/Crawler.java b/jbake-core/src/main/java/org/jbake/app/Crawler.java index 764a3e035..0c3d8b9d6 100644 --- a/jbake-core/src/main/java/org/jbake/app/Crawler.java +++ b/jbake-core/src/main/java/org/jbake/app/Crawler.java @@ -1,14 +1,14 @@ package org.jbake.app; import com.orientechnologies.orient.core.record.impl.ODocument; -import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.configuration2.CompositeConfiguration; import org.apache.commons.io.FilenameUtils; -import org.jbake.app.Crawler.Attributes.Status; import org.jbake.app.configuration.JBakeConfiguration; import org.jbake.app.configuration.JBakeConfigurationFactory; -import org.jbake.model.DocumentAttributes; +import org.jbake.model.DocumentModel; import org.jbake.model.DocumentStatus; import org.jbake.model.DocumentTypes; +import org.jbake.model.ModelAttributes; import org.jbake.util.HtmlUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,10 +28,10 @@ */ public class Crawler { - private static final Logger LOGGER = LoggerFactory.getLogger(Crawler.class); + private static final Logger logger = LoggerFactory.getLogger(Crawler.class); private final ContentStore db; - private JBakeConfiguration config; - private Parser parser; + private final JBakeConfiguration config; + private final Parser parser; /** * @param db Database instance for content @@ -63,14 +63,24 @@ public Crawler(ContentStore db, JBakeConfiguration config) { public void crawl() { crawl(config.getContentFolder()); - LOGGER.info("Content detected:"); + logger.info("Content detected:"); for (String docType : DocumentTypes.getDocumentTypes()) { long count = db.getDocumentCount(docType); if (count > 0) { - LOGGER.info("Parsed {} files of type: {}", count, docType); + logger.info("Parsed {} files of type: {}", count, docType); } } + } + + public void crawlDataFiles() { + crawlDataFiles(config.getDataFolder()); + logger.info("Data files detected:"); + String docType = config.getDataFileDocType(); + long count = db.getDocumentCount(docType); + if (count > 0) { + logger.info("Parsed {} files", count); + } } /** @@ -79,7 +89,49 @@ public void crawl() { * @param path Folder to start from */ private void crawl(File path) { - File[] contents = path.listFiles(FileUtil.getFileFilter()); + File[] contents = path.listFiles(FileUtil.getFileFilter(config)); + if (contents != null) { + Arrays.sort(contents); + for (File sourceFile : contents) { + if (sourceFile.isFile()) { + crawlFile(sourceFile); + } else if (sourceFile.isDirectory()) { + crawl(sourceFile); + } + } + } + } + + private void crawlFile(File sourceFile) { + + StringBuilder sb = new StringBuilder(); + sb.append("Processing [").append(sourceFile.getPath()).append("]... "); + String sha1 = buildHash(sourceFile); + String uri = buildURI(sourceFile); + DocumentStatus status = findDocumentStatus(uri, sha1); + if (status == DocumentStatus.UPDATED) { + sb.append(" : modified "); + db.deleteContent(uri); + } else if (status == DocumentStatus.IDENTICAL) { + sb.append(" : same "); + } else if (DocumentStatus.NEW == status) { + sb.append(" : new "); + } + + logger.info("{}", sb); + + if (status != DocumentStatus.IDENTICAL) { + processSourceFile(sourceFile, sha1, uri); + } + } + + /** + * Crawl all files and folders looking for data files. + * + * @param path Folder to start from + */ + private void crawlDataFiles(File path) { + File[] contents = path.listFiles(FileUtil.getDataFileFilter()); if (contents != null) { Arrays.sort(contents); for (File sourceFile : contents) { @@ -87,33 +139,31 @@ private void crawl(File path) { StringBuilder sb = new StringBuilder(); sb.append("Processing [").append(sourceFile.getPath()).append("]... "); String sha1 = buildHash(sourceFile); - String uri = buildURI(sourceFile); + String uri = buildDataFileURI(sourceFile); boolean process = true; DocumentStatus status = DocumentStatus.NEW; - for (String docType : DocumentTypes.getDocumentTypes()) { - status = findDocumentStatus(docType, uri, sha1); - if (status == DocumentStatus.UPDATED) { - sb.append(" : modified "); - db.deleteContent(docType, uri); - - } else if (status == DocumentStatus.IDENTICAL) { - sb.append(" : same "); - process = false; - } - if (!process) { - break; - } + String docType = config.getDataFileDocType(); + status = findDocumentStatus(uri, sha1); + if (status == DocumentStatus.UPDATED) { + sb.append(" : modified "); + db.deleteContent(uri); + } else if (status == DocumentStatus.IDENTICAL) { + sb.append(" : same "); + process = false; + } + if (!process) { + break; } if (DocumentStatus.NEW == status) { sb.append(" : new "); } if (process) { // new or updated - crawlSourceFile(sourceFile, sha1, uri); + crawlDataFile(sourceFile, sha1, uri, docType); } - LOGGER.info("{}", sb); + logger.info("{}", sb); } if (sourceFile.isDirectory()) { - crawl(sourceFile); + crawlDataFiles(sourceFile); } } } @@ -124,7 +174,7 @@ private String buildHash(final File sourceFile) { try { sha1 = FileUtil.sha1(sourceFile); } catch (Exception e) { - LOGGER.error("unable to build sha1 hash for source file '{}'", sourceFile); + logger.error("unable to build sha1 hash for source file '{}'", sourceFile); sha1 = ""; } return sha1; @@ -142,12 +192,21 @@ private String buildURI(final File sourceFile) { // strip off leading / to enable generating non-root based sites if (uri.startsWith(FileUtil.URI_SEPARATOR_CHAR)) { - uri = uri.substring(1, uri.length()); + uri = uri.substring(1); } return uri; } + private String buildDataFileURI(final File sourceFile) { + String uri = FileUtil.asPath(sourceFile).replace(FileUtil.asPath(config.getDataFolder()), ""); + // strip off leading / + if (uri.startsWith(FileUtil.URI_SEPARATOR_CHAR)) { + uri = uri.substring(1, uri.length()); + } + return uri; + } + // TODO: Refactor - parametrize the following two methods into one. // commons-codec's URLCodec could be used when we add that dependency. private String createUri(String uri) { @@ -184,50 +243,63 @@ private boolean useNoExtensionUri(String uri) { && uri.startsWith(noExtensionUriPrefix); } - private void crawlSourceFile(final File sourceFile, final String sha1, final String uri) { + private void crawlDataFile(final File sourceFile, final String sha1, final String uri, final String documentType) { try { - Map fileContents = parser.processFile(sourceFile); - if (fileContents != null) { - fileContents.put(Attributes.ROOTPATH, getPathToRoot(sourceFile)); - fileContents.put(String.valueOf(DocumentAttributes.SHA1), sha1); - fileContents.put(String.valueOf(DocumentAttributes.RENDERED), false); - if (fileContents.get(Attributes.TAGS) != null) { - // store them as a String[] - String[] tags = (String[]) fileContents.get(Attributes.TAGS); - fileContents.put(Attributes.TAGS, tags); - } - fileContents.put(Attributes.FILE, sourceFile.getPath()); - fileContents.put(String.valueOf(DocumentAttributes.SOURCE_URI), uri); - fileContents.put(Attributes.URI, uri); - - String documentType = (String) fileContents.get(Attributes.TYPE); - if (fileContents.get(Attributes.STATUS).equals(Status.PUBLISHED_DATE)) { - if (fileContents.get(Attributes.DATE) != null && (fileContents.get(Attributes.DATE) instanceof Date)) { - if (new Date().after((Date) fileContents.get(Attributes.DATE))) { - fileContents.put(Attributes.STATUS, Status.PUBLISHED); - } - } - } + DocumentModel document = parser.processFile(sourceFile); + if (document != null) { + document.setSha1(sha1); + document.setRendered(true); + document.setFile(sourceFile.getPath()); + document.setSourceUri(uri); + document.setType(documentType); + + db.addDocument(document); + } else { + logger.warn("{} couldn't be parsed so it has been ignored!", sourceFile); + } + } catch (Exception ex) { + throw new RuntimeException("Failed crawling file: " + sourceFile.getPath() + " " + ex.getMessage(), ex); + } + } - if (config.getUriWithoutExtension()) { - fileContents.put(Attributes.NO_EXTENSION_URI, uri.replace("/index.html", "/")); - } + private void processSourceFile(final File sourceFile, final String sha1, final String uri) { + DocumentModel document = parser.processFile(sourceFile); + + if (document != null) { + if (DocumentTypes.contains(document.getType())) { + addAdditionalDocumentAttributes(document, sourceFile, sha1, uri); if (config.getImgPathUpdate()) { // Prevent image source url's from breaking - HtmlUtil.fixImageSourceUrls(fileContents, config); + HtmlUtil.fixImageSourceUrls(document, config); } - ODocument doc = new ODocument(documentType); - doc.fromMap(fileContents); - boolean cached = fileContents.get(String.valueOf(DocumentAttributes.CACHED)) != null ? Boolean.valueOf((String) fileContents.get(String.valueOf(DocumentAttributes.CACHED))) : true; - doc.field(String.valueOf(DocumentAttributes.CACHED), cached); - doc.save(); + db.addDocument(document); } else { - LOGGER.warn("{} has an invalid header, it has been ignored!", sourceFile); + logger.warn("{} has an unknown document type '{}' and has been ignored!", sourceFile, document.getType()); } - } catch (Exception ex) { - throw new RuntimeException("Failed crawling file: " + sourceFile.getPath() + " " + ex.getMessage(), ex); + } else { + logger.warn("{} has an invalid header, it has been ignored!", sourceFile); + } + } + + private void addAdditionalDocumentAttributes(DocumentModel document, File sourceFile, String sha1, String uri) { + document.setRootPath(getPathToRoot(sourceFile)); + document.setSha1(sha1); + document.setRendered(false); + document.setFile(sourceFile.getPath()); + document.setSourceUri(uri); + document.setUri(uri); + document.setCached(true); + + if (document.getStatus().equals(ModelAttributes.Status.PUBLISHED_DATE) + && (document.getDate() != null) + && new Date().after(document.getDate())) { + document.setStatus(ModelAttributes.Status.PUBLISHED); + } + + if (config.getUriWithoutExtension()) { + document.setNoExtensionUri(uri.replace("/index.html", "/")); } } @@ -235,12 +307,12 @@ private String getPathToRoot(File sourceFile) { return FileUtil.getUriPathToContentRoot(config, sourceFile); } - private DocumentStatus findDocumentStatus(String docType, String uri, String sha1) { - DocumentList match = db.getDocumentStatus(docType, uri); + private DocumentStatus findDocumentStatus(String uri, String sha1) { + DocumentList match = db.getDocumentStatus(uri); if (!match.isEmpty()) { - Map entries = match.get(0); - String oldHash = (String) entries.get(String.valueOf(DocumentAttributes.SHA1)); - if (!(oldHash.equals(sha1)) || Boolean.FALSE.equals(entries.get(String.valueOf(DocumentAttributes.RENDERED)))) { + DocumentModel document = match.get(0); + String oldHash = document.getSha1(); + if (!oldHash.equals(sha1) || !document.getRendered()) { return DocumentStatus.UPDATED; } else { return DocumentStatus.IDENTICAL; @@ -250,40 +322,4 @@ private DocumentStatus findDocumentStatus(String docType, String uri, String sha } } - public abstract static class Attributes { - - public static final String DATE = "date"; - public static final String STATUS = "status"; - public static final String TYPE = "type"; - public static final String TITLE = "title"; - public static final String URI = "uri"; - public static final String FILE = "file"; - public static final String TAGS = "tags"; - public static final String TAG = "tag"; - public static final String ROOTPATH = "rootpath"; - public static final String ID = "id"; - public static final String NO_EXTENSION_URI = "noExtensionUri"; - public static final String ALLTAGS = "alltags"; - public static final String PUBLISHED_DATE = "published_date"; - public static final String BODY = "body"; - public static final String DB = "db"; - - private Attributes() { - } - - /** - * Possible values of the {@link Attributes#STATUS} property - * - * @author ndx - */ - public abstract static class Status { - public static final String PUBLISHED_DATE = "published-date"; - public static final String PUBLISHED = "published"; - public static final String DRAFT = "draft"; - - private Status() { - } - } - - } } diff --git a/jbake-core/src/main/java/org/jbake/app/DBUtil.java b/jbake-core/src/main/java/org/jbake/app/DBUtil.java index 20355e4d3..33568b450 100644 --- a/jbake-core/src/main/java/org/jbake/app/DBUtil.java +++ b/jbake-core/src/main/java/org/jbake/app/DBUtil.java @@ -4,10 +4,9 @@ import com.orientechnologies.orient.core.record.OElement; import com.orientechnologies.orient.core.sql.executor.OResult; import org.jbake.app.configuration.JBakeConfiguration; +import org.jbake.model.DocumentModel; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; +import java.util.ArrayList; public class DBUtil { private static ContentStore contentStore; @@ -37,12 +36,11 @@ public static void closeDataStore() { contentStore = null; } - public static Map documentToModel(OResult doc) { - Map result = new HashMap<>(); - Iterator fieldIterator = doc.getPropertyNames().iterator(); - while (fieldIterator.hasNext()) { - String entry = fieldIterator.next(); - result.put(entry, doc.getProperty(entry)); + public static DocumentModel documentToModel(OResult doc) { + DocumentModel result = new DocumentModel(); + + for (String key : doc.getPropertyNames()) { + result.put(key, doc.getProperty(key)); } return result; } @@ -60,6 +58,9 @@ public static String[] toStringArray(Object entry) { } else if (entry instanceof OTrackedList) { OTrackedList list = (OTrackedList) entry; return list.toArray(new String[list.size()]); + } else if (entry instanceof ArrayList) { + ArrayList list = (ArrayList) entry; + return list.toArray(new String[list.size()]); } return new String[0]; } diff --git a/jbake-core/src/main/java/org/jbake/app/DocumentList.java b/jbake-core/src/main/java/org/jbake/app/DocumentList.java index 9364db3ec..d297ff387 100644 --- a/jbake-core/src/main/java/org/jbake/app/DocumentList.java +++ b/jbake-core/src/main/java/org/jbake/app/DocumentList.java @@ -2,9 +2,9 @@ import com.orientechnologies.orient.core.sql.executor.OResult; import com.orientechnologies.orient.core.sql.executor.OResultSet; +import org.jbake.model.DocumentModel; import java.util.LinkedList; -import java.util.Map; /** * Wraps an OrientDB document iterator into a model usable by @@ -12,10 +12,10 @@ * * @author Cédric Champeau */ -public class DocumentList extends LinkedList> { +public class DocumentList extends LinkedList { - public static DocumentList wrap(OResultSet docs) { - DocumentList list = new DocumentList(); + public static DocumentList wrap(OResultSet docs) { + DocumentList list = new DocumentList<>(); while (docs.hasNext()) { OResult next = docs.next(); list.add(DBUtil.documentToModel(next)); diff --git a/jbake-core/src/main/java/org/jbake/app/FileUtil.java b/jbake-core/src/main/java/org/jbake/app/FileUtil.java index 2e1aa9e9d..c80dbf9e2 100644 --- a/jbake-core/src/main/java/org/jbake/app/FileUtil.java +++ b/jbake-core/src/main/java/org/jbake/app/FileUtil.java @@ -23,11 +23,32 @@ public class FileUtil { public static final String URI_SEPARATOR_CHAR = "/"; + /** + * Filters files based on their file extension. + * + * @param config the jbake configuration + * @return Object for filtering files + */ + public static FileFilter getFileFilter(JBakeConfiguration config) { + return new FileFilter() { + + @Override + public boolean accept(File pathname) { + //Accept if input is a non-hidden file with registered extension + //or if a non-hidden and not-ignored directory + return !pathname.isHidden() && (pathname.isFile() + && Engines.getRecognizedExtensions().contains(fileExt(pathname))) || (directoryOnlyIfNotIgnored(pathname, config)); + } + }; + } + /** * Filters files based on their file extension. * * @return Object for filtering files + * @deprecated use {@link #getFileFilter(JBakeConfiguration)} instead */ + @Deprecated public static FileFilter getFileFilter() { return new FileFilter() { @@ -41,11 +62,49 @@ public boolean accept(File pathname) { }; } + /** + * Filters files based on their file extension - only find data files (i.e. files with .yaml or .yml extension) + * + * @return Object for filtering files + */ + public static FileFilter getDataFileFilter() { + return new FileFilter() { + + @Override + public boolean accept(File pathname) { + return "yaml".equalsIgnoreCase(fileExt(pathname)) || "yml".equalsIgnoreCase(fileExt(pathname)); + } + }; + } + /** * Gets the list of files that are not content files based on their extension. * + * @param config the jbake configuration * @return FileFilter object */ + public static FileFilter getNotContentFileFilter(JBakeConfiguration config) { + return new FileFilter() { + + @Override + public boolean accept(File pathname) { + //Accept if input is a non-hidden file with NOT-registered extension + //or if a non-hidden and not-ignored directory + return !pathname.isHidden() && (pathname.isFile() + //extension should not be from registered content extensions + && !Engines.getRecognizedExtensions().contains(fileExt(pathname))) + || (directoryOnlyIfNotIgnored(pathname, config)); + } + }; + } + + /** + * Gets the list of files that are not content files based on their extension. + * + * @return FileFilter object + * @deprecated use {@link #getNotContentFileFilter(JBakeConfiguration)} instead + */ + @Deprecated public static FileFilter getNotContentFileFilter() { return new FileFilter() { @@ -61,12 +120,37 @@ public boolean accept(File pathname) { }; } + /** + * Ignores directory (and children) if it contains a file named in the + * configuration as a marker to ignore the directory. + * + * @param file the file to test + * @param config the jbake configuration + * @return true if file is directory and not ignored + */ + public static boolean directoryOnlyIfNotIgnored(File file, JBakeConfiguration config) { + boolean accept = false; + + FilenameFilter ignoreFile = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.equalsIgnoreCase(config.getIgnoreFileName()); + } + }; + + accept = file.isDirectory() && (file.listFiles(ignoreFile).length == 0); + + return accept; + } + /** * Ignores directory (and children) if it contains a file named ".jbakeignore". * - * @param file {@link File} - * @return {@link Boolean} true/false + * @param file the file to test + * @return true if file is directory and not ignored + * @deprecated use {@link #directoryOnlyIfNotIgnored(File, JBakeConfiguration)} instead */ + @Deprecated public static boolean directoryOnlyIfNotIgnored(File file) { boolean accept = false; @@ -179,7 +263,7 @@ public static String asPath(File file) { * platform independent file.getPath() * * @param path the path to transform, or {@code null} - * @return The result will have alle platform path separators replaced by "/". + * @return The result will have all platform path separators replaced by "/". */ public static String asPath(String path) { if (path == null) { diff --git a/jbake-core/src/main/java/org/jbake/app/JBakeException.java b/jbake-core/src/main/java/org/jbake/app/JBakeException.java index a3af314e4..f3e858a4a 100644 --- a/jbake-core/src/main/java/org/jbake/app/JBakeException.java +++ b/jbake-core/src/main/java/org/jbake/app/JBakeException.java @@ -1,5 +1,7 @@ package org.jbake.app; +import org.jbake.launcher.SystemExit; + /** * This runtime exception is thrown by JBake API to indicate an processing * error. @@ -9,18 +11,26 @@ public class JBakeException extends RuntimeException { private static final long serialVersionUID = 1L; + final private SystemExit exit; + /** + * * @param message * The error message. * @param cause * The causing exception or null if no cause * available. */ - public JBakeException(final String message, final Throwable cause) { + public JBakeException(final SystemExit exit, final String message, final Throwable cause) { super(message, cause); + this.exit = exit; + } + + public JBakeException(final SystemExit exit, final String message) { + this(exit, message, null); } - public JBakeException(final String message) { - this(message, null); + public int getExit() { + return exit.getStatus(); } } diff --git a/jbake-core/src/main/java/org/jbake/app/Oven.java b/jbake-core/src/main/java/org/jbake/app/Oven.java index 07b391491..ead7ce2cf 100644 --- a/jbake-core/src/main/java/org/jbake/app/Oven.java +++ b/jbake-core/src/main/java/org/jbake/app/Oven.java @@ -1,6 +1,7 @@ package org.jbake.app; -import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.configuration2.CompositeConfiguration; +import org.apache.commons.lang3.LocaleUtils; import org.jbake.app.configuration.DefaultJBakeConfiguration; import org.jbake.app.configuration.JBakeConfiguration; import org.jbake.app.configuration.JBakeConfigurationFactory; @@ -18,6 +19,7 @@ import java.util.Date; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.ServiceLoader; /** @@ -29,8 +31,8 @@ public class Oven { private static final Logger LOGGER = LoggerFactory.getLogger(Oven.class); - private Utensils utensils; - private List errors = new LinkedList<>(); + private final Utensils utensils; + private final List errors = new LinkedList<>(); private int renderedCount = 0; /** @@ -114,6 +116,16 @@ private void checkConfiguration(JBakeConfiguration configuration) { inspector.inspect(); } + /** + * Sets the Locale for the JVM + * + */ + private void setLocale() { + String localeString = getUtensils().getConfiguration().getJvmLocale(); + Locale locale = localeString != null ? LocaleUtils.toLocale(localeString) : Locale.getDefault(); + Locale.setDefault(locale); + } + /** * Responsible for incremental baking, typically a single file at a time. * @@ -139,6 +151,7 @@ public void bake() { JBakeConfiguration config = utensils.getConfiguration(); Crawler crawler = utensils.getCrawler(); Asset asset = utensils.getAsset(); + setLocale(); try { @@ -152,6 +165,9 @@ public void bake() { // process source content crawler.crawl(); + // process data files + crawler.crawlDataFiles(); + // render content renderContent(); @@ -187,6 +203,9 @@ private void updateDocTypesFromConfiguration() { for (String docType : config.getDocumentTypes()) { DocumentTypes.addDocumentType(docType); } + + // needs manually setting as this isn't defined in same way as document types for content files + DocumentTypes.addDocumentType(config.getDataFileDocType()); } private void resetDocumentTypesAndExtractors() { diff --git a/jbake-core/src/main/java/org/jbake/app/Parser.java b/jbake-core/src/main/java/org/jbake/app/Parser.java index d2da80ee9..129b01abf 100644 --- a/jbake-core/src/main/java/org/jbake/app/Parser.java +++ b/jbake-core/src/main/java/org/jbake/app/Parser.java @@ -1,13 +1,13 @@ package org.jbake.app; import org.jbake.app.configuration.JBakeConfiguration; +import org.jbake.model.DocumentModel; import org.jbake.parser.Engines; import org.jbake.parser.ParserEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.util.Map; /** * Parses a File for content. @@ -31,13 +31,13 @@ public Parser(JBakeConfiguration config) { /** * Process the file by parsing the contents. * - * @param file File input for parsing - * @return The contents of the file + * @param file File input for parsing + * @return The contents of the file */ - public Map processFile(File file) { + public DocumentModel processFile(File file) { ParserEngine engine = Engines.get(FileUtil.fileExt(file)); - if (engine==null) { - LOGGER.error("Unable to find suitable markup engine for {}",file); + if (engine == null) { + LOGGER.error("Unable to find suitable markup engine for {}", file); return null; } diff --git a/jbake-core/src/main/java/org/jbake/app/Renderer.java b/jbake-core/src/main/java/org/jbake/app/Renderer.java index 696569ec2..5508fd327 100644 --- a/jbake-core/src/main/java/org/jbake/app/Renderer.java +++ b/jbake-core/src/main/java/org/jbake/app/Renderer.java @@ -1,11 +1,13 @@ package org.jbake.app; -import org.apache.commons.configuration.CompositeConfiguration; -import org.jbake.app.Crawler.Attributes; +import org.apache.commons.configuration2.CompositeConfiguration; import org.jbake.app.configuration.DefaultJBakeConfiguration; import org.jbake.app.configuration.JBakeConfiguration; import org.jbake.app.configuration.JBakeConfigurationFactory; +import org.jbake.model.DocumentModel; +import org.jbake.model.ModelAttributes; import org.jbake.template.DelegatingTemplateEngine; +import org.jbake.template.model.TemplateModel; import org.jbake.util.PagingHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,10 +17,10 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import java.nio.file.Files; import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; /** * Render output to a file. @@ -30,8 +32,9 @@ public class Renderer { private static final String SITEMAP_TEMPLATE_NAME = "sitemap"; private static final String FEED_TEMPLATE_NAME = "feed"; private static final String ARCHIVE_TEMPLATE_NAME = "archive"; + private static final String ERROR404_TEMPLATE_NAME = "error404"; - private final Logger LOGGER = LoggerFactory.getLogger(Renderer.class); + private final Logger logger = LoggerFactory.getLogger(Renderer.class); private final JBakeConfiguration config; private final DelegatingTemplateEngine renderingEngine; private final ContentStore db; @@ -97,7 +100,7 @@ public Renderer(ContentStore db, JBakeConfiguration config, DelegatingTemplateEn } private String findTemplateName(String docType) { - return config.getTemplateFileByDocType(docType).getName(); + return config.getTemplateByDocType(docType); } /** @@ -106,9 +109,9 @@ private String findTemplateName(String docType) { * @param content The content to renderDocument * @throws Exception if IOException or SecurityException are raised */ - public void render(Map content) throws Exception { - String docType = (String) content.get(Crawler.Attributes.TYPE); - String outputFilename = config.getDestinationFolder().getPath() + File.separatorChar + content.get(Attributes.URI); + public void render(DocumentModel content) throws Exception { + String docType = content.getType(); + String outputFilename = config.getDestinationFolder().getPath() + File.separatorChar + content.getUri(); if (outputFilename.lastIndexOf('.') > outputFilename.lastIndexOf(File.separatorChar)) { outputFilename = outputFilename.substring(0, outputFilename.lastIndexOf('.')); } @@ -117,30 +120,30 @@ public void render(Map content) throws Exception { String outputExtension = config.getOutputExtensionByDocType(docType); File draftFile = new File(outputFilename, config.getDraftSuffix() + outputExtension); if (draftFile.exists()) { - draftFile.delete(); + Files.delete(draftFile.toPath()); } File publishedFile = new File(outputFilename + outputExtension); if (publishedFile.exists()) { - publishedFile.delete(); + Files.delete(publishedFile.toPath()); } - if (content.get(Crawler.Attributes.STATUS).equals(Crawler.Attributes.Status.DRAFT)) { + if (content.getStatus().equals(ModelAttributes.Status.DRAFT)) { outputFilename = outputFilename + config.getDraftSuffix(); } File outputFile = new File(outputFilename + outputExtension); - Map model = new HashMap(); - model.put("content", content); - model.put("renderer", renderingEngine); + TemplateModel model = new TemplateModel(); + model.setContent(content); + model.setRenderer(renderingEngine); try { try (Writer out = createWriter(outputFile)) { renderingEngine.renderDocument(model, findTemplateName(docType), out); } - LOGGER.info("Rendering [{}]... done!", outputFile); + logger.info("Rendering [{}]... done!", outputFile); } catch (Exception e) { - LOGGER.error("Rendering [{}]... failed!", outputFile, e); + logger.error("Rendering [{}]... failed!", outputFile, e); throw new Exception("Failed to render file " + outputFile.getAbsolutePath() + ". Cause: " + e.getMessage(), e); } } @@ -160,9 +163,9 @@ private void render(RenderingConfig renderConfig) throws Exception { try (Writer out = createWriter(outputFile)) { renderingEngine.renderDocument(renderConfig.getModel(), renderConfig.getTemplate(), out); } - LOGGER.info("Rendering {} [{}]... done!", renderConfig.getName(), outputFile); + logger.info("Rendering {} [{}]... done!", renderConfig.getName(), outputFile); } catch (Exception e) { - LOGGER.error("Rendering {} [{}]... failed!", renderConfig.getName(), outputFile, e); + logger.error("Rendering {} [{}]... failed!", renderConfig.getName(), outputFile, e); throw new Exception("Failed to render " + renderConfig.getName(), e); } } @@ -174,7 +177,6 @@ private void render(RenderingConfig renderConfig) throws Exception { * @throws Exception if IOException or SecurityException are raised */ public void renderIndex(String indexFile) throws Exception { - render(new DefaultRenderingConfig(indexFile, MASTERINDEX_TEMPLATE_NAME)); } @@ -188,9 +190,9 @@ public void renderIndexPaging(String indexFile) throws Exception { } else { PagingHelper pagingHelper = new PagingHelper(totalPosts, postsPerPage); - Map model = new HashMap(); - model.put("renderer", renderingEngine); - model.put("numberOfPages", pagingHelper.getNumberOfPages()); + TemplateModel model = new TemplateModel(); + model.setRenderer(renderingEngine); + model.setNumberOfPages(pagingHelper.getNumberOfPages()); try { db.setLimit(postsPerPage); @@ -198,18 +200,18 @@ public void renderIndexPaging(String indexFile) throws Exception { String fileName = indexFile; db.setStart(pageStart); - model.put("currentPageNumber", page); + model.setCurrentPageNuber(page); String previous = pagingHelper.getPreviousFileName(page); - model.put("previousFileName", previous); + model.setPreviousFilename(previous); String nextFileName = pagingHelper.getNextFileName(page); - model.put("nextFileName", nextFileName); + model.setNextFileName(nextFileName); - Map contentModel = buildSimpleModel(MASTERINDEX_TEMPLATE_NAME); + DocumentModel contentModel = buildSimpleModel(MASTERINDEX_TEMPLATE_NAME); - if(page > 1){ - contentModel.put(Attributes.ROOTPATH, "../"); + if (page > 1) { + contentModel.setRootPath("../"); } - model.put("content", contentModel); + model.setContent(contentModel); // Add page number to file name fileName = pagingHelper.getCurrentFileName(page, fileName); @@ -255,6 +257,16 @@ public void renderArchive(String archiveFile) throws Exception { render(new DefaultRenderingConfig(archiveFile, ARCHIVE_TEMPLATE_NAME)); } + /** + * Render an 404 file using the predefined template. + * + * @param errorFile The name of the output file + * @throws Exception if default rendering configuration is not loaded correctly + */ + public void renderError404(String errorFile) throws Exception { + render(new DefaultRenderingConfig(errorFile, ERROR404_TEMPLATE_NAME)); + } + /** * Render tag files using the supplied content. * @@ -268,16 +280,16 @@ public int renderTags(String tagPath) throws Exception { for (String tag : db.getAllTags()) { try { - Map model = new HashMap<>(); - model.put("renderer", renderingEngine); - model.put(Attributes.TAG, tag); - Map map = buildSimpleModel(Attributes.TAG); - model.put("content", map); - + TemplateModel model = new TemplateModel(); + model.setRenderer(renderingEngine); + model.setTag(tag); + DocumentModel map = buildSimpleModel(ModelAttributes.TAG.toString()); File path = new File(config.getDestinationFolder() + File.separator + tagPath + File.separator + tag + config.getOutputExtension()); - map.put(Attributes.ROOTPATH, FileUtil.getUriPathToDestinationRoot(config, path)); - render(new ModelRenderingConfig(path, Attributes.TAG, model, findTemplateName(Attributes.TAG))); + map.setRootPath(FileUtil.getUriPathToDestinationRoot(config, path)); + model.setContent(map); + + render(new ModelRenderingConfig(path, ModelAttributes.TAG.toString(), model, findTemplateName(ModelAttributes.TAG.toString()))); renderedCount++; } catch (Exception e) { @@ -290,13 +302,15 @@ public int renderTags(String tagPath) throws Exception { // Add an index file at root folder of tags. // This will prevent directory listing and also provide an option to // display all tags page. - Map model = new HashMap(); - model.put("renderer", renderingEngine); - Map map = buildSimpleModel(Attributes.TAGS); - model.put("content", map); - + TemplateModel model = new TemplateModel(); + model.setRenderer(renderingEngine); + DocumentModel map = buildSimpleModel(ModelAttributes.TAGS.toString()); File path = new File(config.getDestinationFolder() + File.separator + tagPath + File.separator + "index" + config.getOutputExtension()); - map.put(Attributes.ROOTPATH, FileUtil.getUriPathToDestinationRoot(config, path)); + + map.setRootPath(FileUtil.getUriPathToDestinationRoot(config, path)); + model.setContent(map); + + render(new ModelRenderingConfig(path, "tagindex", model, findTemplateName("tagsindex"))); renderedCount++; } catch (Exception e) { @@ -320,12 +334,12 @@ public int renderTags(String tagPath) throws Exception { * Builds simple map of values, which are exposed when rendering index/archive/sitemap/feed/tags. * * @param type - * @return + * @return a basic {@link DocumentModel} */ - private Map buildSimpleModel(String type) { - Map content = new HashMap(); - content.put(Attributes.TYPE, type); - content.put(Attributes.ROOTPATH, ""); + private DocumentModel buildSimpleModel(String type) { + DocumentModel content = new DocumentModel(); + content.setType(type); + content.setRootPath(""); // add any more keys here that need to have a default value to prevent need to perform null check in templates return content; } @@ -338,10 +352,10 @@ private interface RenderingConfig { String getTemplate(); - Map getModel(); + TemplateModel getModel(); } - private static abstract class AbstractRenderingConfig implements RenderingConfig { + private abstract static class AbstractRenderingConfig implements RenderingConfig { protected final File path; protected final String name; @@ -372,27 +386,27 @@ public String getTemplate() { } public class ModelRenderingConfig extends AbstractRenderingConfig { - private final Map model; + private final TemplateModel model; - public ModelRenderingConfig(String fileName, Map model, String templateType) { + public ModelRenderingConfig(String fileName, TemplateModel model, String templateType) { super(new File(config.getDestinationFolder(), fileName), fileName, findTemplateName(templateType)); this.model = model; } - public ModelRenderingConfig(File path, String name, Map model, String template) { + public ModelRenderingConfig(File path, String name, TemplateModel model, String template) { super(path, name, template); this.model = model; } @Override - public Map getModel() { + public TemplateModel getModel() { return model; } } class DefaultRenderingConfig extends AbstractRenderingConfig { - private final Object content; + private final DocumentModel content; private DefaultRenderingConfig(File path, String allInOneName) { super(path, allInOneName, findTemplateName(allInOneName)); @@ -415,16 +429,16 @@ public DefaultRenderingConfig(String allInOneName) { } @Override - public Map getModel() { - Map model = new HashMap(); - model.put("renderer", renderingEngine); - model.put("content", content); + public TemplateModel getModel() { + TemplateModel model = new TemplateModel(); + model.setRenderer(renderingEngine); + model.setContent(content); if (config.getPaginateIndex()) { - model.put("numberOfPages", 0); - model.put("currentPageNumber", 0); - model.put("previousFileName", ""); - model.put("nextFileName", ""); + model.setNumberOfPages(0); + model.setCurrentPageNuber(0); + model.setPreviousFilename(""); + model.setNextFileName(""); } return model; diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/ConfigUtil.java b/jbake-core/src/main/java/org/jbake/app/configuration/ConfigUtil.java index 08bcf0290..c0c6260f4 100644 --- a/jbake-core/src/main/java/org/jbake/app/configuration/ConfigUtil.java +++ b/jbake-core/src/main/java/org/jbake/app/configuration/ConfigUtil.java @@ -1,17 +1,19 @@ package org.jbake.app.configuration; -import org.apache.commons.configuration.CompositeConfiguration; -import org.apache.commons.configuration.ConfigurationException; -import org.apache.commons.configuration.PropertiesConfiguration; -import org.apache.commons.configuration.SystemConfiguration; +import org.apache.commons.configuration2.CompositeConfiguration; +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.commons.configuration2.SystemConfiguration; +import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder; +import org.apache.commons.configuration2.builder.fluent.Parameters; +import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler; +import org.apache.commons.configuration2.ex.ConfigurationException; import org.jbake.app.JBakeException; +import org.jbake.launcher.SystemExit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.charset.Charset; /** @@ -21,44 +23,115 @@ */ public class ConfigUtil { + public static final char LIST_DELIMITER = ','; + public static final String DEFAULT_ENCODING = "UTF-8"; private static final Logger LOGGER = LoggerFactory.getLogger(ConfigUtil.class); - private static final String LEGACY_CONFIG_FILE = "custom.properties"; - private static final String CONFIG_FILE = "jbake.properties"; - private static final String DEFAULT_CONFIG_FILE = "default.properties"; + public static final String LEGACY_CONFIG_FILE = "custom.properties"; + public static final String CONFIG_FILE = "jbake.properties"; + public static final String DEFAULT_CONFIG_FILE = "default.properties"; + private String encoding = DEFAULT_ENCODING; - private CompositeConfiguration load(File source) throws ConfigurationException { + private CompositeConfiguration load(File source, File propertiesFile) throws ConfigurationException { if (!source.exists()) { - throw new JBakeException("The given source folder '" + source.getAbsolutePath() + "' does not exist."); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "The given source folder '" + source.getAbsolutePath() + "' does not exist."); } if (!source.isDirectory()) { - throw new JBakeException("The given source folder is not a directory."); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR,"The given source folder is not a directory."); } + + File legacyConfigFile = new File(source, LEGACY_CONFIG_FILE); + File customConfigFile = propertiesFile != null ? propertiesFile : new File(source, CONFIG_FILE); + CompositeConfiguration config = new CompositeConfiguration(); - config.setListDelimiter(','); - File customConfigFile = new File(source, LEGACY_CONFIG_FILE); - if (customConfigFile.exists()) { + config.setListDelimiterHandler(new DefaultListDelimiterHandler(LIST_DELIMITER)); + + if (legacyConfigFile.exists()) { displayLegacyConfigFileWarningIfRequired(); - config.addConfiguration(new PropertiesConfiguration(customConfigFile)); + config.addConfiguration(getFileBasedPropertiesConfiguration(legacyConfigFile)); } - customConfigFile = new File(source, CONFIG_FILE); if (customConfigFile.exists()) { - config.addConfiguration(new PropertiesConfiguration(customConfigFile)); + config.addConfiguration(getFileBasedPropertiesConfiguration(customConfigFile)); } URL defaultPropertiesLocation = this.getClass().getClassLoader().getResource(DEFAULT_CONFIG_FILE); - config.addConfiguration(new PropertiesConfiguration(defaultPropertiesLocation)); + if (defaultPropertiesLocation != null) { + config.addConfiguration(getFileBasedPropertiesConfiguration(defaultPropertiesLocation)); + } + config.addConfiguration(new SystemConfiguration()); return config; } + private PropertiesConfiguration getFileBasedPropertiesConfiguration(File propertiesFile) throws ConfigurationException { + FileBasedConfigurationBuilder builder = + new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class) + .configure(new Parameters().properties() + .setFile(propertiesFile) + .setEncoding(encoding) + .setThrowExceptionOnMissing(true) + .setListDelimiterHandler(new DefaultListDelimiterHandler(LIST_DELIMITER)) + .setIncludesAllowed(false)); + return builder.getConfiguration(); + } + + private PropertiesConfiguration getFileBasedPropertiesConfiguration(URL propertiesFile) throws ConfigurationException { + FileBasedConfigurationBuilder builder = + new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class) + .configure(new Parameters().properties() + .setURL(propertiesFile) + .setEncoding(encoding) + .setThrowExceptionOnMissing(true) + .setListDelimiterHandler(new DefaultListDelimiterHandler(LIST_DELIMITER)) + .setIncludesAllowed(false)); + return builder.getConfiguration(); + } + private void displayLegacyConfigFileWarningIfRequired() { LOGGER.warn("You have defined a part of your JBake configuration in {}", LEGACY_CONFIG_FILE); LOGGER.warn("Usage of this file is being deprecated, please rename this file to: {} to remove this warning", CONFIG_FILE); } + /** + * Load a configuration. + * + * @param source the source directory of the project + * @param propertiesFile the properties file for the project + * @return the configuration + * @throws JBakeException if unable to configure + */ + public JBakeConfiguration loadConfig(File source, File propertiesFile) throws JBakeException { + try { + CompositeConfiguration configuration = load(source, propertiesFile); + return new DefaultJBakeConfiguration(source, configuration); + } catch (ConfigurationException e) { + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, e.getMessage(), e); + } + } + + /** + * Load a configuration. + * + * @param source the source directory of the project + * @return the configuration + * @throws ConfigurationException if unable to configure + * @deprecated use {@link #loadConfig(File, File)} instead + */ + @Deprecated public JBakeConfiguration loadConfig(File source) throws ConfigurationException { - CompositeConfiguration configuration = load(source); - return new DefaultJBakeConfiguration(source, configuration); + return loadConfig(source, null); } + public String getEncoding() { + return this.encoding; + } + + public ConfigUtil setEncoding(String encoding) { + if (Charset.isSupported(encoding)) { + this.encoding = encoding; + } else { + this.encoding = DEFAULT_ENCODING; + LOGGER.warn("Unsupported encoding '{}'. Using default encoding '{}'", encoding, this.encoding); + } + return this; + } } diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/DefaultJBakeConfiguration.java b/jbake-core/src/main/java/org/jbake/app/configuration/DefaultJBakeConfiguration.java index e28af05de..223b0f624 100644 --- a/jbake-core/src/main/java/org/jbake/app/configuration/DefaultJBakeConfiguration.java +++ b/jbake-core/src/main/java/org/jbake/app/configuration/DefaultJBakeConfiguration.java @@ -1,32 +1,33 @@ package org.jbake.app.configuration; -import org.apache.commons.configuration.CompositeConfiguration; -import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration2.CompositeConfiguration; +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.MapConfiguration; +import org.apache.commons.configuration2.SystemConfiguration; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.jbake.app.configuration.PropertyList.*; + /** * The default implementation of a {@link JBakeConfiguration} */ public class DefaultJBakeConfiguration implements JBakeConfiguration { + public static final String DEFAULT_TYHMELEAF_TEMPLATE_MODE = "HTML"; private static final String SOURCE_FOLDER_KEY = "sourceFolder"; private static final String DESTINATION_FOLDER_KEY = "destinationFolder"; private static final String ASSET_FOLDER_KEY = "assetFolder"; private static final String TEMPLATE_FOLDER_KEY = "templateFolder"; private static final String CONTENT_FOLDER_KEY = "contentFolder"; + private static final String DATA_FOLDER_KEY = "dataFolder"; private static final Pattern TEMPLATE_DOC_PATTERN = Pattern.compile("(?:template\\.)([a-zA-Z0-9-_]+)(?:\\.file)"); private static final String DOCTYPE_FILE_POSTFIX = ".file"; private static final String DOCTYPE_EXTENSION_POSTFIX = ".extension"; @@ -58,7 +59,7 @@ public Object get(String key) { @Override public String getArchiveFileName() { - return getAsString(JBakeProperty.ARCHIVE_FILE); + return getAsString(ARCHIVE_FILE.getKey()); } private boolean getAsBoolean(String key) { @@ -74,7 +75,7 @@ private int getAsInt(String key, int defaultValue) { } private List getAsList(String key) { - return Arrays.asList(compositeConfiguration.getStringArray(key)); + return compositeConfiguration.getList(String.class, key); } private String getAsString(String key) { @@ -87,24 +88,24 @@ private String getAsString(String key, String defaultValue) { @Override public List getAsciidoctorAttributes() { - return getAsList(JBakeProperty.ASCIIDOCTOR_ATTRIBUTES); + return getAsList(ASCIIDOCTOR_ATTRIBUTES.getKey()); } - public Object getAsciidoctorOption(String optionKey) { - Configuration subConfig = compositeConfiguration.subset(JBakeProperty.ASCIIDOCTOR_OPTION); - Object value = subConfig.getProperty(optionKey); + public List getAsciidoctorOption(String optionKey) { + Configuration subConfig = compositeConfiguration.subset(ASCIIDOCTOR_OPTION.getKey()); - if (value == null) { - logger.warn("Cannot find asciidoctor option '{}.{}'", JBakeProperty.ASCIIDOCTOR_OPTION, optionKey); - return ""; + if (subConfig.containsKey(optionKey)) { + return subConfig.getList(String.class, optionKey); + } else { + logger.warn("Cannot find asciidoctor option '{}.{}'", ASCIIDOCTOR_OPTION.getKey(), optionKey); + return Collections.emptyList(); } - return value; } @Override public List getAsciidoctorOptionKeys() { List options = new ArrayList<>(); - Configuration subConfig = compositeConfiguration.subset(JBakeProperty.ASCIIDOCTOR_OPTION); + Configuration subConfig = compositeConfiguration.subset(ASCIIDOCTOR_OPTION.getKey()); Iterator iterator = subConfig.getKeys(); while (iterator.hasNext()) { @@ -128,35 +129,35 @@ public void setAssetFolder(File assetFolder) { @Override public String getAssetFolderName() { - return getAsString(JBakeProperty.ASSET_FOLDER); + return getAsString(ASSET_FOLDER.getKey()); } @Override public boolean getAssetIgnoreHidden() { - return getAsBoolean(JBakeProperty.ASSET_IGNORE_HIDDEN); + return getAsBoolean(ASSET_IGNORE_HIDDEN.getKey()); } public void setAssetIgnoreHidden(boolean assetIgnoreHidden) { - setProperty(JBakeProperty.ASSET_IGNORE_HIDDEN, assetIgnoreHidden); + setProperty(ASSET_IGNORE_HIDDEN.getKey(), assetIgnoreHidden); } @Override public String getAttributesExportPrefixForAsciidoctor() { - return getAsString(JBakeProperty.ASCIIDOCTOR_ATTRIBUTES_EXPORT_PREFIX, ""); + return getAsString(ASCIIDOCTOR_ATTRIBUTES_EXPORT_PREFIX.getKey(), ""); } @Override public String getBuildTimeStamp() { - return getAsString(JBakeProperty.BUILD_TIMESTAMP); + return getAsString(BUILD_TIMESTAMP.getKey()); } @Override public boolean getClearCache() { - return getAsBoolean(JBakeProperty.CLEAR_CACHE); + return getAsBoolean(CLEAR_CACHE.getKey()); } public void setClearCache(boolean clearCache) { - setProperty(JBakeProperty.CLEAR_CACHE, clearCache); + setProperty(CLEAR_CACHE.getKey(), clearCache); } public CompositeConfiguration getCompositeConfiguration() { @@ -180,48 +181,75 @@ public void setContentFolder(File contentFolder) { @Override public String getContentFolderName() { - return getAsString(JBakeProperty.CONTENT_FOLDER); + return getAsString(CONTENT_FOLDER.getKey()); + } + + @Override + public File getDataFolder() { + return getAsFolder(DATA_FOLDER_KEY); + } + + public void setDataFolder(File dataFolder) { + if (dataFolder != null) { + setProperty(DATA_FOLDER_KEY, dataFolder); + } + } + + @Override + public String getDataFolderName() { + return getAsString(DATA_FOLDER.getKey()); + } + + @Override + public String getDataFileDocType() { + return getAsString(DATA_FILE_DOCTYPE.getKey()); + } + + public void setDataFileDocType(String dataFileDocType) { + setProperty(DATA_FILE_DOCTYPE.getKey(), dataFileDocType); } @Override public String getDatabasePath() { - return getAsString(JBakeProperty.DB_PATH); + return getAsString(DB_PATH.getKey()); } public void setDatabasePath(String path) { - setProperty(JBakeProperty.DB_PATH, path); + setProperty(DB_PATH.getKey(), path); } @Override public String getDatabaseStore() { - return getAsString(JBakeProperty.DB_STORE); + return getAsString(DB_STORE.getKey()); } public void setDatabaseStore(String storeType) { - setProperty(JBakeProperty.DB_STORE, storeType); + setProperty(DB_STORE.getKey(), storeType); } + + @Override public String getDateFormat() { - return getAsString(JBakeProperty.DATE_FORMAT); + return getAsString(DATE_FORMAT.getKey()); } @Override public String getDefaultStatus() { - return getAsString(JBakeProperty.DEFAULT_STATUS, ""); + return getAsString(DEFAULT_STATUS.getKey(), ""); } public void setDefaultStatus(String status) { - setProperty(JBakeProperty.DEFAULT_STATUS, status); + setProperty(DEFAULT_STATUS.getKey(), status); } @Override public String getDefaultType() { - return getAsString(JBakeProperty.DEFAULT_TYPE, ""); + return getAsString(DEFAULT_TYPE.getKey(), ""); } public void setDefaultType(String type) { - setProperty(JBakeProperty.DEFAULT_TYPE, type); + setProperty(DEFAULT_TYPE.getKey(), type); } @Override @@ -252,7 +280,12 @@ public List getDocumentTypes() { @Override public String getDraftSuffix() { - return getAsString(JBakeProperty.DRAFT_SUFFIX, ""); + return getAsString(DRAFT_SUFFIX.getKey(), ""); + } + + @Override + public String getError404FileName() { + return getAsString(ERROR404_FILE.getKey()); } @Override @@ -262,17 +295,22 @@ public String getExampleProjectByType(String templateType) { @Override public boolean getExportAsciidoctorAttributes() { - return getAsBoolean(JBakeProperty.ASCIIDOCTOR_ATTRIBUTES_EXPORT); + return getAsBoolean(ASCIIDOCTOR_ATTRIBUTES_EXPORT.getKey()); } @Override public String getFeedFileName() { - return getAsString(JBakeProperty.FEED_FILE); + return getAsString(FEED_FILE.getKey()); + } + + @Override + public String getIgnoreFileName() { + return getAsString(IGNORE_FILE.getKey()); } @Override public String getIndexFileName() { - return getAsString(JBakeProperty.INDEX_FILE); + return getAsString(INDEX_FILE.getKey()); } @Override @@ -282,20 +320,20 @@ public Iterator getKeys() { @Override public List getMarkdownExtensions() { - return getAsList(JBakeProperty.MARKDOWN_EXTENSIONS); + return getAsList(MARKDOWN_EXTENSIONS.getKey()); } public void setMarkdownExtensions(String... extensions) { - setProperty(JBakeProperty.MARKDOWN_EXTENSIONS, StringUtils.join(extensions, ",")); + setProperty(MARKDOWN_EXTENSIONS.getKey(), StringUtils.join(extensions, ",")); } @Override public String getOutputExtension() { - return getAsString(JBakeProperty.OUTPUT_EXTENSION); + return getAsString(OUTPUT_EXTENSION.getKey()); } public void setOutputExtension(String outputExtension) { - setProperty(JBakeProperty.OUTPUT_EXTENSION, outputExtension); + setProperty(OUTPUT_EXTENSION.getKey(), outputExtension); } @Override @@ -307,96 +345,106 @@ public String getOutputExtensionByDocType(String docType) { @Override public boolean getPaginateIndex() { - return getAsBoolean(JBakeProperty.PAGINATE_INDEX); + return getAsBoolean(PAGINATE_INDEX.getKey()); } public void setPaginateIndex(boolean paginateIndex) { - setProperty(JBakeProperty.PAGINATE_INDEX, paginateIndex); + setProperty(PAGINATE_INDEX.getKey(), paginateIndex); } @Override public int getPostsPerPage() { - return getAsInt(JBakeProperty.POSTS_PER_PAGE, 5); + return getAsInt(POSTS_PER_PAGE.getKey(), 5); } public void setPostsPerPage(int postsPerPage) { - setProperty(JBakeProperty.POSTS_PER_PAGE, postsPerPage); + setProperty(POSTS_PER_PAGE.getKey(), postsPerPage); } @Override public String getPrefixForUriWithoutExtension() { - return getAsString(JBakeProperty.URI_NO_EXTENSION_PREFIX); + return getAsString(URI_NO_EXTENSION_PREFIX.getKey()); } public void setPrefixForUriWithoutExtension(String prefix) { - setProperty(JBakeProperty.URI_NO_EXTENSION_PREFIX, prefix); + setProperty(URI_NO_EXTENSION_PREFIX.getKey(), prefix); } @Override public boolean getRenderArchive() { - return getAsBoolean(JBakeProperty.RENDER_ARCHIVE); + return getAsBoolean(RENDER_ARCHIVE.getKey()); } @Override public String getRenderEncoding() { - return getAsString(JBakeProperty.RENDER_ENCODING); + return getAsString(RENDER_ENCODING.getKey()); + } + + @Override + public String getOutputEncoding() { + return getAsString(OUTPUT_ENCODING.getKey()); + } + + @Override + public boolean getRenderError404() { + return getAsBoolean(RENDER_ERROR404.getKey()); } @Override public boolean getRenderFeed() { - return getAsBoolean(JBakeProperty.RENDER_FEED); + return getAsBoolean(RENDER_FEED.getKey()); } @Override public boolean getRenderIndex() { - return getAsBoolean(JBakeProperty.RENDER_INDEX); + return getAsBoolean(RENDER_INDEX.getKey()); } @Override public boolean getRenderSiteMap() { - return getAsBoolean(JBakeProperty.RENDER_SITEMAP); + return getAsBoolean(RENDER_SITEMAP.getKey()); } @Override public boolean getRenderTags() { - return getAsBoolean(JBakeProperty.RENDER_TAGS); + return getAsBoolean(RENDER_TAGS.getKey()); } @Override public boolean getRenderTagsIndex() { - return compositeConfiguration.getBoolean(JBakeProperty.RENDER_TAGS_INDEX, false); + return compositeConfiguration.getBoolean(RENDER_TAGS_INDEX.getKey(), false); } public void setRenderTagsIndex(boolean enable) { - compositeConfiguration.setProperty(JBakeProperty.RENDER_TAGS_INDEX, enable); + compositeConfiguration.setProperty(RENDER_TAGS_INDEX.getKey(), enable); } @Override public boolean getSanitizeTag() { - return getAsBoolean(JBakeProperty.TAG_SANITIZE); + return getAsBoolean(TAG_SANITIZE.getKey()); } @Override public int getServerPort() { - return getAsInt(JBakeProperty.SERVER_PORT, 8080); + return getAsInt(SERVER_PORT.getKey(), 8080); } public void setServerPort(int port) { - setProperty(JBakeProperty.SERVER_PORT, port); + setProperty(SERVER_PORT.getKey(), port); } @Override public String getSiteHost() { - return getAsString(JBakeProperty.SITE_HOST, "http://www.jbake.org"); + return getAsString(SITE_HOST.getKey(), "http://www.jbake.org"); } public void setSiteHost(String siteHost) { - setProperty(JBakeProperty.SITE_HOST, siteHost); + setProperty(SITE_HOST.getKey(), siteHost); } @Override public String getSiteMapFileName() { - return getAsString(JBakeProperty.SITEMAP_FILE); + return getAsString(SITEMAP_FILE.getKey()); } @Override @@ -411,25 +459,34 @@ public void setSourceFolder(File sourceFolder) { @Override public String getTagPathName() { - return getAsString(JBakeProperty.TAG_PATH); + return getAsString(TAG_PATH.getKey()); } @Override public String getTemplateEncoding() { - return getAsString(JBakeProperty.TEMPLATE_ENCODING); + return getAsString(TEMPLATE_ENCODING.getKey()); } @Override - public File getTemplateFileByDocType(String docType) { + public String getTemplateByDocType(String docType) { String templateKey = DOCTYPE_TEMPLATE_PREFIX + docType + DOCTYPE_FILE_POSTFIX; String templateFileName = getAsString(templateKey); if (templateFileName != null) { - return new File(getTemplateFolder(), templateFileName); + return templateFileName; } logger.warn("Cannot find configuration key '{}' for document type '{}'", templateKey, docType); return null; } + @Override + public File getTemplateFileByDocType(String docType) { + String templateFileName = getTemplateByDocType(docType); + if (templateFileName != null) { + return new File(getTemplateFolder(), templateFileName); + } + return null; + } + @Override public File getTemplateFolder() { return getAsFolder(TEMPLATE_FOLDER_KEY); @@ -443,30 +500,30 @@ public void setTemplateFolder(File templateFolder) { @Override public String getTemplateFolderName() { - return getAsString(JBakeProperty.TEMPLATE_FOLDER); + return getAsString(TEMPLATE_FOLDER.getKey()); } @Override public String getThymeleafLocale() { - return getAsString(JBakeProperty.THYMELEAF_LOCALE); + return getAsString(THYMELEAF_LOCALE.getKey()); } @Override public boolean getUriWithoutExtension() { - return getAsBoolean(JBakeProperty.URI_NO_EXTENSION); + return getAsBoolean(URI_NO_EXTENSION.getKey()); } public void setUriWithoutExtension(boolean withoutExtension) { - setProperty(JBakeProperty.URI_NO_EXTENSION, withoutExtension); + setProperty(URI_NO_EXTENSION.getKey(), withoutExtension); } @Override public String getVersion() { - return getAsString(JBakeProperty.VERSION); + return getAsString(VERSION.getKey()); } public void setDestinationFolderName(String folderName) { - setProperty(JBakeProperty.DESTINATION_FOLDER, folderName); + setProperty(DESTINATION_FOLDER.getKey(), folderName); setupDefaultDestination(); } @@ -477,17 +534,43 @@ public void setExampleProject(String type, String fileName) { @Override public void setProperty(String key, Object value) { + compositeConfiguration.setProperty(key, value); } + @Override + public String getThymeleafModeByType(String type) { + String key = "template_" + type + "_thymeleaf_mode"; + return getAsString(key, DEFAULT_TYHMELEAF_TEMPLATE_MODE); + } + @Override public String getServerContextPath() { - return getAsString(JBakeProperty.SERVER_CONTEXT_PATH); + return getAsString(SERVER_CONTEXT_PATH.getKey()); } @Override public String getServerHostname() { - return getAsString(JBakeProperty.SERVER_HOSTNAME); + return getAsString(SERVER_HOSTNAME.getKey()); + } + + @Override + public Map asHashMap() { + HashMap configModel = new HashMap<>(); + Iterator configKeys = this.getKeys(); + while (configKeys.hasNext()) { + String key = configKeys.next(); + Object valueObject; + + if (key.equals(PAGINATE_INDEX.getKey())) { + valueObject = this.getPaginateIndex(); + } else { + valueObject = this.get(key); + } + //replace "." in key so you can use dot notation in templates + configModel.put(key.replace(".", "_"), valueObject); + } + return configModel; } public void setTemplateExtensionForDocType(String docType, String extension) { @@ -505,10 +588,11 @@ private void setupPaths() { setupDefaultAssetFolder(); setupDefaultTemplateFolder(); setupDefaultContentFolder(); + setupDefaultDataFolder(); } private void setupDefaultDestination() { - String destinationPath = getAsString(JBakeProperty.DESTINATION_FOLDER); + String destinationPath = getAsString(DESTINATION_FOLDER.getKey()); File destination = new File(destinationPath); if ( destination.isAbsolute() ) { @@ -519,7 +603,7 @@ private void setupDefaultDestination() { } private void setupDefaultAssetFolder() { - String assetFolder = getAsString(JBakeProperty.ASSET_FOLDER); + String assetFolder = getAsString(ASSET_FOLDER.getKey()); File asset = new File(assetFolder); @@ -531,7 +615,7 @@ private void setupDefaultAssetFolder() { } private void setupDefaultTemplateFolder() { - String templateFolder = getAsString(JBakeProperty.TEMPLATE_FOLDER); + String templateFolder = getAsString(TEMPLATE_FOLDER.getKey()); File template = new File(templateFolder); if(template.isAbsolute()) { @@ -541,34 +625,90 @@ private void setupDefaultTemplateFolder() { } } + private void setupDefaultDataFolder() { + String dataFolder = getAsString(DATA_FOLDER.getKey()); + + File data = new File(dataFolder); + if(data.isAbsolute()) { + setDataFolder(data); + } else { + setDataFolder(new File(getSourceFolder(), dataFolder)); + } + } + private void setupDefaultContentFolder() { setContentFolder(new File(getSourceFolder(), getContentFolderName())); } @Override public String getHeaderSeparator() { - return getAsString(JBakeProperty.HEADER_SEPARATOR); + return getAsString(HEADER_SEPARATOR.getKey()); } public void setHeaderSeparator(String headerSeparator) { - setProperty(JBakeProperty.HEADER_SEPARATOR, headerSeparator); + setProperty(HEADER_SEPARATOR.getKey(), headerSeparator); } @Override public boolean getImgPathPrependHost() { - return getAsBoolean(JBakeProperty.IMG_PATH_PREPEND_HOST); + return getAsBoolean(IMG_PATH_PREPEND_HOST.getKey()); } public void setImgPathPrependHost(boolean imgPathPrependHost) { - setProperty(JBakeProperty.IMG_PATH_PREPEND_HOST, imgPathPrependHost); + setProperty(IMG_PATH_PREPEND_HOST.getKey(), imgPathPrependHost); } @Override public boolean getImgPathUpdate() { - return getAsBoolean(JBakeProperty.IMG_PATH_UPDATE); + return getAsBoolean(IMG_PATH_UPDATE.getKey()); } public void setImgPathUPdate(boolean imgPathUpdate) { - setProperty(JBakeProperty.IMG_PATH_UPDATE, imgPathUpdate); + setProperty(IMG_PATH_UPDATE.getKey(), imgPathUpdate); + } + + public List getJbakeProperties() { + + List jbakeKeys = new ArrayList<>(); + + for (int i = 0; i < compositeConfiguration.getNumberOfConfigurations(); i++) { + Configuration configuration = compositeConfiguration.getConfiguration(i); + + if (!(configuration instanceof SystemConfiguration)) { + for (Iterator it = configuration.getKeys(); it.hasNext(); ) { + String key = it.next(); + Property property = PropertyList.getPropertyByKey(key); + if (!jbakeKeys.contains(property)) { + jbakeKeys.add(property); + } + } + } + } + Collections.sort(jbakeKeys); + return jbakeKeys; + } + + @Override + public void addConfiguration(Properties properties) { + compositeConfiguration.addConfiguration(new MapConfiguration(properties)); + } + + @Override + public String getAbbreviatedGitHash() { + return getAsString(GIT_HASH.getKey()); + } + + @Override + public String getJvmLocale() { + return getAsString(JVM_LOCALE.getKey()); + } + + @Override + public TimeZone getFreemarkerTimeZone() { + String timezone = getAsString(FREEMARKER_TIMEZONE.getKey()); + if (StringUtils.isNotEmpty(timezone)) { + return TimeZone.getTimeZone(timezone); + } + return null; } } diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfiguration.java b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfiguration.java index 218c62177..98e2625db 100644 --- a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfiguration.java +++ b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfiguration.java @@ -3,6 +3,9 @@ import java.io.File; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; /** * JBakeConfiguration gives you access to the project configuration. Typically located in a file called jbake.properties. @@ -84,6 +87,21 @@ public interface JBakeConfiguration { */ String getContentFolderName(); + /** + * @return the data folder + */ + File getDataFolder(); + + /** + * @return name of Folder where data files reside in + */ + String getDataFolderName(); + + /** + * @return docType for data files + */ + String getDataFileDocType(); + /** * @return Folder to store database files in */ @@ -123,6 +141,11 @@ public interface JBakeConfiguration { */ String getDraftSuffix(); + /** + * @return Output filename for error404 file, is only used when {@link #getRenderError404()} is true + */ + String getError404FileName(); + /** * Get name for example project name by given template type * @@ -137,7 +160,7 @@ public interface JBakeConfiguration { boolean getExportAsciidoctorAttributes(); /** - * @return Output filename for feed file, is only used when {@link #getRenderFeed()} der} is true + * @return Output filename for feed file, is only used when {@link #getRenderFeed()} is true */ String getFeedFileName(); @@ -147,6 +170,11 @@ public interface JBakeConfiguration { */ String getHeaderSeparator(); + /** + * @return Filename to use to ignore a directory in addition to ".jbakeignore" + */ + String getIgnoreFileName(); + /** * @return Output filename for index, is only used when {@link #getRenderIndex()} is true */ @@ -200,6 +228,16 @@ public interface JBakeConfiguration { */ String getRenderEncoding(); + /** + * @return Output encoding for freemarker url escaping + */ + String getOutputEncoding(); + + /** + * @return Flag indicating if error404 file should be generated + */ + boolean getRenderError404(); + /** * @return Flag indicating if feed file should be generated */ @@ -260,7 +298,9 @@ public interface JBakeConfiguration { */ String getTemplateEncoding(); - File getTemplateFileByDocType(String masterindex); + String getTemplateByDocType(String doctype); + + File getTemplateFileByDocType(String doctype); /** * @return the template folder @@ -307,8 +347,37 @@ public interface JBakeConfiguration { */ void setProperty(String key, Object value); + /** + * + * @param type the documents type + * @return the the thymeleaf render mode ( defaults to {@link DefaultJBakeConfiguration#DEFAULT_TYHMELEAF_TEMPLATE_MODE} ) + */ + String getThymeleafModeByType(String type); + String getServerContextPath(); String getServerHostname(); + + /** + * @return Abbreviated hash of latest git commit + */ + String getAbbreviatedGitHash(); + + /** + * @return Locale to set in the JVM + */ + String getJvmLocale(); + + /** + * + * @return TimeZone to use within Freemarker + */ + TimeZone getFreemarkerTimeZone(); + + Map asHashMap(); + + List getJbakeProperties(); + + void addConfiguration(Properties properties); } diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationFactory.java b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationFactory.java index 06220e5fb..1ff3fa764 100644 --- a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationFactory.java +++ b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationFactory.java @@ -1,7 +1,8 @@ package org.jbake.app.configuration; -import org.apache.commons.configuration.CompositeConfiguration; -import org.apache.commons.configuration.ConfigurationException; + +import org.apache.commons.configuration2.CompositeConfiguration; +import org.jbake.app.JBakeException; import java.io.File; @@ -17,19 +18,31 @@ public JBakeConfigurationFactory() { } /** - * Creates a {@link DefaultJBakeConfiguration} + * Creates a {@link DefaultJBakeConfiguration} using default.properties and jbake.properties + * * @param sourceFolder The source folder of the project * @param destination The destination folder to render and copy files to * @param isClearCache Whether to clear database cache or not * @return A configuration by given parameters - * @throws ConfigurationException if loading the configuration fails + * @throws JBakeException if loading the configuration fails */ - public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, File destination, boolean isClearCache) throws ConfigurationException { + public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, File destination, boolean isClearCache) throws JBakeException { + return createDefaultJbakeConfiguration(sourceFolder, destination, (File) null, isClearCache); + } - DefaultJBakeConfiguration configuration = (DefaultJBakeConfiguration) getConfigUtil().loadConfig(sourceFolder); + /** + * Creates a {@link DefaultJBakeConfiguration} + * @param sourceFolder The source folder of the project + * @param destination The destination folder to render and copy files to + * @param propertiesFile The properties file for the project + * @param isClearCache Whether to clear database cache or not + * @return A configuration by given parameters + * @throws JBakeException if loading the configuration fails + */ + public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, File destination, File propertiesFile, boolean isClearCache) throws JBakeException { + DefaultJBakeConfiguration configuration = (DefaultJBakeConfiguration) getConfigUtil().loadConfig(sourceFolder, propertiesFile); configuration.setDestinationFolder(destination); configuration.setClearCache(isClearCache); - return configuration; } @@ -43,8 +56,10 @@ public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFold * @param compositeConfiguration A given {@link CompositeConfiguration} * @param isClearCache Whether to clear database cache or not * @return A configuration by given parameters + * @deprecated use {@link #createDefaultJbakeConfiguration(File, File, File, boolean)} instead */ - public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, File destination, CompositeConfiguration compositeConfiguration, boolean isClearCache) { + @Deprecated + public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, File destination, CompositeConfiguration compositeConfiguration, boolean isClearCache) throws JBakeException { DefaultJBakeConfiguration configuration = new DefaultJBakeConfiguration(sourceFolder, compositeConfiguration); configuration.setDestinationFolder(destination); configuration.setClearCache(isClearCache); @@ -60,8 +75,10 @@ public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFold * @param destination The destination folder to render and copy files to * @param compositeConfiguration A given {@link CompositeConfiguration} * @return A configuration by given parameters + * @deprecated use {@link #createDefaultJbakeConfiguration(File, File, File, boolean)} instead */ - public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, File destination, CompositeConfiguration compositeConfiguration) { + @Deprecated + public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, File destination, CompositeConfiguration compositeConfiguration) throws JBakeException { DefaultJBakeConfiguration configuration = new DefaultJBakeConfiguration(sourceFolder, compositeConfiguration); configuration.setDestinationFolder(destination); return configuration; @@ -75,7 +92,8 @@ public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFold * @param config A {@link CompositeConfiguration} * @return A configuration by given parameters */ - public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, CompositeConfiguration config) { + @Deprecated + public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFolder, CompositeConfiguration config) throws JBakeException { return new DefaultJBakeConfiguration(sourceFolder,config); } @@ -88,10 +106,28 @@ public DefaultJBakeConfiguration createDefaultJbakeConfiguration(File sourceFold * @param destinationFolder The destination folder to render and copy files to * @param isClearCache Whether to clear database cache or not * @return A configuration by given parameters - * @throws ConfigurationException if loading the configuration fails + * @throws JBakeException if loading the configuration fails + * @deprecated use {@link #createJettyJbakeConfiguration(File, File, File, boolean)} instead + */ + @Deprecated + public DefaultJBakeConfiguration createJettyJbakeConfiguration(File sourceFolder, File destinationFolder, boolean isClearCache) throws JBakeException { + return createJettyJbakeConfiguration(sourceFolder, destinationFolder, (File)null, isClearCache); + } + + /** + * Creates a {@link DefaultJBakeConfiguration} with value site.host replaced + * by http://localhost:[server.port]. + * The server.port is read from the project properties file. + * + * @param sourceFolder The source folder of the project + * @param destinationFolder The destination folder to render and copy files to + * @param propertiesFile The properties file for the project + * @param isClearCache Whether to clear database cache or not + * @return A configuration by given parameters + * @throws JBakeException if loading the configuration fails */ - public DefaultJBakeConfiguration createJettyJbakeConfiguration(File sourceFolder, File destinationFolder, boolean isClearCache) throws ConfigurationException { - DefaultJBakeConfiguration configuration = (DefaultJBakeConfiguration) getConfigUtil().loadConfig(sourceFolder); + public DefaultJBakeConfiguration createJettyJbakeConfiguration(File sourceFolder, File destinationFolder, File propertiesFile, boolean isClearCache) throws JBakeException { + DefaultJBakeConfiguration configuration = (DefaultJBakeConfiguration) getConfigUtil().loadConfig(sourceFolder, propertiesFile); configuration.setDestinationFolder(destinationFolder); configuration.setClearCache(isClearCache); configuration.setSiteHost("http://" + configuration.getServerHostname() + ":" +configuration.getServerPort() + configuration.getServerContextPath()); @@ -105,4 +141,9 @@ public ConfigUtil getConfigUtil() { public void setConfigUtil(ConfigUtil configUtil) { this.configUtil = configUtil; } + + public JBakeConfigurationFactory setEncoding(String charset) { + this.configUtil.setEncoding(charset); + return this; + } } diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationInspector.java b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationInspector.java index 2fb214d96..abe3737cf 100644 --- a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationInspector.java +++ b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationInspector.java @@ -2,11 +2,15 @@ import org.jbake.app.FileUtil; import org.jbake.app.JBakeException; +import org.jbake.launcher.SystemExit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; +import static org.jbake.app.configuration.PropertyList.CONTENT_FOLDER; +import static org.jbake.app.configuration.PropertyList.TEMPLATE_FOLDER; + public class JBakeConfigurationInspector { private static final Logger LOGGER = LoggerFactory.getLogger(JBakeConfigurationInspector.class); @@ -28,21 +32,21 @@ public void inspect() throws JBakeException { private void ensureSource() throws JBakeException { File source = configuration.getSourceFolder(); if (!FileUtil.isExistingFolder(source)) { - throw new JBakeException("Error: Source folder must exist: " + source.getAbsolutePath()); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "Error: Source folder must exist: " + source.getAbsolutePath()); } if (!configuration.getSourceFolder().canRead()) { - throw new JBakeException("Error: Source folder is not readable: " + source.getAbsolutePath()); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "Error: Source folder is not readable: " + source.getAbsolutePath()); } } private void ensureTemplateFolder() { File path = configuration.getTemplateFolder(); - checkRequiredFolderExists(JBakeProperty.TEMPLATE_FOLDER, path); + checkRequiredFolderExists(TEMPLATE_FOLDER.getKey(), path); } private void ensureContentFolder() { File path = configuration.getContentFolder(); - checkRequiredFolderExists(JBakeProperty.CONTENT_FOLDER, path); + checkRequiredFolderExists(CONTENT_FOLDER.getKey(), path); } private void ensureDestination() { @@ -51,7 +55,7 @@ private void ensureDestination() { destination.mkdirs(); } if (!destination.canWrite()) { - throw new JBakeException("Error: Destination folder is not writable: " + destination.getAbsolutePath()); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "Error: Destination folder is not writable: " + destination.getAbsolutePath()); } } @@ -62,9 +66,9 @@ private void checkAssetFolder() { } } - private void checkRequiredFolderExists(String property, File path) { + private void checkRequiredFolderExists(String folderName, File path) { if (!FileUtil.isExistingFolder(path)) { - throw new JBakeException("Error: Required folder cannot be found! Expected to find [" + property + "] at: " + path.getAbsolutePath()); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "Error: Required folder cannot be found! Expected to find [" + folderName + "] at: " + path.getAbsolutePath()); } } diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeProperty.java b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeProperty.java deleted file mode 100644 index 0c5159ccd..000000000 --- a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeProperty.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.jbake.app.configuration; - -public class JBakeProperty { - - public static final String ARCHIVE_FILE = "archive.file"; - public static final String ASCIIDOCTOR_ATTRIBUTES = "asciidoctor.attributes"; - public static final String ASCIIDOCTOR_ATTRIBUTES_EXPORT = "asciidoctor.attributes.export"; - public static final String ASCIIDOCTOR_OPTION = "asciidoctor.option"; - public static final String ASCIIDOCTOR_ATTRIBUTES_EXPORT_PREFIX = "asciidoctor.attributes.export.prefix"; - public static final String ASSET_FOLDER = "asset.folder"; - public static final String ASSET_IGNORE_HIDDEN = "asset.ignore"; - public static final String BUILD_TIMESTAMP = "build.timestamp"; - public static final String CLEAR_CACHE = "db.clear.cache"; - public static final String CONTENT_FOLDER = "content.folder"; - public static final String DATE_FORMAT = "date.format"; - public static final String DB_STORE = "db.store"; - public static final String DB_PATH = "db.path"; - public static final String DEFAULT_STATUS = "default.status"; - public static final String DEFAULT_TYPE = "default.type"; - public static final String DESTINATION_FOLDER = "destination.folder"; - public static final String DRAFT_SUFFIX = "draft.suffix"; - public static final String FEED_FILE = "feed.file"; - public static final String HEADER_SEPARATOR = "header.separator"; - public static final String INDEX_FILE = "index.file"; - public static final String MARKDOWN_EXTENSIONS = "markdown.extensions"; - public static final String OUTPUT_EXTENSION = "output.extension"; - public static final String PAGINATE_INDEX = "index.paginate"; - public static final String POSTS_PER_PAGE = "index.posts_per_page"; - public static final String RENDER_ARCHIVE = "render.archive"; - public static final String RENDER_FEED = "render.feed"; - public static final String RENDER_INDEX = "render.index"; - public static final String RENDER_SITEMAP = "render.sitemap"; - public static final String RENDER_TAGS = "render.tags"; - public static final String RENDER_TAGS_INDEX = "render.tagsindex"; - public static final String RENDER_ENCODING = "render.encoding"; - public static final String SERVER_PORT = "server.port"; - public static final String SERVER_HOSTNAME = "server.hostname"; - public static final String SERVER_CONTEXT_PATH = "server.contextPath"; - public static final String SITE_HOST = "site.host"; - public static final String SITEMAP_FILE = "sitemap.file"; - public static final String TAG_SANITIZE = "tag.sanitize"; - public static final String TAG_PATH = "tag.path"; - public static final String TEMPLATE_FOLDER = "template.folder"; - public static final String TEMPLATE_ENCODING = "template.encoding"; - public static final String THYMELEAF_LOCALE = "thymeleaf.locale"; - public static final String URI_NO_EXTENSION = "uri.noExtension"; - public static final String URI_NO_EXTENSION_PREFIX = "uri.noExtension.prefix"; - public static final String IMG_PATH_UPDATE = "img.path.update"; - public static final String IMG_PATH_PREPEND_HOST = "img.path.prepend.host"; - public static final String VERSION = "version"; - - private JBakeProperty() {} - -} diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/Property.java b/jbake-core/src/main/java/org/jbake/app/configuration/Property.java new file mode 100644 index 000000000..3ea349763 --- /dev/null +++ b/jbake-core/src/main/java/org/jbake/app/configuration/Property.java @@ -0,0 +1,72 @@ +package org.jbake.app.configuration; + +import java.util.Objects; + +public class Property implements Comparable { + + private final String key; + private final String description; + private final Group group; + + public Property(String key, String description) { + this(key, description, Group.DEFAULT); + } + + public Property(String key, String description, Group group) { + this.key = key; + this.description = description; + this.group = group; + } + + public String getKey() { + return key; + } + + public String getDescription() { + return description; + } + + public Group getGroup() { + return group; + } + + @Override + public String toString() { + return getKey(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Property property = (Property) o; + return Objects.equals(key, property.key) && + Objects.equals(description, property.description) && + group == property.group; + } + + @Override + public int hashCode() { + return Objects.hash(key, description, group); + } + + @Override + public int compareTo(Property other) { + int result = this.getGroup().compareTo(other.getGroup()); + + if (result == 0) { + result = this.getKey().compareTo(other.getKey()); + } + return result; + } + + public enum Group { + DEFAULT, CUSTOM + } + + +} diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/PropertyList.java b/jbake-core/src/main/java/org/jbake/app/configuration/PropertyList.java new file mode 100644 index 000000000..99db20081 --- /dev/null +++ b/jbake-core/src/main/java/org/jbake/app/configuration/PropertyList.java @@ -0,0 +1,371 @@ +package org.jbake.app.configuration; + +import java.lang.reflect.Field; + +import static org.jbake.app.configuration.Property.Group.CUSTOM; + +public abstract class PropertyList { + + public static final Property ARCHIVE_FILE = new Property( + "archive.file", + "Output filename for archive file" + ); + + public static final Property ASCIIDOCTOR_ATTRIBUTES = new Property( + "asciidoctor.attributes", + "attributes to be set when processing input" + ); + + public static final Property ASCIIDOCTOR_ATTRIBUTES_EXPORT = new Property( + "asciidoctor.attributes.export", + "Prefix to be used when exporting JBake properties to Asciidoctor" + ); + + public static final Property ASCIIDOCTOR_ATTRIBUTES_EXPORT_PREFIX = new Property( + "asciidoctor.attributes.export.prefix", + "prefix that should be used when JBake config options are exported" + ); + + public static final Property ASCIIDOCTOR_OPTION = new Property( + "asciidoctor.option", + "default asciidoctor options" + ); + + public static final Property ASSET_FOLDER = new Property( + "asset.folder", + "folder that contains all asset files" + ); + + public static final Property ASSET_IGNORE_HIDDEN = new Property( + "asset.ignore", + "Flag indicating if hidden asset resources should be ignored" + ); + + public static final Property BUILD_TIMESTAMP = new Property( + "build.timestamp", + "timestamp jbake was build"); + + public static final Property CLEAR_CACHE = new Property( + "db.clear.cache", + "clear database cache" + ); + + public static final Property CONTENT_FOLDER = new Property( + "content.folder", + "folder that contains all content files" + ); + + public static final Property DATA_FOLDER = new Property( + "data.folder", + "folder that contains all data files" + ); + + public static final Property DATA_FILE_DOCTYPE = new Property( + "data.file.docType", + "document type to use for data files" + ); + + public static final Property DATE_FORMAT = new Property( + "date.format", + "default date format used in content files" + ); + + public static final Property DB_STORE = new Property( + "db.store", + "database store (plocal, memory)" + ); + + public static final Property DB_PATH = new Property( + "db.path", + "database path for persistent storage" + ); + + public static final Property DEFAULT_STATUS = new Property( + "default.status", + "default document status" + ); + + public static final Property DEFAULT_TYPE = new Property( + "default.type", + "default document type" + ); + + public static final Property DESTINATION_FOLDER = new Property( + "destination.folder", + "path to destination folder by default" + ); + + public static final Property DRAFT_SUFFIX = new Property( + "draft.suffix", + "draft content suffix" + ); + + public static final Property ERROR404_FILE = new Property( + "error404.file", + "filename to use for 404 error" + ); + + public static final Property FEED_FILE = new Property( + "feed.file", + "filename to use for feed" + ); + + public static final Property FREEMARKER_TIMEZONE = new Property( + "freemarker.timezone", + "TimeZone to use within Freemarker" + ); + + public static final Property GIT_HASH = new Property( + "git.hash", + "abbreviated git hash" + ); + + public static final Property HEADER_SEPARATOR = new Property( + "header.separator", + "String used to separate the header from the body" + ); + + public static final Property IGNORE_FILE = new Property( + "ignore.file", + "file used to ignore a directory" + ); + + public static final Property IMG_PATH_UPDATE = new Property( + "img.path.update", + "update image path?" + ); + + public static final Property IMG_PATH_PREPEND_HOST = new Property( + "img.path.prepend.host", + "Prepend site.host to image paths" + ); + + public static final Property INDEX_FILE = new Property( + "index.file", + "filename to use for index file" + ); + + public static final Property JVM_LOCALE = new Property( + "jvm.locale", + "locale for the jvm" + ); + + public static final Property MARKDOWN_EXTENSIONS = new Property( + "markdown.extensions", + "comma delimited default markdown extensions; for available extensions: http://www.decodified.com/pegdown/api/org/pegdown/Extensions.html" + ); + + public static final Property OUTPUT_ENCODING = new Property( + "freemarker.outputencoding", + "default output_encoding setting for freemarker" + ); + + public static final Property OUTPUT_EXTENSION = new Property( + "output.extension", + "file extension for output content files" + ); + + public static final Property PAGINATE_INDEX = new Property( + "index.paginate", + "paginate index?" + ); + + public static final Property POSTS_PER_PAGE = new Property( + "index.posts_per_page", + "number of post per page for pagination" + ); + + public static final Property RENDER_ARCHIVE = new Property( + "render.archive", + "render archive file?" + ); + + public static final Property RENDER_ENCODING = new Property( + "render.encoding", + "character encoding MIME name used in templates. use one of http://www.iana.org/assignments/character-sets/character-sets.xhtml" + ); + + public static final Property RENDER_ERROR404 = new Property( + "render.error404", + "render 404 page?" + ); + + public static final Property RENDER_FEED = new Property( + "render.feed", + "render feed file?" + ); + + public static final Property RENDER_INDEX = new Property( + "render.index", + "render index file?" + ); + + public static final Property RENDER_SITEMAP = new Property( + "render.sitemap", + "render sitemap.xml file?" + ); + + public static final Property RENDER_TAGS = new Property( + "render.tags", + "render tag files?" + ); + + public static final Property RENDER_TAGS_INDEX = new Property( + "render.tagsindex", + "render tag index file?" + ); + + public static final Property SERVER_PORT = new Property( + "server.port", + "default server port" + ); + + public static final Property SERVER_HOSTNAME = new Property( + "server.hostname", + "default server hostname" + ); + + public static final Property SERVER_CONTEXT_PATH = new Property( + "server.contextPath", + "default server context path" + ); + + public static final Property SITE_HOST = new Property( + "site.host", + "site host" + ); + + public static final Property SITEMAP_FILE = new Property( + "sitemap.file", + "filename to use for sitemap file" + ); + + public static final Property TAG_SANITIZE = new Property( + "tag.sanitize", + "sanitize tag value before it is used as filename (i.e. replace spaces with hyphens)" + ); + + public static final Property TAG_PATH = new Property( + "tag.path", + "folder name to use for tag files" + ); + + public static final Property TEMPLATE_FOLDER = new Property( + "template.folder", + "folder that contains all template files" + ); + + public static final Property TEMPLATE_ENCODING = new Property( + "template.encoding", + "character encoding MIME name used in templates. use one of http://www.iana.org/assignments/character-sets/character-sets.xhtml" + ); + + public static final Property TEMPLATE_MASTERINDEX_FILE = new Property( + "template.masterindex.file", + "filename of masterindex template file" + ); + + public static final Property TEMPLATE_FEED_FILE = new Property( + "template.feed.file", + "filename of feed template file" + ); + + public static final Property TEMPLATE_ARCHIVE_FILE = new Property( + "template.archive.file", + "filename of archive template file" + ); + + public static final Property TEMPLATE_TAG_FILE = new Property( + "template.tag.file", + "filename of tag template file" + ); + + public static final Property TEMPLATE_TAGSINDEX_FILE = new Property( + "template.tagsindex.file", + "filename of tag index template file" + ); + + public static final Property TEMPLATE_SITEMAP_FILE = new Property( + "template.sitemap.file", + "filename of sitemap template file" + ); + + public static final Property TEMPLATE_POST_FILE = new Property( + "template.post.file", + "filename of post template file" + ); + + public static final Property TEMPLATE_PAGE_FILE = new Property( + "template.page.file", + "filename of page template file" + ); + + public static final Property EXAMPLE_PROJECT_FREEMARKER = new Property( + "example.project.freemarker", + "zip file containing example project structure using freemarker templates" + ); + + public static final Property EXAMPLE_PROJECT_GROOVY = new Property( + "example.project.groovy", + "zip file containing example project structure using groovy templates" + ); + + public static final Property EXAMPLE_PROJECT_GROOVY_MTE = new Property( + "example.project.groovy-mte", + "zip file containing example project structure using groovy markup templates" + ); + + public static final Property EXAMPLE_PROJECT_THYMELEAF = new Property( + "example.project.thymeleaf", + "zip file containing example project structure using thymeleaf templates" + ); + + public static final Property EXAMPLE_PROJECT_JADE = new Property( + "example.project.jade", + "zip file containing example project structure using jade templates" + ); + + public static final Property MARKDOWN_MAX_PARSINGTIME = new Property( + "markdown.maxParsingTimeInMillis", + "millis to parse single markdown page. See PegDown Parse configuration for details" + ); + + public static final Property THYMELEAF_LOCALE = new Property( + "thymeleaf.locale", + "default thymeleafe locale" + ); + + public static final Property URI_NO_EXTENSION = new Property( + "uri.noExtension", + "enable extension-less URI option?" + ); + + public static final Property URI_NO_EXTENSION_PREFIX = new Property( + "uri.noExtension.prefix", + "Set to a prefix path (starting with a slash) for which to generate extension-less URI's (i.e. a folder with index.html in)" + ); + + public static final Property VERSION = new Property( + "version", + "jbake application version" + ); + + private PropertyList() { + } + + public static Property getPropertyByKey(String key) { + + for (Field field : PropertyList.class.getFields()) { + try { + Property property = (Property) field.get(null); + + if (property.getKey().equals(key)) { + return property; + } + } catch (IllegalAccessException e) { + return new Property(key, "", CUSTOM); + } + } + return new Property(key, "", CUSTOM); + } +} diff --git a/jbake-core/src/main/java/org/jbake/launcher/BakeOff.java b/jbake-core/src/main/java/org/jbake/launcher/BakeOff.java new file mode 100644 index 000000000..29bd60a29 --- /dev/null +++ b/jbake-core/src/main/java/org/jbake/launcher/BakeOff.java @@ -0,0 +1,10 @@ +package org.jbake.launcher; + +public final class BakeOff { + + private BakeOff() {} + + public static void exit(int status) { + System.exit(status); + } +} diff --git a/jbake-core/src/main/java/org/jbake/launcher/BakeWatcher.java b/jbake-core/src/main/java/org/jbake/launcher/BakeWatcher.java index b6e2df1e8..3512cb83d 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/BakeWatcher.java +++ b/jbake-core/src/main/java/org/jbake/launcher/BakeWatcher.java @@ -1,6 +1,6 @@ package org.jbake.launcher; -import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.configuration2.CompositeConfiguration; import org.apache.commons.vfs2.FileObject; import org.apache.commons.vfs2.FileSystemException; import org.apache.commons.vfs2.FileSystemManager; @@ -18,7 +18,7 @@ */ public class BakeWatcher { - private Logger logger = LoggerFactory.getLogger(BakeWatcher.class); + private final Logger logger = LoggerFactory.getLogger(BakeWatcher.class); /** * Starts watching the file system for changes to trigger a bake. @@ -45,13 +45,15 @@ public void start(JBakeConfiguration config) { FileObject listenPath = fsMan.resolveFile(config.getContentFolder().toURI()); FileObject templateListenPath = fsMan.resolveFile(config.getTemplateFolder().toURI()); FileObject assetPath = fsMan.resolveFile(config.getAssetFolder().toURI()); + FileObject dataPath = fsMan.resolveFile(config.getDataFolder().toURI()); - logger.info("Watching for (content, template, asset) changes in [{}]", config.getSourceFolder().getPath()); + logger.info("Watching for (content, data, template, asset) changes in [{}]", config.getSourceFolder().getPath()); DefaultFileMonitor monitor = new DefaultFileMonitor(new CustomFSChangeListener(config)); monitor.setRecursive(true); monitor.addFile(listenPath); monitor.addFile(templateListenPath); monitor.addFile(assetPath); + monitor.addFile(dataPath); monitor.start(); } catch (FileSystemException e) { logger.error("Problems watching filesystem changes", e); diff --git a/jbake-core/src/main/java/org/jbake/launcher/Baker.java b/jbake-core/src/main/java/org/jbake/launcher/Baker.java index a1dcb373e..536625c98 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/Baker.java +++ b/jbake-core/src/main/java/org/jbake/launcher/Baker.java @@ -1,6 +1,6 @@ package org.jbake.launcher; -import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.configuration2.CompositeConfiguration; import org.jbake.app.JBakeException; import org.jbake.app.Oven; import org.jbake.app.configuration.JBakeConfiguration; @@ -41,7 +41,7 @@ public void bake(final JBakeConfiguration config) { msg.append(MessageFormat.format("{0}. {1}\n", errNr, error.getMessage())); ++errNr; } - throw new JBakeException(msg.toString(), errors.get(0)); + throw new JBakeException(SystemExit.ERROR ,msg.toString(), errors.get(0)); } } } diff --git a/jbake-core/src/main/java/org/jbake/launcher/CustomFSChangeListener.java b/jbake-core/src/main/java/org/jbake/launcher/CustomFSChangeListener.java index bd8135b54..3c6006177 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/CustomFSChangeListener.java +++ b/jbake-core/src/main/java/org/jbake/launcher/CustomFSChangeListener.java @@ -12,9 +12,9 @@ public class CustomFSChangeListener implements FileListener { - private final static Logger LOGGER = LoggerFactory.getLogger(CustomFSChangeListener.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CustomFSChangeListener.class); - private JBakeConfiguration config; + private final JBakeConfiguration config; public CustomFSChangeListener(JBakeConfiguration config) { this.config = config; @@ -22,20 +22,20 @@ public CustomFSChangeListener(JBakeConfiguration config) { @Override public void fileCreated(FileChangeEvent event) throws Exception { - LOGGER.info("File created event detected: {}", event.getFile().getURL()); - exec(event.getFile()); + LOGGER.info("File created event detected: {}", event.getFileObject().getURL()); + exec(event.getFileObject()); } @Override public void fileDeleted(FileChangeEvent event) throws Exception { - LOGGER.info("File deleted event detected: {}", event.getFile().getURL()); - exec(event.getFile()); + LOGGER.info("File deleted event detected: {}", event.getFileObject().getURL()); + exec(event.getFileObject()); } @Override public void fileChanged(FileChangeEvent event) throws Exception { - LOGGER.info("File changed event detected: {}", event.getFile().getURL()); - exec(event.getFile()); + LOGGER.info("File changed event detected: {}", event.getFileObject().getURL()); + exec(event.getFileObject()); } private void exec(FileObject file) { diff --git a/jbake-core/src/main/java/org/jbake/launcher/Init.java b/jbake-core/src/main/java/org/jbake/launcher/Init.java index 2a62f7875..ae1dd1bca 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/Init.java +++ b/jbake-core/src/main/java/org/jbake/launcher/Init.java @@ -1,6 +1,6 @@ package org.jbake.launcher; -import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.configuration2.CompositeConfiguration; import org.jbake.app.ZipUtil; import org.jbake.app.configuration.DefaultJBakeConfiguration; import org.jbake.app.configuration.JBakeConfiguration; @@ -16,7 +16,7 @@ */ public class Init { - private JBakeConfiguration config; + private final JBakeConfiguration config; /** * @param config The project configuration diff --git a/jbake-core/src/main/java/org/jbake/launcher/JettyServer.java b/jbake-core/src/main/java/org/jbake/launcher/JettyServer.java index 8e00af26c..cff105932 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/JettyServer.java +++ b/jbake-core/src/main/java/org/jbake/launcher/JettyServer.java @@ -7,6 +7,7 @@ import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.ResourceHandler; +import org.jbake.app.JBakeException; import org.jbake.app.configuration.JBakeConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,41 +35,42 @@ public void run(String resourceBase, String port) { public void run(String resourceBase, JBakeConfiguration configuration) { run(resourceBase, configuration.getServerContextPath(), configuration.getServerHostname(), configuration.getServerPort()); } + /** * Run Jetty web server serving out supplied path on supplied port * * @param resourceBase Base directory for resources to be served - * @param port Required server port + * @param port Required server port */ private void run(String resourceBase, String contextPath, String hostname, int port) { - server = new Server(); - ServerConnector connector = new ServerConnector(server); - connector.setHost(hostname); - connector.setPort(port); - server.addConnector(connector); + try { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setHost(hostname); + connector.setPort(port); + server.addConnector(connector); - ResourceHandler resource_handler = new ResourceHandler(); - resource_handler.setDirectoriesListed(true); - resource_handler.setWelcomeFiles(new String[]{"index"}); - resource_handler.setResourceBase(resourceBase); + ResourceHandler resource_handler = new ResourceHandler(); + resource_handler.setDirectoriesListed(true); + resource_handler.setWelcomeFiles(new String[]{"index", "index.html"}); + resource_handler.setResourceBase(resourceBase); - ContextHandler contextHandler = new ContextHandler(); - contextHandler.setContextPath(contextPath); - contextHandler.setHandler(resource_handler); + ContextHandler contextHandler = new ContextHandler(); + contextHandler.setContextPath(contextPath); + contextHandler.setHandler(resource_handler); - HandlerList handlers = new HandlerList(); + HandlerList handlers = new HandlerList(); - handlers.setHandlers(new Handler[]{contextHandler, new DefaultHandler()}); - server.setHandler(handlers); + handlers.setHandlers(new Handler[]{contextHandler, new DefaultHandler()}); + server.setHandler(handlers); - LOGGER.info("Serving out contents of: [{}] on http://{}:{}{}", resourceBase, hostname, port, contextHandler.getContextPath()); - LOGGER.info("(To stop server hit CTRL-C)"); + LOGGER.info("Serving out contents of: [{}] on http://{}:{}{}", resourceBase, hostname, port, contextHandler.getContextPath()); + LOGGER.info("(To stop server hit CTRL-C)"); - try { server.start(); server.join(); } catch (Exception e) { - LOGGER.error("unable to start server", e); + throw new JBakeException(SystemExit.SERVER_ERROR, "unable to start the server", e); } } diff --git a/jbake-core/src/main/java/org/jbake/launcher/LaunchOptions.java b/jbake-core/src/main/java/org/jbake/launcher/LaunchOptions.java index d30bd5865..c4bd9cfc2 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/LaunchOptions.java +++ b/jbake-core/src/main/java/org/jbake/launcher/LaunchOptions.java @@ -1,41 +1,51 @@ package org.jbake.launcher; -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; +import org.jbake.app.configuration.ConfigUtil; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; import java.io.File; +@Command( + description = "JBake is a Java based, open source, static site/blog generator for developers & designers", + name = "jbake", + usageHelpAutoWidth = true +) public class LaunchOptions { - @Argument(index = 0, usage = "source folder of site content (with templates and assets), if not supplied will default to current directory", metaVar = "") + @Parameters(index = "0", description = "source folder of site content (with templates and assets), if not supplied will default to current directory", arity = "0..1") private String source; - @Argument(index = 1, usage = "destination folder for output, if not supplied will default to a folder called \"output\" in the current directory", metaVar = "") + @Parameters(index = "1", description = "destination folder for output, if not supplied will default to a folder called \"output\" in the current directory", arity = "0..1") private String destination; - @Option(name = "-b", aliases = {"--bake"}, usage = "performs a bake") + @Option(names = {"-b", "--bake"}, description = "performs a bake") private boolean bake; - @Option(name = "-i", aliases = {"--init"}, usage = "initialises required folder structure with default templates (defaults to current directory if is not supplied)") - private boolean init; + @ArgGroup(exclusive = false, heading = "%n%nJBake initialization%n%n") + private InitOptions initGroup; - @Option(name = "-t", aliases = {"--template"}, usage = "use specified template engine for default templates (uses Freemarker if is not supplied) ", depends = ("-i")) - private String template; - - @Option(name = "-s", aliases = {"--server"}, usage = "runs HTTP server to serve out baked site, if no is supplied will default to a folder called \"output\" in the current directory") + @Option(names = {"-s", "--server"}, description = "runs HTTP server to serve out baked site, if no is supplied will default to a folder called \"output\" in the current directory") private boolean runServer; - @Option(name = "-h", aliases = {"--help"}, usage = "prints this message") - private boolean helpNeeded; + @Option(names = {"-h", "--help"}, description = "prints this message", usageHelp = true) + private boolean helpRequested; - @Option(name = "--reset", usage = "clears the local cache, enforcing rendering from scratch") + @Option(names = {"--reset"}, description = "clears the local cache, enforcing rendering from scratch") private boolean clearCache; + @Option(names = {"-c", "--config"}, description = "use specified file for configuration (defaults to " + ConfigUtil.CONFIG_FILE +" in the source folder if not supplied)") + private String config; + + @Option(names = {"--prop-encoding"}, description = "use given encoding to load properties file. default: utf-8") + private String propertiesEncoding = "utf-8"; + + @Option(names = {"-ls", "--list-settings"}, description = "list configuration settings") + private boolean listConfig; + public String getTemplate() { - if (template != null) { - return template; - } else { - return "freemarker"; - } + return initGroup.template; } public File getSource() { @@ -62,8 +72,20 @@ public String getDestinationValue() { return destination; } + public File getConfig() { + if (config != null) { + return new File(config); + } else { + return new File(getSource(), "jbake.properties"); + } + } + + public String getConfigValue() { + return config; + } + public boolean isHelpNeeded() { - return helpNeeded || !(isBake() || isRunServer() || isInit() || source != null || destination != null); + return helpRequested || !(isListConfig() || isBake() || isRunServer() || isInit() || source != null || destination != null); } public boolean isRunServer() { @@ -71,7 +93,7 @@ public boolean isRunServer() { } public boolean isInit() { - return init; + return (initGroup != null && initGroup.init); } public boolean isClearCache() { @@ -81,4 +103,21 @@ public boolean isClearCache() { public boolean isBake() { return bake || (source != null && destination != null); } + + public boolean isListConfig() { + return listConfig; + } + + public String getPropertiesEncoding() { + return propertiesEncoding; + } + + static class InitOptions { + + @Option(names = {"-i", "--init"}, paramLabel = "", description = "initialises required folder structure with default templates (defaults to current directory if is not supplied)", required = true) + private boolean init; + + @Option(names = {"-t", "--template"}, defaultValue = "freemarker", fallbackValue = "freemarker", description = "use specified template engine for default templates (uses Freemarker if