diff --git a/.openshift/action_hooks/build b/.openshift/action_hooks/build index 5d848806..9e117af7 100755 --- a/.openshift/action_hooks/build +++ b/.openshift/action_hooks/build @@ -1,6 +1,23 @@ #!/bin/bash -cd $OPENSHIFT_REPO_DIR +if [ ! -d $OPENSHIFT_DATA_DIR/jdk1.8.0_20 ] + then + cd $OPENSHIFT_DATA_DIR + wget http://www.java.net/download/jdk8u20/archive/b17/binaries/jdk-8u20-ea-bin-b17-linux-x64-04_jun_2014.tar.gz + tar xvf *.tar.gz + rm -f *.tar.gz +fi +if [ ! -d $OPENSHIFT_DATA_DIR/apache-maven-3.2.3 ] + then + cd $OPENSHIFT_DATA_DIR + wget http://archive.apache.org/dist/maven/maven-3/3.2.3/binaries/apache-maven-3.2.3-bin.tar.gz + tar xvf *.tar.gz + rm -f *.tar.gz +fi +export M2=$OPENSHIFT_DATA_DIR/apache-maven-3.2.3/bin +export JAVA_HOME=$OPENSHIFT_DATA_DIR/jdk1.8.0_20 +export PATH=$JAVA_HOME/bin:$M2:$PATH +cd $OPENSHIFT_REPO_DIR rm -rf $OPENSHIFT_REPO_DIR/node rm -rf $OPENSHIFT_REPO_DIR/node_modules rm -rf $OPENSHIFT_TMP_DIR/npm @@ -16,7 +33,4 @@ export HOME="$OPENSHIFT_TMP_DIR/local" export NPM_CONFIG_ARCH="x64" -npm install npm -export PATH="$OPENSHIFT_REPO_DIR/node_modules/.bin:$PATH" - -mvn clean package -DskipTests -Dos.arch=x64 +mvn clean package -DskipTests -Dos.arch=x64 -s .openshift/settings.xml diff --git a/.openshift/action_hooks/start b/.openshift/action_hooks/start index 53ce9a5c..a1562fc5 100755 --- a/.openshift/action_hooks/start +++ b/.openshift/action_hooks/start @@ -1,3 +1,4 @@ #!/bin/bash cd $OPENSHIFT_REPO_DIR -nohup java -jar target/commafeed.jar server .openshift/config.mysql.yml > ${OPENSHIFT_DIY_LOG_DIR}/commafeed.log 2>&1 & +export JAVA_HOME=$OPENSHIFT_DATA_DIR/jdk1.8.0_20 +nohup $JAVA_HOME/bin/java -jar target/commafeed.jar server .openshift/config.mysql.yml > ${OPENSHIFT_DIY_LOG_DIR}/commafeed.log 2>&1 & diff --git a/.openshift/config.mysql.yml b/.openshift/config.mysql.yml index 3cefeb46..c312270d 100644 --- a/.openshift/config.mysql.yml +++ b/.openshift/config.mysql.yml @@ -1,18 +1,21 @@ # CommaFeed settings # ------------------ app: - # context path of the application - contextPath: / - # url used to access commafeed publicUrl: https://@OPENSHIFT_APP_DNS@/ # wether to allow user registrations allowRegistrations: false + # create a demo account the first time the app starts + createDemoAccount: false + # put your google analytics tracking code here googleAnalyticsTrackingCode: + # put your google server key (used for youtube favicon fetching) + googleAuthKey: + # number of http threads backgroundThreads: 3 @@ -47,6 +50,9 @@ app: # time to keep unread statuses (in days), 0 to disable keepStatusDays: 0 + # entries to keep per feed, old entries will be deleted, 0 to disable + maxFeedCapacity: 500 + # cache service to use, possible values are 'noop' and 'redis' cache: noop diff --git a/.openshift/markers/java8 b/.openshift/markers/java8 new file mode 100644 index 00000000..e69de29b diff --git a/.openshift/settings.xml b/.openshift/settings.xml index 8cfcb0a7..397d208f 100644 --- a/.openshift/settings.xml +++ b/.openshift/settings.xml @@ -1,3 +1,41 @@ - $OPENSHIFT_DATA_DIR + + + nexus + central + http://mirror1.ops.rhcloud.com/nexus/content/groups/public + + + + + nexus + + + central + http://central + + true + + + true + + + + + + central + http://central + + true + + + true + + + + + + + nexus + diff --git a/.travis.yml b/.travis.yml index a39a2226..e7969656 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ language: java jdk: - - openjdk7 - - oraclejdk7 - oraclejdk8 \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index d714369d..25deec3c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,17 @@ +v 2.2.0 + - fix youtube and instagram favicon fetching + - mark as read filter was lost when a feed was rearranged with drag&drop + - feed entry categories are now displayed if available + - various performance and dependencies upgrades + - java8 is now required +v 2.1.0 + - dropwizard upgrade to 0.8.0 + - you have to remove the "app.contextPath" setting from your yml file, you can optionally use server.applicationContextPath instead + - new setting app.maxFeedCapacity for deleting old entries + - ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title, content, author or url. + - ability to use !keyword or -keyword to exclude a keyword from a search query + - facebook feeds now show user favicon instead of facebook favicon + - new dark theme 'nightsky' v 2.0.3 - internet explorer ajax cache workaround - categories are now deletable again diff --git a/README.md b/README.md index 06b359c8..099d9ac0 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,50 @@ -CommaFeed [![Build Status](https://travis-ci.org/Athou/commafeed.svg?branch=master)](https://travis-ci.org/Athou/commafeed) -========= +# CommaFeed [![Build Status](https://travis-ci.org/Athou/commafeed.svg?branch=master)](https://travis-ci.org/Athou/commafeed) + Sources for [CommaFeed.com](http://www.commafeed.com/). Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS. -Related open-source projects ----------------------------- +## Related open-source projects -Android apps: [News+ extension](https://github.com/Athou/commafeed-newsplus) - [Android app](https://github.com/doomrobo/CommaFeed-Android-Reader) + +Android apps: [News+ extension](https://github.com/Athou/commafeed-newsplus) Browser extensions: [Chrome](https://github.com/Athou/commafeed-chrome) - [Firefox](https://github.com/Athou/commafeed-firefox) - [Opera](https://github.com/Athou/commafeed-opera) - [Safari](https://github.com/Athou/commafeed-safari) -Deployment on your own server ------------------------------ +## Deployment on your own server + +### The very short version (download precompiled package) + + mkdir commafeed && cd commafeed + wget https://github.com/Athou/commafeed/releases/download/2.2.0/commafeed.jar + wget https://raw.githubusercontent.com/Athou/commafeed/2.2.0/config.yml.example -O config.yml + vi config.yml + java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml + +### The short version (build from sources) + + git clone https://github.com/Athou/commafeed.git + cd commafeed + ./mvnw clean package + cp config.yml.example config.yml + vi config.yml + java -Djava.net.preferIPv4Stack=true -jar target/commafeed.jar server config.yml + +### The long version (same as the short version, but more detailed) CommaFeed 2.0 has been rewritten to use Dropwizard and gulp instead of using tomee and wro4j. The latest version of the 1.x branch is available [here](https://github.com/Athou/commafeed/tree/1.x). -For storage, you can either use an embedded H2 database or an external MySQL, PostgreSQL or SQLServer database. -You also need Maven 3.x (and a Java 1.7+ JDK) installed in order to build the application. +For storage, you can either use an embedded H2 database (use it only to test CommaFeed) or an external MySQL, PostgreSQL or SQLServer database. +You also need the Java 1.8+ JDK in order to build the application. -To install maven and openjdk on Ubuntu, issue the following commands +To install the required packages to build CommaFeed on Ubuntu, issue the following commands - sudo apt-get install build-essential openjdk-7-jdk maven - # Make sure java7 is the selected java version + sudo apt-get install g++ build-essential openjdk-8-jdk + # Make sure java8 is the selected java version sudo update-alternatives --config java sudo update-alternatives --config javac -On Windows and other operating systems, just download maven 3.x from the [official site](http://maven.apache.org/), extract it somewhere and add the `bin` directory to your `PATH` environment variable. - Clone this repository. If you don't have git you can download the sources as a zip file from [here](https://github.com/Athou/commafeed/archive/master.zip) git clone https://github.com/Athou/commafeed.git @@ -36,17 +52,16 @@ Clone this repository. If you don't have git you can download the sources as a z Now build the application - mvn clean package + ./mvnw clean package Copy `config.yml.example` to `config.yml` then edit the file to your liking. Issue the following command to run the app, the server will listen by default on `http://localhost:8082`. The default user is `admin` and the default password is `admin`. - java -jar target/commafeed.jar server config.yml + java -Djava.net.preferIPv4Stack=true -jar target/commafeed.jar server config.yml You can use a proxy http server such as nginx or apache. -Deployment on OpenShift ------------------------------ +## Deployment on OpenShift [OpenShift](https://openshift.redhat.com) is Red Hat's Platform-as-a-Service (PaaS) that allows developers to quickly develop, host, and scale applications in a cloud environment. CommaFeed runs perfectly on OpenShift and can even be used in the free tier. Follow the [Getting Started](https://developers.openshift.com/en/getting-started-overview.html) guide and after you sign up and install the Command Line Tools (RHC), do: @@ -55,9 +70,12 @@ Deployment on OpenShift git remote add upstream -m master https://github.com/Athou/commafeed.git git pull -s recursive -X theirs upstream master git push + + # To upgrade an existing openshift installation + git pull upstream master + git push -Translate CommaFeed into your language --------------------------------------- +## Translate CommaFeed into your language Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/src/main/app/i18n). @@ -65,18 +83,16 @@ To add a new language, create a new file in that directory. The name of the file should be the two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). The language has to be referenced in the `src/main/app/js/i18n.js` file to be picked up. -Themes ---------------------- +## Themes -To create a theme, create a new file `src/main/webapp/sass/themes/_.scss`. Your styles should be wrapped in a `#theme-` element and use the [SCSS format](http://sass-lang.com/) which is a superset of CSS. +To create a theme, create a new file `src/main/app/sass/themes/_.scss`. Your styles should be wrapped in a `#theme-` element and use the [SCSS format](http://sass-lang.com/) which is a superset of CSS. -Don't forget to reference your theme in `src/main/webapp/sass/app.scss` and in `src/main/webapp/js/controllers.js` (look for `$scope.themes`). +Don't forget to reference your theme in `src/main/app/sass/app.scss` and in `src/main/app/js/controllers.js` (look for `$scope.themes`). -See [_test.scss](https://github.com/Athou/commafeed/blob/master/src/main/webapp/sass/themes/_test.scss) for an example. +See [_test.scss](https://github.com/Athou/commafeed/blob/master/src/main/app/sass/themes/_test.scss) for an example. -Local development ------------------ +## Local development Steps to configuring a development environment for CommaFeed may include, but may not be limited to: @@ -100,10 +116,9 @@ Steps to configuring a development environment for CommaFeed may include, but ma 14. When you're done developing, create a fork at the top of https://github.com/Athou/CommaFeed page and commit your changes to it. 15. If you'd like to contribute to CommaFeed, create a pull request from your repository to https://github.com/Athou/CommaFeed when your changes are ready. There's a button to do so at the top of https://github.com/Athou/CommaFeed. -Copyright and license ---------------------- +## Copyright and license -Copyright 2013-2014 CommaFeed. +Copyright 2013-2015 CommaFeed. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. diff --git a/bower.json b/bower.json index 9e866faa..7b2fb50a 100644 --- a/bower.json +++ b/bower.json @@ -2,32 +2,36 @@ "name": "commafeed", "version": "2.0.0", "dependencies": { - "jquery": "1.11.0", + "jquery": "2.1.3", "jquery-ui": "1.10.3", "jquery-mousewheel": "3.1.12", - "lodash": "2.4.1", - "bootstrap": "3.1.1", + "lodash": "3.4.0", + "bootstrap": "3.3.2", "font-awesome": "3.2.1", - "angular": "1.2.16", - "angular-resource": "1.2.16", - "angular-route": "1.2.16", - "angular-sanitize": "1.2.16", - "angular-touch": "1.2.16", - "angular-animate": "1.2.16", - "angular-ui-router": "0.2.8", + "angular": "1.3.14", + "angular-resource": "1.3.14", + "angular-route": "1.3.14", + "angular-sanitize": "1.3.14", + "angular-touch": "1.3.14", + "angular-animate": "1.3.14", + "angular-ui-router": "0.2.13", "angular-ui-utils": "0.1.0", "angular-ui-select2": "0.0.5", "angular-bootstrap": "0.2.0", - "angular-loading-bar": "0.5.0", - "angular-translate": "2.2.0", - "angular-translate-loader-static-files": "2.2.0", + "angular-loading-bar": "0.6.0", + "angular-translate": "2.6.1", + "angular-translate-loader-static-files": "2.6.1", "ngInfiniteScroll": "1.0.0", "ng-grid": "2.0.6", "mousetrap": "1.4.6", - "momentjs": "2.6.0", - "device.js": "matthewhudson/device.js#2ae5c775e35ccc837589e5af34e292c54936778c", + "momentjs": "2.9.0", + "devicejs": "0.2.4", "readabilicons": "arc90/readability-readabilicons#34c55561c5b8ec6e90714b50237c06b13cb9d59c", - "zocial": "samcollins/css-social-buttons#1f59ecacde475e563fb6771667597493ec4eecb6", - "swagger-ui": "2.0.21" + "zocial-less": "1.0.0", + "swagger-ui": "2.1.0" + }, + "resolutions": { + "angular": "1.3.14", + "angular-translate": "2.6.1" } } diff --git a/config.dev.yml b/config.dev.yml index e2ddf666..a7771204 100644 --- a/config.dev.yml +++ b/config.dev.yml @@ -1,18 +1,21 @@ # CommaFeed settings # ------------------ app: - # context path of the application - contextPath: / - # url used to access commafeed publicUrl: http://localhost:8082/ # wether to allow user registrations allowRegistrations: true + # create a demo account the first time the app starts + createDemoAccount: false + # put your google analytics tracking code here googleAnalyticsTrackingCode: + # put your google server key (used for youtube favicon fetching) + googleAuthKey: + # number of http threads backgroundThreads: 3 @@ -47,6 +50,9 @@ app: # time to keep unread statuses (in days), 0 to disable keepStatusDays: 0 + # entries to keep per feed, old entries will be deleted, 0 to disable + maxFeedCapacity: 500 + # cache service to use, possible values are 'noop' and 'redis' cache: noop @@ -69,17 +75,12 @@ app: database: driverClass: org.h2.Driver - url: jdbc:h2:./target/example + url: jdbc:h2:./target/example;mv_store=false user: sa password: sa properties: charSet: UTF-8 - maxWaitForConnection: 1s - validationQuery: "/* CommaFeed Health Check */ SELECT 1" - minSize: 1 - maxSize: 50 - checkConnectionWhileIdle: true - maxConnectionAge: 30m + validationQuery: "/* CommaFeed Health Check */ SELECT 1" server: applicationConnectors: @@ -94,7 +95,7 @@ logging: loggers: com.commafeed: DEBUG liquibase: INFO - org.hibernate.SQL: ALL + org.hibernate.SQL: INFO # or ALL for sql debugging org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN appenders: - type: console diff --git a/config.yml.example b/config.yml.example index 9c7ae8c1..5f6d1674 100644 --- a/config.yml.example +++ b/config.yml.example @@ -1,18 +1,21 @@ # CommaFeed settings # ------------------ app: - # context path of the application - contextPath: / - # url used to access commafeed publicUrl: http://localhost:8082/ # wether to allow user registrations allowRegistrations: false + # create a demo account the first time the app starts + createDemoAccount: false + # put your google analytics tracking code here googleAnalyticsTrackingCode: + # put your google server key (used for youtube favicon fetching) + googleAuthKey: + # number of http threads backgroundThreads: 3 @@ -25,6 +28,7 @@ app: smtpTls: false smtpUserName: smtpPassword: + smtpFromAddress: # wether this commafeed instance has a lot of feeds to refresh # leave this to false in almost all cases @@ -47,6 +51,9 @@ app: # time to keep unread statuses (in days), 0 to disable keepStatusDays: 0 + # entries to keep per feed, old entries will be deleted, 0 to disable + maxFeedCapacity: 500 + # cache service to use, possible values are 'noop' and 'redis' cache: noop @@ -69,17 +76,15 @@ app: database: driverClass: org.h2.Driver - url: jdbc:h2:/home/commafeed/db + url: jdbc:h2:/home/commafeed/db;mv_store=false user: sa password: sa properties: charSet: UTF-8 - maxWaitForConnection: 1s - validationQuery: "/* CommaFeed Health Check */ SELECT 1" - minSize: 1 - maxSize: 50 - checkConnectionWhileIdle: true - maxConnectionAge: 30m + validationQuery: "/* CommaFeed Health Check */ SELECT 1" + minSize: 1 + maxSize: 50 + maxConnectionAge: 30m server: applicationConnectors: diff --git a/gulpfile.js b/gulpfile.js index e7d273fc..c3d90b0b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -4,7 +4,6 @@ var revReplace = require('gulp-rev-replace'); var minifyCSS = require('gulp-minify-css'); var uglify = require('gulp-uglify'); var filter = require('gulp-filter'); -var bower = require('gulp-bower'); var connect = require('gulp-connect'); var modRewrite = require('connect-modrewrite'); var sass = require('gulp-sass'); @@ -15,10 +14,6 @@ var SRC_DIR = 'src/main/app/'; var TEMP_DIR = 'target/gulp/' var BUILD_DIR = 'target/classes/assets/'; -gulp.task('bower', function() { - return bower(); -}); - gulp.task('images', function() { return gulp.src(SRC_DIR + 'images/**/*').pipe(gulp.dest(BUILD_DIR + 'images')); }); @@ -27,34 +22,36 @@ gulp.task('i18n', function() { return gulp.src(SRC_DIR + 'i18n/**/*.js').pipe(gulp.dest(BUILD_DIR + 'i18n')); }); -gulp.task('favicons', function() { +gulp.task('resources', function() { var favicons_png = SRC_DIR + '*.png'; var favicons_ico = SRC_DIR + '*.ico'; var favicons_svg = SRC_DIR + '*.svg'; - return gulp.src([favicons_png, favicons_ico, favicons_svg]).pipe(gulp.dest(BUILD_DIR)); + var manifest = SRC_DIR + 'manifest.json'; + return gulp.src([favicons_png, favicons_ico, favicons_svg, manifest]).pipe(gulp.dest(BUILD_DIR)); }); gulp.task('sass', function() { return gulp.src(SRC_DIR + 'sass/app.scss').pipe(sass()).pipe(gulp.dest(TEMP_DIR + 'css')); }); -gulp.task('fonts', ['bower'], function() { +gulp.task('fonts', function() { var font_awesome = SRC_DIR + 'lib/font-awesome/font/fontawesome-webfont.*'; - var zocial = SRC_DIR + 'lib/zocial/css/zocial-regular-*'; + var zocial = SRC_DIR + 'lib/zocial-less/css/zocial-regular-*'; var readabilicons = SRC_DIR + 'lib/readabilicons/webfont/fonts/readabilicons-*'; return gulp.src([font_awesome, zocial, readabilicons]).pipe(gulp.dest(BUILD_DIR + 'font')); }); -gulp.task('select2', ['bower'], function() { +gulp.task('select2', function() { var gif = SRC_DIR + 'lib/select2/*.gif'; var png = SRC_DIR + 'lib/select2/*.png'; return gulp.src([gif, png]).pipe(gulp.dest(BUILD_DIR + 'css')); }); -gulp.task('swagger-ui', ['bower'], function() { +gulp.task('swagger-ui', function() { var index_html = SRC_DIR + 'api/index.html'; + var swagger_json = 'target/swagger/swagger.json'; var lib = SRC_DIR + 'lib/swagger-ui/dist/**/*'; - return gulp.src([lib, index_html]).pipe(gulp.dest(BUILD_DIR + 'api')); + return gulp.src([lib, index_html, swagger_json]).pipe(gulp.dest(BUILD_DIR + 'api')); }); gulp.task('template-cache', function() { @@ -65,17 +62,17 @@ gulp.task('template-cache', function() { return gulp.src(SRC_DIR + 'templates/**/*.html').pipe(templateCache(options)).pipe(gulp.dest(TEMP_DIR + 'js')); }); -gulp.task('build-dev', ['images', 'i18n', 'favicons', 'sass', 'fonts', 'select2', 'swagger-ui', 'template-cache', 'bower'], function() { +gulp.task('build-dev', ['images', 'i18n', 'resources', 'sass', 'fonts', 'select2', 'swagger-ui', 'template-cache'], function() { var assets = useref.assets({ searchPath : [SRC_DIR, TEMP_DIR] }); var jsFilter = filter("**/*.js"); var cssFilter = filter("**/*.css"); return gulp.src([SRC_DIR + 'index.html', TEMP_DIR + 'app.css']).pipe(assets).pipe(rev()).pipe(assets.restore()).pipe(useref()).pipe( - revReplace()).pipe(gulp.dest(BUILD_DIR)); + revReplace()).pipe(gulp.dest(BUILD_DIR)).pipe(connect.reload()); }); -gulp.task('build', ['images', 'i18n', 'favicons', 'sass', 'fonts', 'select2', 'swagger-ui', 'template-cache', 'bower'], function() { +gulp.task('build', ['images', 'i18n', 'resources', 'sass', 'fonts', 'select2', 'swagger-ui', 'template-cache'], function() { var assets = useref.assets({ searchPath : [SRC_DIR, TEMP_DIR] }); @@ -101,7 +98,9 @@ gulp.task('serve', function() { connect.server({ root : BUILD_DIR, port : 8082, + livereload : true, middleware : function() { + var api = '^/api/(.*)$ http://localhost:8083/rest/$1 [P]'; var rest = '^/rest/(.*)$ http://localhost:8083/rest/$1 [P]'; var next = '^/next(.*)$ http://localhost:8083/next$1 [P]'; var logout = '^/logout$ http://localhost:8083/logout [P]'; @@ -113,4 +112,4 @@ gulp.task('serve', function() { }); gulp.task('dev', ['build-dev', 'watch', 'serve']); -gulp.task('default', ['build']); \ No newline at end of file +gulp.task('default', ['build']); diff --git a/maven/maven-wrapper.jar b/maven/maven-wrapper.jar new file mode 100644 index 00000000..18ba302c Binary files /dev/null and b/maven/maven-wrapper.jar differ diff --git a/maven/maven-wrapper.properties b/maven/maven-wrapper.properties new file mode 100644 index 00000000..558d7418 --- /dev/null +++ b/maven/maven-wrapper.properties @@ -0,0 +1,3 @@ +#Maven download properties +#Sat Jul 04 09:06:32 CEST 2015 +distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip diff --git a/mvnw b/mvnw new file mode 100755 index 00000000..2275ac76 --- /dev/null +++ b/mvnw @@ -0,0 +1,234 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + wdir=$(cd "$wdir/.."; pwd) + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER="org.apache.maven.wrapper.MavenWrapperMain" + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + -classpath \ +"$MAVEN_PROJECTBASEDIR/maven/maven-wrapper.jar" \ + ${WRAPPER_LAUNCHER} "$@" diff --git a/mvnw.bat b/mvnw.bat new file mode 100644 index 00000000..922fc372 --- /dev/null +++ b/mvnw.bat @@ -0,0 +1,141 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:init + +set MAVEN_CMD_LINE_ARGS=%* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\maven\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +%MAVEN_JAVA_EXE% -Dmaven.multiModuleProjectDirectory="" %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% + +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/package.json b/package.json index ec24705a..41239ace 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,17 @@ "main": "main.js", "private": true, "devDependencies": { - "gulp": "3.8.7", - "gulp-rev": "1.0.0", - "gulp-rev-replace": "0.3.0", - "gulp-minify-css": "0.3.7", - "gulp-uglify": "0.3.1", - "gulp-filter": "1.0.0", - "gulp-bower": "0.0.6", - "gulp-connect": "2.0.6", - "connect-modrewrite": "0.7.7", - "gulp-sass": "0.7.2", - "gulp-useref": "0.6.0", - "gulp-angular-templatecache": "1.3.0" + "bower": "1.4.1", + "gulp": "3.8.11", + "gulp-rev": "4.0.0", + "gulp-rev-replace": "0.4.1", + "gulp-minify-css": "1.1.5", + "gulp-uglify": "1.2.0", + "gulp-filter": "2.0.2", + "gulp-connect": "2.2.0", + "connect-modrewrite": "0.8.1", + "gulp-sass": "2.0.2", + "gulp-useref": "1.1.2", + "gulp-angular-templatecache": "1.6.0" } } diff --git a/pom.xml b/pom.xml index 0b00a49f..a12a52c0 100644 --- a/pom.xml +++ b/pom.xml @@ -4,19 +4,20 @@ 4.0.0 com.commafeed commafeed - 2.0.3 + 2.3.0-SNAPSHOT jar CommaFeed - 3.0.0 + 3.1.0 UTF-8 - 0.7.1 - 3.0 - 3.5.0 + 1.8 + 0.9.0-rc2 + 4.0 + 4.0.2 1.5.0 @@ -34,20 +35,53 @@ true + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + com.github.eirslett + frontend-maven-plugin + [0.0.22,) + + npm + gulp + bower + + + + + false + + + + + + + + + org.apache.maven.plugins maven-compiler-plugin 3.1 - 1.7 - 1.7 + ${java.version} + ${java.version} pl.project13.maven git-commit-id-plugin - 2.1.11 + 2.1.13 @@ -95,20 +129,50 @@ + + com.github.kongchen + swagger-maven-plugin + 3.1.0 + + + + com.commafeed.frontend.resource;com.commafeed.frontend.model;com.commafeed.frontend.model.request + target/swagger + /rest + + CommaFeed + ${project.version} + + + + + + + + + + + compile + + generate + + + + com.github.eirslett frontend-maven-plugin - 0.0.16 + 0.0.24 install node and npm install-node-and-npm - generate-resources + compile - v0.10.30 - 1.3.8 + v0.10.39 + 2.12.1 @@ -116,21 +180,31 @@ npm - generate-resources + compile + + + bower install + + bower + + compile + + install + gulp build gulp - generate-resources + compile org.apache.maven.plugins maven-jar-plugin - 2.5 + 2.6 @@ -139,29 +213,6 @@ - - com.jamierf.dropwizard - dropwizard-debpkg-maven-plugin - 0.7 - - ${basedir}/config.yml.example - - openjdk-7-jdk - true - - - commafeed - - - - - package - - dwpackage - - - - @@ -169,13 +220,13 @@ org.projectlombok lombok - 1.14.8 + 1.16.4 provided org.slf4j slf4j-api - 1.7.7 + 1.7.12 @@ -193,17 +244,22 @@ io.dropwizard dropwizard-core ${dropwizard.version} + + + org.glassfish.hk2.external + aopalliance-repackaged + + + org.glassfish.hk2.external + javax.inject + + io.dropwizard dropwizard-hibernate ${dropwizard.version} - - io.dropwizard - dropwizard-client - ${dropwizard.version} - io.dropwizard dropwizard-migrations @@ -218,34 +274,50 @@ io.dropwizard dropwizard-forms ${dropwizard.version} - pom - - - - com.wordnik - swagger-jaxrs_2.10 - 1.3.10 - javax.ws.rs - jsr311-api + org.glassfish.hk2.external + javax.inject - + - com.mysema.querydsl + org.apache.httpcomponents + httpclient + 4.5 + + + commons-logging + commons-logging + + + + + + io.swagger + swagger-annotations + 1.5.0 + + + + com.querydsl querydsl-apt ${querydsl.version} provided hibernate - com.mysema.querydsl + com.querydsl querydsl-jpa ${querydsl.version} - + + + com.google.guava + guava + 18.0 + commons-io commons-io @@ -259,65 +331,101 @@ commons-codec commons-codec - 1.9 + 1.10 org.apache.commons commons-math3 - 3.3 + 3.5 - + + org.apache.commons + commons-jexl + 2.1.1 + + + commons-logging + commons-logging + + + + redis.clients jedis - 2.6.0 + 2.7.2 com.sun.mail javax.mail - 1.5.2 + 1.5.3 - + + com.rometools rome ${rome.version} + + + jdom + org.jdom + + com.rometools rome-opml ${rome.version} + + org.jdom + jdom2 + 2.0.6 + + org.jsoup jsoup - 1.8.1 + 1.8.2 - com.googlecode.juniversalchardet - juniversalchardet - 1.0.3 + com.ibm.icu + icu4j + 55.1 net.sourceforge.cssparser cssparser - 0.9.14 + 0.9.16 + + + + com.google.apis + google-api-services-youtube + v3-rev139-1.20.0 + + + com.google.guava + guava-jdk5 + + com.h2database h2 - 1.4.182 + 1.4.187 mysql mysql-connector-java - 5.1.33 + 5.1.35 org.postgresql postgresql - 9.3-1102-jdbc41 + 9.4-1201-jdbc41 net.sourceforge.jtds @@ -328,13 +436,13 @@ junit junit - 4.11 + 4.12 test org.mockito mockito-core - 1.10.8 + 2.0.11-beta test diff --git a/src/main/app/api/index.html b/src/main/app/api/index.html index 05a25131..e78dbfa7 100644 --- a/src/main/app/api/index.html +++ b/src/main/app/api/index.html @@ -2,7 +2,7 @@ Swagger UI - + @@ -12,25 +12,23 @@ - + - + + - + @@ -62,8 +68,8 @@ - - + + diff --git a/src/main/app/js/controllers.js b/src/main/app/js/controllers.js index 7ae8d904..aa360db3 100644 --- a/src/main/app/js/controllers.js +++ b/src/main/app/js/controllers.js @@ -322,17 +322,21 @@ module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedS $scope.save = function() { var sub = $scope.sub; + $scope.error = null; FeedService.modify({ id : sub.id, name : sub.name, position : sub.position, - categoryId : sub.categoryId + categoryId : sub.categoryId, + filter : sub.filter }, function() { CategoryService.init(); $state.transitionTo('feeds.view', { _id : 'all', _type : 'category' }); + }, function(e) { + $scope.error = e.data; }); }; }]); @@ -489,7 +493,7 @@ module.controller('ToolbarCtrl', [ type : $stateParams._type, id : $stateParams._id, olderThan : olderThan, - keywords: $location.search().q, + keywords : $location.search().q, read : true }); }; @@ -882,7 +886,7 @@ module.controller('FeedListCtrl', [ service.mark({ id : $scope.selectedId, olderThan : olderThan || $scope.timestamp, - keywords: $location.search().q, + keywords : $location.search().q, read : true }, function() { CategoryService.refresh(function() { @@ -1367,7 +1371,7 @@ module.controller('SettingsCtrl', ['$scope', '$location', 'SettingsService', 'An $scope.langs = LangService.langs; - $scope.themes = ['default', 'bootstrap', 'dark', 'ebraminio', 'MRACHINI', 'svetla', 'third']; + $scope.themes = ['default', 'bootstrap', 'dark', 'ebraminio', 'MRACHINI', 'nightsky', 'svetla', 'third']; $scope.settingsService = SettingsService; $scope.$watch('settingsService.settings', function(value) { diff --git a/src/main/app/js/directives.js b/src/main/app/js/directives.js index 463877c5..200ebe83 100644 --- a/src/main/app/js/directives.js +++ b/src/main/app/js/directives.js @@ -72,7 +72,7 @@ module.directive('tags', function() { tags : [] }; if (newValue) { - data.tags = newValue.split(','); + data.tags = newValue; } EntryService.tag(data); } @@ -308,7 +308,8 @@ module.directive('droppable', ['CategoryService', 'FeedService', function(Catego var data = { id : source.id, - name : source.name + name : source.name, + filter : source.filter }; if (source.children) { @@ -359,3 +360,14 @@ module.directive('metricGauge', function() { templateUrl : 'templates/_metrics.gauge.html' }; }); + +module.directive('metricTimer', function() { + return { + scope : { + metric : '=', + label : '=' + }, + restrict : 'E', + templateUrl : 'templates/_metrics.timer.html' + }; +}); diff --git a/src/main/app/js/main.js b/src/main/app/js/main.js index bb668c49..8a51e0d0 100644 --- a/src/main/app/js/main.js +++ b/src/main/app/js/main.js @@ -23,26 +23,23 @@ app.config([ $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/); var interceptor = ['$rootScope', '$q', '$injector', function(scope, $q, $injector) { - - var success = function(response) { + var f = {}; + + f.response = function(response) { return response; }; - var error = function(response) { + + f.responseError = function(response) { var status = response.status; if (status == 401) { $injector.get('$state').transitionTo('welcome'); } return $q.reject(response); }; - - var promise = function(promise) { - return promise.then(success, error); - }; - - return promise; + return f; }]; - $httpProvider.responseInterceptors.push(interceptor); + $httpProvider.interceptors.push(interceptor); $stateProvider.state('feeds', { 'abstract' : true, diff --git a/src/main/app/js/services.js b/src/main/app/js/services.js index f9cb5306..57fc7e34 100644 --- a/src/main/app/js/services.js +++ b/src/main/app/js/services.js @@ -62,7 +62,7 @@ module.factory('SettingsService', ['$resource', '$translate', function($resource } else if (lang === 'ms') { lang = 'ms-my'; } - moment.lang(lang, {}); + moment.locale(lang, {}); if (callback) { callback(data); } @@ -298,6 +298,7 @@ module.factory('EntryService', ['$resource', '$http', function($resource, $http) $http.get('rest/entry/tags').success(function(data) { res.tags = []; res.tags.push.apply(res.tags, data); + res.tags.sort(); }); }; var oldTag = res.tag; diff --git a/src/main/app/manifest.json b/src/main/app/manifest.json new file mode 100644 index 00000000..78489d88 --- /dev/null +++ b/src/main/app/manifest.json @@ -0,0 +1,31 @@ +{ + "name": "CommaFeed", + "icons": [ + { + "src": "app-icon-72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "app-icon-114.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "app-icon-144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "app-icon-192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ], + "start_url": "/", + "display": "standalone" +} diff --git a/src/main/app/sass/app.scss b/src/main/app/sass/app.scss index aaa20831..bd30232f 100644 --- a/src/main/app/sass/app.scss +++ b/src/main/app/sass/app.scss @@ -17,6 +17,7 @@ @import "themes/bootstrap"; @import "themes/ebraminio"; @import "themes/MRACHINI"; +@import "themes/nightsky"; @import "themes/svetla"; @import "themes/dark"; @import "themes/third"; diff --git a/src/main/app/sass/components/readabilicons.scss b/src/main/app/sass/components/readabilicons.scss index 5e65e7f6..ed237ba8 100644 --- a/src/main/app/sass/components/readabilicons.scss +++ b/src/main/app/sass/components/readabilicons.scss @@ -13,8 +13,4 @@ content: "\e018"; font-family: "readabilicons"; -webkit-font-smoothing: antialiased; - font-size: 21px; - top: 5px; - position: relative; - line-height: 0px; } \ No newline at end of file diff --git a/src/main/app/sass/generic/_misc.scss b/src/main/app/sass/generic/_misc.scss index a60f5dbe..34bc3b25 100644 --- a/src/main/app/sass/generic/_misc.scss +++ b/src/main/app/sass/generic/_misc.scss @@ -1,3 +1,8 @@ +a:focus { + outline: none; + text-decoration: none; +} + .container-full { width: 100%; margin: 0 auto; @@ -43,6 +48,10 @@ label { display: block; } +.pre-wrap { + white-space: pre-wrap; +} + .form-horizontal .control-group { margin-bottom: 10px; } @@ -116,19 +125,23 @@ blockquote p { font-size: 14px; } -.btn,.btn-large,.btn-small,.btn-mini { +.form-group { + margin-bottom: 10px; +} + +.btn, .btn-large, .btn-small, .btn-mini { border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } -.btn-group>.btn:first-child,.btn-group>.btn-large:first-child { +.btn-group>.btn:first-child, .btn-group>.btn-large:first-child { border-top-left-radius: 2px; border-bottom-left-radius: 2px; } -.btn-group>.btn:last-child,.btn-group>.btn-large:last-child,.btn-group>.dropdown-toggle +.btn-group>.btn:last-child, .btn-group>.btn-large:last-child, .btn-group>.dropdown-toggle { border-top-right-radius: 2px; border-bottom-right-radius: 2px; diff --git a/src/main/app/sass/themes/_nightsky.scss b/src/main/app/sass/themes/_nightsky.scss new file mode 100644 index 00000000..0096d076 --- /dev/null +++ b/src/main/app/sass/themes/_nightsky.scss @@ -0,0 +1,126 @@ +#theme-nightsky { + a { + color: #2A9FD6; + } + + .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { + color: #FFF; + background-color: #2A9FD6; + } + + body, .toolbar { + color: #C6C6C6; + background-color: #2F2F2F; + } + + .btn-default { + color: #C6C6C6; + background-color: #424242; + border-color: #424242; + } + + .btn-default:hover, .btn-default:focus, .btn-default.focus, .btn-default:active, + .btn-default.active, .open>.dropdown-toggle.btn-default { + background-color: #282828; + border-color: #232323; + } + + .css-treeview li .tree-item:hover { + background-color: #282828; + } + + .css-treeview .unread-counter { + color: #939393; + } + + .css-treeview .category-link, .css-treeview a { + color: #939393; + } + + .css-treeview a .unread, .css-treeview .category-link .unread { + color: #C6C6C6; + } + + .css-treeview .selected { + color: #C00; + } + + .entrylist-header { + border-bottom: 1px solid #282828; + } + + #feed-accordion .entry { + border-bottom: 1px solid #282828; + } + + #feed-accordion .entry-body .entry-title { + font-weight: normal; + } + + #feed-accordion .entry-heading .entry-name { + color: #939393; + } + + #feed-accordion .unread .entry-heading .entry-name { + font-weight: normal; + color: #C6C6C6; + } + + #feed-accordion .unread .entry-heading { + background-color: #444; + } + + #feed-accordion .entry-heading { + background-color: #2F2F2F; + } + + #feed-accordion .unread .entry-heading:hover { + background-color: #2F2F2F; + } + + #feed-accordion .current.closed .entry-heading { + background-color: #151515; + } + + #feed-accordion .entry-heading-link { + color: #C6C6C6; + } + + #feed-accordion .entry-external-link { + color: #C6C6C6; + } + + #feed-accordion .icon-star-empty { + color: #C6C6C6; + } + + #feed-accordion .entry-heading .feed-name { + color: #939393; + } + + #feed-accordion .entry-body-content { + color: #C6C6C6; + } + + #feed-accordion .entry-buttons { + background-color: #494949; + border-top: 1px solid #282828; + } + + #feed-accordion .share-buttons a { + color: #C6C6C6; + } + + #feed-accordion a.mark-up-to { + color: #C6C6C6; + } + + #loading-bar .bar { + background: #C6C6C6; + } + + #loading-bar .peg { + box-shadow: 0 0 10px #C6C6C6, 0 0 5px #C6C6C6; + } +} + diff --git a/src/main/app/templates/_feedsearch.html b/src/main/app/templates/_feedsearch.html index 2c14722f..2f87e9ed 100644 --- a/src/main/app/templates/_feedsearch.html +++ b/src/main/app/templates/_feedsearch.html @@ -6,7 +6,7 @@

{{ 'feedsearch.help' | translate }} diff --git a/src/main/app/templates/_metrics.meter.html b/src/main/app/templates/_metrics.meter.html index dec86966..585d1386 100644 --- a/src/main/app/templates/_metrics.meter.html +++ b/src/main/app/templates/_metrics.meter.html @@ -4,14 +4,8 @@
Mean
{{metric.meanRate | number:2}}
-
1 min
-
{{metric.oneMinuteRate | number:2}}
- -
5 min
-
{{metric.fiveMinuteRate | number:2}}
- -
15 min
-
{{metric.fifteenMinuteRate | number:2}}
+
1/5/15 min
+
{{metric.oneMinuteRate | number:2}} {{metric.fiveMinuteRate | number:2}} {{metric.fifteenMinuteRate | number:2}}
Total
{{metric.count}}
diff --git a/src/main/app/templates/_metrics.timer.html b/src/main/app/templates/_metrics.timer.html new file mode 100644 index 00000000..638997f6 --- /dev/null +++ b/src/main/app/templates/_metrics.timer.html @@ -0,0 +1,17 @@ +
+ {{label}} +
+
Mean
+
{{metric.meanRate | number:2}}
+ +
1/5/15 min
+
{{metric.oneMinuteRate | number:2}} {{metric.fiveMinuteRate | number:2}} {{metric.fifteenMinuteRate | number:2}}
+ +
Total
+
{{metric.count}}
+ +
min/max/mean (ms)
+
{{metric.snapshot.min/1000000 | number:0}} {{metric.snapshot.max/1000000 | number:0}} {{metric.snapshot.mean/1000000 | number:0}}
+ +
+
\ No newline at end of file diff --git a/src/main/app/templates/_tags.html b/src/main/app/templates/_tags.html index 2c59e872..31923388 100644 --- a/src/main/app/templates/_tags.html +++ b/src/main/app/templates/_tags.html @@ -1,12 +1,12 @@ - + {{ 'global.tags' | translate }} - + {{tag}} - + \ No newline at end of file diff --git a/src/main/app/templates/_toolbar.html b/src/main/app/templates/_toolbar.html index e6ce7707..d59c3d8f 100644 --- a/src/main/app/templates/_toolbar.html +++ b/src/main/app/templates/_toolbar.html @@ -124,9 +124,9 @@ diff --git a/src/main/app/templates/admin.metrics.html b/src/main/app/templates/admin.metrics.html index 64f05657..d853fcbc 100644 --- a/src/main/app/templates/admin.metrics.html +++ b/src/main/app/templates/admin.metrics.html @@ -1,21 +1,28 @@
- - - - - +
+ + + + + + + + - - + + - - - - - - + + + +
+
+
+ +
+
\ No newline at end of file diff --git a/src/main/app/templates/feeds.category_details.html b/src/main/app/templates/feeds.category_details.html index d0698c7b..d44441f8 100644 --- a/src/main/app/templates/feeds.category_details.html +++ b/src/main/app/templates/feeds.category_details.html @@ -30,7 +30,7 @@
-
+
{{ 'global.link' | translate }} {{ 'details.generate_api_key_first' | translate }}
diff --git a/src/main/app/templates/feeds.feed_details.html b/src/main/app/templates/feeds.feed_details.html index 699b25f9..66f1ebe1 100644 --- a/src/main/app/templates/feeds.feed_details.html +++ b/src/main/app/templates/feeds.feed_details.html @@ -3,15 +3,16 @@

{{ 'details.feed_details' | translate }}

+
{{ error }}
-
- @@ -49,26 +50,34 @@
-
+
{{sub.nextRefresh|entryDate:('details.queued_for_refresh' | translate) }}
-
+
{{sub.message}}
-
+
{{ 'global.link' | translate }} {{ 'details.generate_api_key_first' | translate }}
+
+ +
+ +

+
+
+
diff --git a/src/main/app/templates/feeds.view.html b/src/main/app/templates/feeds.view.html index 9d29c0a6..1f306f8e 100644 --- a/src/main/app/templates/feeds.view.html +++ b/src/main/app/templates/feeds.view.html @@ -51,6 +51,9 @@ + + ({{entry.categories}}) +
@@ -58,8 +61,7 @@
- -
+
diff --git a/src/main/java/com/commafeed/CommaFeedApplication.java b/src/main/java/com/commafeed/CommaFeedApplication.java index 41018307..decb0fc1 100644 --- a/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/src/main/java/com/commafeed/CommaFeedApplication.java @@ -1,17 +1,9 @@ package com.commafeed; -import io.dropwizard.Application; -import io.dropwizard.assets.AssetsBundle; -import io.dropwizard.db.DataSourceFactory; -import io.dropwizard.hibernate.HibernateBundle; -import io.dropwizard.migrations.MigrationsBundle; -import io.dropwizard.servlets.CacheBustingFilter; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; - import java.io.IOException; import java.util.Date; import java.util.EnumSet; +import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import javax.servlet.DispatcherType; @@ -22,6 +14,7 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.eclipse.jetty.server.session.SessionHandler; +import org.hibernate.cfg.AvailableSettings; import com.commafeed.backend.feed.FeedRefreshTaskGiver; import com.commafeed.backend.feed.FeedRefreshUpdater; @@ -39,10 +32,8 @@ import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.service.StartupService; import com.commafeed.backend.service.UserService; -import com.commafeed.backend.task.OldStatusesCleanupTask; -import com.commafeed.backend.task.OrphansCleanupTask; -import com.commafeed.frontend.auth.SecurityCheckProvider; -import com.commafeed.frontend.auth.SecurityCheckProvider.SecurityCheckUserServiceProvider; +import com.commafeed.backend.task.ScheduledTask; +import com.commafeed.frontend.auth.SecurityCheckFactoryProvider; import com.commafeed.frontend.resource.AdminREST; import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.frontend.resource.EntryREST; @@ -54,19 +45,22 @@ import com.commafeed.frontend.servlet.AnalyticsServlet; import com.commafeed.frontend.servlet.CustomCssServlet; import com.commafeed.frontend.servlet.LogoutServlet; import com.commafeed.frontend.servlet.NextUnreadServlet; -import com.commafeed.frontend.session.SessionHelperProvider; +import com.commafeed.frontend.session.SessionHelperFactoryProvider; import com.google.inject.Guice; import com.google.inject.Injector; -import com.sun.jersey.api.core.ResourceConfig; -import com.wordnik.swagger.config.ConfigFactory; -import com.wordnik.swagger.config.ScannerFactory; -import com.wordnik.swagger.config.SwaggerConfig; -import com.wordnik.swagger.jaxrs.config.DefaultJaxrsScanner; -import com.wordnik.swagger.jaxrs.listing.ApiDeclarationProvider; -import com.wordnik.swagger.jaxrs.listing.ApiListingResourceJSON; -import com.wordnik.swagger.jaxrs.listing.ResourceListingProvider; -import com.wordnik.swagger.jaxrs.reader.DefaultJaxrsApiReader; -import com.wordnik.swagger.reader.ClassReaders; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; + +import io.dropwizard.Application; +import io.dropwizard.assets.AssetsBundle; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.forms.MultiPartBundle; +import io.dropwizard.hibernate.HibernateBundle; +import io.dropwizard.migrations.MigrationsBundle; +import io.dropwizard.server.DefaultServerFactory; +import io.dropwizard.servlets.CacheBustingFilter; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; public class CommaFeedApplication extends Application { @@ -89,36 +83,44 @@ public class CommaFeedApplication extends Application { FeedSubscription.class, User.class, UserRole.class, UserSettings.class) { @Override public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { - return configuration.getDatabase(); + DataSourceFactory factory = configuration.getDataSourceFactory(); + + // keep using old id generator for backward compatibility + factory.getProperties().put(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, "false"); + + factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50"); + factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true"); + return factory; } }); bootstrap.addBundle(new MigrationsBundle() { @Override public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { - return configuration.getDatabase(); + return configuration.getDataSourceFactory(); } }); bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html")); + bootstrap.addBundle(new MultiPartBundle()); } @Override public void run(CommaFeedConfiguration config, Environment environment) throws Exception { - // configure context path - environment.getApplicationContext().setContextPath(config.getApplicationSettings().getContextPath()); - // guice init Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics())); - // Auth/session management + // session management environment.servlets().setSessionHandler(new SessionHandler(config.getSessionManagerFactory().build())); - environment.jersey().register(new SecurityCheckUserServiceProvider(injector.getInstance(UserService.class))); - environment.jersey().register(SecurityCheckProvider.class); - environment.jersey().register(SessionHelperProvider.class); + + // support for "@SecurityCheck User user" injection + environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class))); + // support for "@Context SessionHelper sessionHelper" injection + environment.jersey().register(new SessionHelperFactoryProvider.Binder()); // REST resources environment.jersey().setUrlPattern("/rest/*"); + ((DefaultServerFactory) config.getServerFactory()).setJerseyRootPath("/rest/*"); environment.jersey().register(injector.getInstance(AdminREST.class)); environment.jersey().register(injector.getInstance(CategoryREST.class)); environment.jersey().register(injector.getInstance(EntryREST.class)); @@ -134,9 +136,13 @@ public class CommaFeedApplication extends Application { environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js"); // Scheduled tasks - ScheduledExecutorService executor = environment.lifecycle().scheduledExecutorService("task-scheduler").build(); - injector.getInstance(OldStatusesCleanupTask.class).register(executor); - injector.getInstance(OrphansCleanupTask.class).register(executor); + Set tasks = injector.getInstance(Key.get(new TypeLiteral>() { + })); + ScheduledExecutorService executor = environment.lifecycle().scheduledExecutorService("task-scheduler", true).threads(tasks.size()) + .build(); + for (ScheduledTask task : tasks) { + task.register(executor); + } // database init/changelogs environment.lifecycle().manage(injector.getInstance(StartupService.class)); @@ -146,16 +152,6 @@ public class CommaFeedApplication extends Application { environment.lifecycle().manage(injector.getInstance(FeedRefreshWorker.class)); environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class)); - // Swagger - environment.jersey().register(new ApiListingResourceJSON()); - environment.jersey().register(new ApiDeclarationProvider()); - environment.jersey().register(new ResourceListingProvider()); - ScannerFactory.setScanner(new DefaultJaxrsScanner()); - ClassReaders.setReader(new DefaultJaxrsApiReader()); - SwaggerConfig swaggerConfig = ConfigFactory.config(); - swaggerConfig.setApiVersion("1"); - swaggerConfig.setBasePath("/rest"); - // cache configuration // prevent caching on REST resources, except for favicons environment.servlets().addFilter("cache-filter", new CacheBustingFilter() { @@ -170,8 +166,6 @@ public class CommaFeedApplication extends Application { } }).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*"); - // enable wadl - environment.jersey().disable(ResourceConfig.FEATURE_DISABLE_WADL); } public static void main(String[] args) throws Exception { diff --git a/src/main/java/com/commafeed/CommaFeedConfiguration.java b/src/main/java/com/commafeed/CommaFeedConfiguration.java index 5e8fc163..018b991e 100644 --- a/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -12,7 +12,7 @@ import javax.validation.constraints.NotNull; import lombok.Getter; -import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.time.DateUtils; import org.hibernate.validator.constraints.NotBlank; import com.commafeed.backend.cache.RedisPoolFactory; @@ -35,7 +35,7 @@ public class CommaFeedConfiguration extends Configuration { @Valid @NotNull @JsonProperty("database") - private DataSourceFactory database = new DataSourceFactory(); + private DataSourceFactory dataSourceFactory = new DataSourceFactory(); @Valid @NotNull @@ -64,60 +64,76 @@ public class CommaFeedConfiguration extends Configuration { public static class ApplicationSettings { @NotNull @NotBlank - private String contextPath; - - @NotNull - @NotBlank + @Valid private String publicUrl; @NotNull - private boolean allowRegistrations; + @Valid + private Boolean allowRegistrations; + + @NotNull + @Valid + private Boolean createDemoAccount; private String googleAnalyticsTrackingCode; - @NotNull - @Min(1) - private int backgroundThreads; + private String googleAuthKey; @NotNull @Min(1) - private int databaseUpdateThreads; + @Valid + private Integer backgroundThreads; + + @NotNull + @Min(1) + @Valid + private Integer databaseUpdateThreads; private String smtpHost; - private int smtpPort; - private boolean smtpTls; - private String smtpUserName; - private String smtpPassword; + private String smtpFromAddress; @NotNull - private boolean heavyLoad; + @Valid + private Boolean heavyLoad; @NotNull - private boolean pubsubhubbub; + @Valid + private Boolean pubsubhubbub; @NotNull - private boolean imageProxyEnabled; + @Valid + private Boolean imageProxyEnabled; @NotNull @Min(0) - private int queryTimeout; + @Valid + private Integer queryTimeout; @NotNull @Min(0) - private int keepStatusDays; + @Valid + private Integer keepStatusDays; @NotNull @Min(0) - private int refreshIntervalMinutes; + @Valid + private Integer maxFeedCapacity; @NotNull + @Min(0) + @Valid + private Integer refreshIntervalMinutes; + + @NotNull + @Valid private CacheType cache; @NotNull + @Valid private String announcement; public Date getUnreadThreshold() { diff --git a/src/main/java/com/commafeed/CommaFeedModule.java b/src/main/java/com/commafeed/CommaFeedModule.java index a18b0be3..9cb83429 100644 --- a/src/main/java/com/commafeed/CommaFeedModule.java +++ b/src/main/java/com/commafeed/CommaFeedModule.java @@ -11,9 +11,15 @@ import com.commafeed.CommaFeedConfiguration.CacheType; import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.NoopCacheService; import com.commafeed.backend.cache.RedisCacheService; -import com.commafeed.backend.favicon.DefaultFaviconFetcher; import com.commafeed.backend.favicon.AbstractFaviconFetcher; +import com.commafeed.backend.favicon.DefaultFaviconFetcher; +import com.commafeed.backend.favicon.FacebookFaviconFetcher; import com.commafeed.backend.favicon.YoutubeFaviconFetcher; +import com.commafeed.backend.task.OldEntriesCleanupTask; +import com.commafeed.backend.task.OldStatusesCleanupTask; +import com.commafeed.backend.task.OrphanedContentsCleanupTask; +import com.commafeed.backend.task.OrphanedFeedsCleanupTask; +import com.commafeed.backend.task.ScheduledTask; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.multibindings.Multibinder; @@ -38,8 +44,15 @@ public class CommaFeedModule extends AbstractModule { log.info("using cache {}", cacheService.getClass()); bind(CacheService.class).toInstance(cacheService); - Multibinder multibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class); - multibinder.addBinding().to(YoutubeFaviconFetcher.class); - multibinder.addBinding().to(DefaultFaviconFetcher.class); + Multibinder faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class); + faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class); + faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class); + faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class); + + Multibinder taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class); + taskMultibinder.addBinding().to(OldStatusesCleanupTask.class); + taskMultibinder.addBinding().to(OldEntriesCleanupTask.class); + taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class); + taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class); } } diff --git a/src/main/java/com/commafeed/backend/ContentEncodingInterceptor.java b/src/main/java/com/commafeed/backend/ContentEncodingInterceptor.java index 939ea478..7c4ad666 100644 --- a/src/main/java/com/commafeed/backend/ContentEncodingInterceptor.java +++ b/src/main/java/com/commafeed/backend/ContentEncodingInterceptor.java @@ -16,7 +16,7 @@ import org.apache.http.entity.HttpEntityWrapper; import org.apache.http.protocol.HttpContext; class ContentEncodingInterceptor implements HttpResponseInterceptor { - + private static final Set ALLOWED_CONTENT_ENCODINGS = new HashSet<>(Arrays.asList("gzip", "x-gzip", "deflate", "identity")); @Override @@ -28,17 +28,17 @@ class ContentEncodingInterceptor implements HttpResponseInterceptor { } } } - + private boolean containsUnsupportedEncodings(Header contentEncodingHeader) { HeaderElement[] codecs = contentEncodingHeader.getElements(); - + for (final HeaderElement codec : codecs) { String codecName = codec.getName().toLowerCase(Locale.US); if (!ALLOWED_CONTENT_ENCODINGS.contains(codecName)) { return true; } } - + return false; } @@ -47,9 +47,9 @@ class ContentEncodingInterceptor implements HttpResponseInterceptor { @Override public Header getContentEncoding() { return null; - }; + } }; - + response.setEntity(wrapped); } diff --git a/src/main/java/com/commafeed/backend/HttpGetter.java b/src/main/java/com/commafeed/backend/HttpGetter.java index bca16f9f..a2ac67da 100644 --- a/src/main/java/com/commafeed/backend/HttpGetter.java +++ b/src/main/java/com/commafeed/backend/HttpGetter.java @@ -17,7 +17,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.Consts; import org.apache.http.Header; import org.apache.http.HttpEntity; @@ -34,7 +34,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.ConnectionConfig; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; @@ -53,7 +53,7 @@ public class HttpGetter { private static final String ACCEPT_LANGUAGE = "en"; private static final String PRAGMA_NO_CACHE = "No-cache"; private static final String CACHE_CONTROL_NO_CACHE = "no-cache"; - + private static final HttpResponseInterceptor REMOVE_INCORRECT_CONTENT_ENCODING = new ContentEncodingInterceptor(); private static SSLContext SSL_CONTEXT = null; @@ -181,8 +181,8 @@ public class HttpGetter { builder.addInterceptorFirst(REMOVE_INCORRECT_CONTENT_ENCODING); builder.disableAutomaticRetries(); - builder.setSslcontext(SSL_CONTEXT); - builder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + builder.setSSLContext(SSL_CONTEXT); + builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE); RequestConfig.Builder configBuilder = RequestConfig.custom(); configBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES); diff --git a/src/main/java/com/commafeed/backend/cache/RedisCacheService.java b/src/main/java/com/commafeed/backend/cache/RedisCacheService.java index 6a3eb460..8dd95134 100644 --- a/src/main/java/com/commafeed/backend/cache/RedisCacheService.java +++ b/src/main/java/com/commafeed/backend/cache/RedisCacheService.java @@ -1,5 +1,6 @@ package com.commafeed.backend.cache; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -18,7 +19,6 @@ import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.UnreadCount; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.Lists; @Slf4j @RequiredArgsConstructor @@ -30,7 +30,7 @@ public class RedisCacheService extends CacheService { @Override public List getLastEntries(Feed feed) { - List list = Lists.newArrayList(); + List list = new ArrayList<>(); try (Jedis jedis = pool.getResource()) { String key = buildRedisEntryKey(feed); Set members = jedis.smembers(key); diff --git a/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java b/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java index 302a2793..9cdb6671 100644 --- a/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java +++ b/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java @@ -2,7 +2,7 @@ package com.commafeed.backend.cache; import lombok.Getter; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; diff --git a/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java b/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java index d8e94817..6fc78806 100644 --- a/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java +++ b/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java @@ -1,19 +1,19 @@ package com.commafeed.backend.dao; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; -import org.apache.commons.lang.ObjectUtils; import org.hibernate.SessionFactory; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.QFeedCategory; import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.User; -import com.google.common.collect.Lists; -import com.mysema.query.types.Predicate; +import com.querydsl.core.types.Predicate; @Singleton public class FeedCategoryDAO extends GenericDAO { @@ -26,11 +26,11 @@ public class FeedCategoryDAO extends GenericDAO { } public List findAll(User user) { - return newQuery().from(category).where(category.user.eq(user)).join(category.user, QUser.user).fetch().list(category); + return query().selectFrom(category).where(category.user.eq(user)).join(category.user, QUser.user).fetchJoin().fetch(); } public FeedCategory findById(User user, Long id) { - return newQuery().from(category).where(category.user.eq(user), category.id.eq(id)).uniqueResult(category); + return query().selectFrom(category).where(category.user.eq(user), category.id.eq(id)).fetchOne(); } public FeedCategory findByName(User user, String name, FeedCategory parent) { @@ -40,7 +40,7 @@ public class FeedCategoryDAO extends GenericDAO { } else { parentPredicate = category.parent.eq(parent); } - return newQuery().from(category).where(category.user.eq(user), category.name.eq(name), parentPredicate).uniqueResult(category); + return query().selectFrom(category).where(category.user.eq(user), category.name.eq(name), parentPredicate).fetchOne(); } public List findByParent(User user, FeedCategory parent) { @@ -50,18 +50,11 @@ public class FeedCategoryDAO extends GenericDAO { } else { parentPredicate = category.parent.eq(parent); } - return newQuery().from(category).where(category.user.eq(user), parentPredicate).list(category); + return query().selectFrom(category).where(category.user.eq(user), parentPredicate).fetch(); } public List findAllChildrenCategories(User user, FeedCategory parent) { - List list = Lists.newArrayList(); - List all = findAll(user); - for (FeedCategory cat : all) { - if (isChild(cat, parent)) { - list.add(cat); - } - } - return list; + return findAll(user).stream().filter(c -> isChild(c, parent)).collect(Collectors.toList()); } private boolean isChild(FeedCategory child, FeedCategory parent) { @@ -70,7 +63,7 @@ public class FeedCategoryDAO extends GenericDAO { } boolean isChild = false; while (child != null) { - if (ObjectUtils.equals(child.getId(), parent.getId())) { + if (Objects.equals(child.getId(), parent.getId())) { isChild = true; break; } diff --git a/src/main/java/com/commafeed/backend/dao/FeedDAO.java b/src/main/java/com/commafeed/backend/dao/FeedDAO.java index f8dad975..67f86cb8 100644 --- a/src/main/java/com/commafeed/backend/dao/FeedDAO.java +++ b/src/main/java/com/commafeed/backend/dao/FeedDAO.java @@ -7,7 +7,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.hibernate.SessionFactory; import com.commafeed.backend.model.Feed; @@ -15,8 +15,9 @@ import com.commafeed.backend.model.QFeed; import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QUser; import com.google.common.collect.Iterables; -import com.mysema.query.BooleanBuilder; -import com.mysema.query.jpa.hibernate.HibernateQuery; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.hibernate.HibernateQuery; @Singleton public class FeedDAO extends GenericDAO { @@ -29,24 +30,23 @@ public class FeedDAO extends GenericDAO { } public List findNextUpdatable(int count, Date lastLoginThreshold) { - BooleanBuilder disabledDatePredicate = new BooleanBuilder(); - disabledDatePredicate.or(feed.disabledUntil.isNull()); - disabledDatePredicate.or(feed.disabledUntil.lt(new Date())); + HibernateQuery query = query().selectFrom(feed); + query.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date()))); - HibernateQuery query = newQuery().from(feed); if (lastLoginThreshold != null) { QFeedSubscription subs = QFeedSubscription.feedSubscription; QUser user = QUser.user; - query.join(feed.subscriptions, subs).join(subs.user, user).where(disabledDatePredicate, user.lastLogin.gt(lastLoginThreshold)); - } else { - query.where(disabledDatePredicate); + + JPQLQuery subQuery = JPAExpressions.selectOne().from(subs); + subQuery.join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold)); + query.where(subQuery.exists()); } - return query.orderBy(feed.disabledUntil.asc()).limit(count).distinct().list(feed); + return query.orderBy(feed.disabledUntil.asc()).limit(count).distinct().fetch(); } public Feed findByUrl(String normalizedUrl) { - List feeds = newQuery().from(feed).where(feed.normalizedUrlHash.eq(DigestUtils.sha1Hex(normalizedUrl))).list(feed); + List feeds = query().selectFrom(feed).where(feed.normalizedUrlHash.eq(DigestUtils.sha1Hex(normalizedUrl))).fetch(); Feed feed = Iterables.getFirst(feeds, null); if (feed != null && StringUtils.equals(normalizedUrl, feed.getNormalizedUrl())) { return feed; @@ -55,11 +55,12 @@ public class FeedDAO extends GenericDAO { } public List findByTopic(String topic) { - return newQuery().from(feed).where(feed.pushTopicHash.eq(DigestUtils.sha1Hex(topic))).list(feed); + return query().selectFrom(feed).where(feed.pushTopicHash.eq(DigestUtils.sha1Hex(topic))).fetch(); } public List findWithoutSubscriptions(int max) { QFeedSubscription sub = QFeedSubscription.feedSubscription; - return newQuery().from(feed).leftJoin(feed.subscriptions, sub).where(sub.id.isNull()).limit(max).list(feed); + return query().selectFrom(feed).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(feed)).notExists()).limit(max) + .fetch(); } } diff --git a/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java b/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java index b0239132..68eeb41e 100644 --- a/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java +++ b/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java @@ -10,12 +10,14 @@ import org.hibernate.SessionFactory; import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntryContent; -import com.google.common.collect.Iterables; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; @Singleton public class FeedEntryContentDAO extends GenericDAO { private QFeedEntryContent content = QFeedEntryContent.feedEntryContent; + private QFeedEntry entry = QFeedEntry.feedEntry; @Inject public FeedEntryContentDAO(SessionFactory sessionFactory) { @@ -23,15 +25,15 @@ public class FeedEntryContentDAO extends GenericDAO { } public Long findExisting(String contentHash, String titleHash) { - List list = newQuery().from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)).limit(1) - .list(content.id); - return Iterables.getFirst(list, null); + return query().select(content.id).from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)) + .fetchFirst(); } public int deleteWithoutEntries(int max) { - QFeedEntry entry = QFeedEntry.feedEntry; - List list = newQuery().from(content).leftJoin(content.entries, entry).where(entry.id.isNull()).limit(max) - .list(content); + + JPQLQuery subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id)); + List list = query().selectFrom(content).where(subQuery.notExists()).limit(max).fetch(); + int deleted = list.size(); delete(list); return deleted; diff --git a/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java b/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java index 65f9997f..bbb3853f 100644 --- a/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java +++ b/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java @@ -1,7 +1,7 @@ package com.commafeed.backend.dao; -import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -11,10 +11,12 @@ import org.hibernate.SessionFactory; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; -import com.commafeed.backend.model.QFeed; import com.commafeed.backend.model.QFeedEntry; -import com.commafeed.backend.model.QFeedSubscription; -import com.google.common.collect.Iterables; +import com.querydsl.core.Tuple; +import com.querydsl.core.types.dsl.NumberExpression; + +import lombok.AllArgsConstructor; +import lombok.Getter; @Singleton public class FeedEntryDAO extends GenericDAO { @@ -27,22 +29,32 @@ public class FeedEntryDAO extends GenericDAO { } public Long findExisting(String guid, Feed feed) { - List list = newQuery().from(entry).where(entry.guidHash.eq(DigestUtils.sha1Hex(guid)), entry.feed.eq(feed)).limit(1) - .list(entry.id); - return Iterables.getFirst(list, null); + return query().select(entry.id).from(entry).where(entry.guidHash.eq(DigestUtils.sha1Hex(guid)), entry.feed.eq(feed)).limit(1) + .fetchOne(); } - public List findWithoutSubscriptions(int max) { - QFeed feed = QFeed.feed; - QFeedSubscription sub = QFeedSubscription.feedSubscription; - return newQuery().from(entry).join(entry.feed, feed).leftJoin(feed.subscriptions, sub).where(sub.id.isNull()).limit(max) - .list(entry); + public List findFeedsExceedingCapacity(long maxCapacity, long max) { + NumberExpression count = entry.id.count(); + List tuples = query().select(entry.feed.id, count).from(entry).groupBy(entry.feed).having(count.gt(maxCapacity)).limit(max) + .fetch(); + return tuples.stream().map(t -> new FeedCapacity(t.get(entry.feed.id), t.get(count))).collect(Collectors.toList()); } - public int delete(Date olderThan, int max) { - List list = newQuery().from(entry).where(entry.inserted.lt(olderThan)).limit(max).list(entry); - int deleted = list.size(); - delete(list); - return deleted; + public int delete(Long feedId, long max) { + + List list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch(); + return delete(list); + } + + public int deleteOldEntries(Long feedId, long max) { + List list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).orderBy(entry.updated.asc()).limit(max).fetch(); + return delete(list); + } + + @AllArgsConstructor + @Getter + public static class FeedCapacity { + private Long id; + private Long capacity; } } diff --git a/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java b/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java index a349c8ed..2f1ea425 100644 --- a/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java +++ b/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java @@ -1,5 +1,6 @@ package com.commafeed.backend.dao; +import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; @@ -7,12 +8,14 @@ import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.builder.CompareToBuilder; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.builder.CompareToBuilder; import org.hibernate.SessionFactory; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.FixedSizeSortedSet; +import com.commafeed.backend.feed.FeedEntryKeyword; +import com.commafeed.backend.feed.FeedEntryKeyword.Mode; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryTag; @@ -26,11 +29,10 @@ import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.frontend.model.UnreadCount; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; import com.google.common.collect.Ordering; -import com.mysema.query.BooleanBuilder; -import com.mysema.query.Tuple; -import com.mysema.query.jpa.hibernate.HibernateQuery; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.Tuple; +import com.querydsl.jpa.hibernate.HibernateQuery; @Singleton public class FeedEntryStatusDAO extends GenericDAO { @@ -60,13 +62,13 @@ public class FeedEntryStatusDAO extends GenericDAO { builder.append(o2.getEntryUpdated(), o1.getEntryUpdated()); builder.append(o2.getId(), o1.getId()); return builder.toComparison(); - }; + } }; private static final Comparator STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse(); public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) { - List statuses = newQuery().from(status).where(status.entry.eq(entry), status.subscription.eq(sub)).list(status); + List statuses = query().selectFrom(status).where(status.entry.eq(entry), status.subscription.eq(sub)).fetch(); FeedEntryStatus status = Iterables.getFirst(statuses, null); return handleStatus(user, status, sub, entry); } @@ -91,7 +93,7 @@ public class FeedEntryStatusDAO extends GenericDAO { } public List findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) { - HibernateQuery query = newQuery().from(status).where(status.user.eq(user), status.starred.isTrue()); + HibernateQuery query = query().selectFrom(status).where(status.user.eq(user), status.starred.isTrue()); if (newerThan != null) { query.where(status.entryInserted.gt(newerThan)); } @@ -102,9 +104,13 @@ public class FeedEntryStatusDAO extends GenericDAO { query.orderBy(status.entryUpdated.desc(), status.id.desc()); } - query.offset(offset).limit(limit).setTimeout(config.getApplicationSettings().getQueryTimeout()); + query.offset(offset).limit(limit); + int timeout = config.getApplicationSettings().getQueryTimeout(); + if (timeout > 0) { + query.setTimeout(timeout / 1000); + } - List statuses = query.list(status); + List statuses = query.fetch(); for (FeedEntryStatus status : statuses) { status = handleStatus(user, status, status.getSubscription(), status.getEntry()); fetchTags(user, status); @@ -112,18 +118,21 @@ public class FeedEntryStatusDAO extends GenericDAO { return lazyLoadContent(includeContent, statuses); } - private HibernateQuery buildQuery(User user, FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset, - int limit, ReadingOrder order, Date last, String tag) { + private HibernateQuery buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List keywords, + Date newerThan, int offset, int limit, ReadingOrder order, Date last, String tag) { - HibernateQuery query = newQuery().from(entry).where(entry.feed.eq(sub.getFeed())); + HibernateQuery query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed())); - if (keywords != null) { + if (CollectionUtils.isNotEmpty(keywords)) { query.join(entry.content, content); - for (String keyword : StringUtils.split(keywords)) { + for (FeedEntryKeyword keyword : keywords) { BooleanBuilder or = new BooleanBuilder(); - or.or(content.content.containsIgnoreCase(keyword)); - or.or(content.title.containsIgnoreCase(keyword)); + or.or(content.content.containsIgnoreCase(keyword.getKeyword())); + or.or(content.title.containsIgnoreCase(keyword.getKeyword())); + if (keyword.getMode() == Mode.EXCLUDE) { + or.not(); + } query.where(or); } } @@ -180,15 +189,16 @@ public class FeedEntryStatusDAO extends GenericDAO { return query; } - public List findBySubscriptions(User user, List subs, boolean unreadOnly, String keywords, - Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds, String tag) { + public List findBySubscriptions(User user, List subs, boolean unreadOnly, + List keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, + boolean onlyIds, String tag) { int capacity = offset + limit; Comparator comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC; FixedSizeSortedSet set = new FixedSizeSortedSet(capacity, comparator); for (FeedSubscription sub : subs) { Date last = (order != null && set.isFull()) ? set.last().getEntryUpdated() : null; - HibernateQuery query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag); - List tuples = query.list(entry.id, entry.updated, status.id); + HibernateQuery query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag); + List tuples = query.select(entry.id, entry.updated, status.id).fetch(); for (Tuple tuple : tuples) { Long id = tuple.get(entry.id); Date updated = tuple.get(entry.updated); @@ -211,7 +221,7 @@ public class FeedEntryStatusDAO extends GenericDAO { List placeholders = set.asList(); int size = placeholders.size(); if (size < offset) { - return Lists.newArrayList(); + return new ArrayList<>(); } placeholders = placeholders.subList(Math.max(offset, 0), size); @@ -219,7 +229,7 @@ public class FeedEntryStatusDAO extends GenericDAO { if (onlyIds) { statuses = placeholders; } else { - statuses = Lists.newArrayList(); + statuses = new ArrayList<>(); for (FeedEntryStatus placeholder : placeholders) { Long statusId = placeholder.getId(); FeedEntry entry = feedEntryDAO.findById(placeholder.getEntry().getId()); @@ -235,8 +245,8 @@ public class FeedEntryStatusDAO extends GenericDAO { public UnreadCount getUnreadCount(User user, FeedSubscription subscription) { UnreadCount uc = null; - HibernateQuery query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null); - List tuples = query.list(entry.count(), entry.updated.max()); + HibernateQuery query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null); + List tuples = query.select(entry.count(), entry.updated.max()).fetch(); for (Tuple tuple : tuples) { Long count = tuple.get(entry.count()); Date updated = tuple.get(entry.updated.max()); @@ -256,7 +266,7 @@ public class FeedEntryStatusDAO extends GenericDAO { } public List getOldStatuses(Date olderThan, int limit) { - return newQuery().from(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).list(status); + return query().selectFrom(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).fetch(); } } diff --git a/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java b/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java index e2dc3ac9..f0ef9388 100644 --- a/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java +++ b/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java @@ -23,10 +23,10 @@ public class FeedEntryTagDAO extends GenericDAO { } public List findByUser(User user) { - return newQuery().from(tag).where(tag.user.eq(user)).distinct().list(tag.name); + return query().selectDistinct(tag.name).from(tag).where(tag.user.eq(user)).fetch(); } public List findByEntry(User user, FeedEntry entry) { - return newQuery().from(tag).where(tag.user.eq(user), tag.entry.eq(entry)).list(tag); + return query().selectFrom(tag).where(tag.user.eq(user), tag.entry.eq(entry)).fetch(); } } diff --git a/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java b/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java index efcddd62..bfdc39e0 100644 --- a/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java +++ b/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java @@ -1,6 +1,8 @@ package com.commafeed.backend.dao; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -13,10 +15,8 @@ import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.Models; import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.User; -import com.google.common.base.Function; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.mysema.query.jpa.hibernate.HibernateQuery; +import com.querydsl.jpa.hibernate.HibernateQuery; @Singleton public class FeedSubscriptionDAO extends GenericDAO { @@ -29,57 +29,44 @@ public class FeedSubscriptionDAO extends GenericDAO { } public FeedSubscription findById(User user, Long id) { - List subs = newQuery().from(sub).where(sub.user.eq(user), sub.id.eq(id)).leftJoin(sub.feed).fetch() - .leftJoin(sub.category).fetch().list(sub); + List subs = query().selectFrom(sub).where(sub.user.eq(user), sub.id.eq(id)).leftJoin(sub.feed).fetchJoin() + .leftJoin(sub.category).fetchJoin().fetch(); return initRelations(Iterables.getFirst(subs, null)); } public List findByFeed(Feed feed) { - return newQuery().from(sub).where(sub.feed.eq(feed)).list(sub); + return query().selectFrom(sub).where(sub.feed.eq(feed)).fetch(); } public FeedSubscription findByFeed(User user, Feed feed) { - List subs = newQuery().from(sub).where(sub.user.eq(user), sub.feed.eq(feed)).list(sub); + List subs = query().selectFrom(sub).where(sub.user.eq(user), sub.feed.eq(feed)).fetch(); return initRelations(Iterables.getFirst(subs, null)); } public List findAll(User user) { - List subs = newQuery().from(sub).where(sub.user.eq(user)).leftJoin(sub.feed).fetch().leftJoin(sub.category) - .fetch().list(sub); + List subs = query().selectFrom(sub).where(sub.user.eq(user)).leftJoin(sub.feed).fetchJoin() + .leftJoin(sub.category).fetchJoin().fetch(); return initRelations(subs); } public List findByCategory(User user, FeedCategory category) { - HibernateQuery query = newQuery().from(sub).where(sub.user.eq(user)); + HibernateQuery query = query().selectFrom(sub).where(sub.user.eq(user)); if (category == null) { query.where(sub.category.isNull()); } else { query.where(sub.category.eq(category)); } - return initRelations(query.list(sub)); + return initRelations(query.fetch()); } public List findByCategories(User user, List categories) { - List categoryIds = Lists.transform(categories, new Function() { - @Override - public Long apply(FeedCategory input) { - return input.getId(); - } - }); - - List subscriptions = Lists.newArrayList(); - for (FeedSubscription sub : findAll(user)) { - if (sub.getCategory() != null && categoryIds.contains(sub.getCategory().getId())) { - subscriptions.add(sub); - } - } - return subscriptions; + Set categoryIds = categories.stream().map(c -> c.getId()).collect(Collectors.toSet()); + return findAll(user).stream().filter(s -> s.getCategory() != null && categoryIds.contains(s.getCategory().getId())) + .collect(Collectors.toList()); } private List initRelations(List list) { - for (FeedSubscription sub : list) { - initRelations(sub); - } + list.forEach(s -> initRelations(s)); return list; } diff --git a/src/main/java/com/commafeed/backend/dao/GenericDAO.java b/src/main/java/com/commafeed/backend/dao/GenericDAO.java index 338ada5f..3a6884ad 100644 --- a/src/main/java/com/commafeed/backend/dao/GenericDAO.java +++ b/src/main/java/com/commafeed/backend/dao/GenericDAO.java @@ -1,22 +1,25 @@ package com.commafeed.backend.dao; -import io.dropwizard.hibernate.AbstractDAO; - import java.util.Collection; import org.hibernate.SessionFactory; import com.commafeed.backend.model.AbstractModel; -import com.mysema.query.jpa.hibernate.HibernateQuery; +import com.querydsl.jpa.hibernate.HibernateQueryFactory; + +import io.dropwizard.hibernate.AbstractDAO; public abstract class GenericDAO extends AbstractDAO { + private HibernateQueryFactory factory; + protected GenericDAO(SessionFactory sessionFactory) { super(sessionFactory); + this.factory = new HibernateQueryFactory(() -> currentSession()); } - protected HibernateQuery newQuery() { - return new HibernateQuery(currentSession()); + protected HibernateQueryFactory query() { + return factory; } public void saveOrUpdate(T model) { @@ -24,19 +27,7 @@ public abstract class GenericDAO extends AbstractDAO } public void saveOrUpdate(Collection models) { - for (T model : models) { - persist(model); - } - } - - public void merge(T model) { - currentSession().merge(model); - } - - public void merge(Collection models) { - for (T model : models) { - merge(model); - } + models.forEach(m -> persist(m)); } public T findById(Long id) { @@ -50,9 +41,7 @@ public abstract class GenericDAO extends AbstractDAO } public int delete(Collection objects) { - for (T object : objects) { - delete(object); - } + objects.forEach(o -> delete(o)); return objects.size(); } diff --git a/src/main/java/com/commafeed/backend/dao/UnitOfWork.java b/src/main/java/com/commafeed/backend/dao/UnitOfWork.java index 27f8d347..ae21c4a8 100644 --- a/src/main/java/com/commafeed/backend/dao/UnitOfWork.java +++ b/src/main/java/com/commafeed/backend/dao/UnitOfWork.java @@ -5,17 +5,26 @@ import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.context.internal.ManagedSessionContext; -public abstract class UnitOfWork { +public class UnitOfWork { - private SessionFactory sessionFactory; - - public UnitOfWork(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; + @FunctionalInterface + public static interface SessionRunner { + public void runInSession(); } - protected abstract T runInSession() throws Exception; + @FunctionalInterface + public static interface SessionRunnerReturningValue { + public T runInSession(); + } - public T run() { + public static void run(SessionFactory sessionFactory, SessionRunner sessionRunner) { + call(sessionFactory, () -> { + sessionRunner.runInSession(); + return null; + }); + } + + public static T call(SessionFactory sessionFactory, SessionRunnerReturningValue sessionRunner) { final Session session = sessionFactory.openSession(); if (ManagedSessionContext.hasBind(sessionFactory)) { throw new IllegalStateException("Already in a unit of work!"); @@ -25,11 +34,11 @@ public abstract class UnitOfWork { ManagedSessionContext.bind(session); session.beginTransaction(); try { - t = runInSession(); + t = sessionRunner.runInSession(); commitTransaction(session); } catch (Exception e) { rollbackTransaction(session); - this. rethrow(e); + UnitOfWork. rethrow(e); } } finally { session.close(); @@ -38,14 +47,14 @@ public abstract class UnitOfWork { return t; } - private void rollbackTransaction(Session session) { + private static void rollbackTransaction(Session session) { final Transaction txn = session.getTransaction(); if (txn != null && txn.isActive()) { txn.rollback(); } } - private void commitTransaction(Session session) { + private static void commitTransaction(Session session) { final Transaction txn = session.getTransaction(); if (txn != null && txn.isActive()) { txn.commit(); @@ -53,7 +62,7 @@ public abstract class UnitOfWork { } @SuppressWarnings("unchecked") - private void rethrow(Exception e) throws E { + private static void rethrow(Exception e) throws E { throw (E) e; } diff --git a/src/main/java/com/commafeed/backend/dao/UserDAO.java b/src/main/java/com/commafeed/backend/dao/UserDAO.java index 6159392f..8b51e95c 100644 --- a/src/main/java/com/commafeed/backend/dao/UserDAO.java +++ b/src/main/java/com/commafeed/backend/dao/UserDAO.java @@ -6,7 +6,6 @@ import javax.inject.Singleton; import org.hibernate.SessionFactory; import com.commafeed.backend.model.QUser; -import com.commafeed.backend.model.QUserRole; import com.commafeed.backend.model.User; @Singleton @@ -20,21 +19,18 @@ public class UserDAO extends GenericDAO { } public User findByName(String name) { - return newQuery().from(user).where(user.name.equalsIgnoreCase(name)).leftJoin(user.roles, QUserRole.userRole).fetch() - .uniqueResult(user); + return query().selectFrom(user).where(user.name.equalsIgnoreCase(name)).fetchOne(); } public User findByApiKey(String key) { - return newQuery().from(user).where(user.apiKey.equalsIgnoreCase(key)).leftJoin(user.roles, QUserRole.userRole).fetch() - .uniqueResult(user); + return query().selectFrom(user).where(user.apiKey.equalsIgnoreCase(key)).fetchOne(); } public User findByEmail(String email) { - return newQuery().from(user).where(user.email.equalsIgnoreCase(email)).leftJoin(user.roles, QUserRole.userRole).fetch() - .uniqueResult(user); + return query().selectFrom(user).where(user.email.equalsIgnoreCase(email)).fetchOne(); } public long count() { - return newQuery().from(user).count(); + return query().selectFrom(user).fetchCount(); } } diff --git a/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java b/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java index 883cd3ce..303d454e 100644 --- a/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java +++ b/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java @@ -2,6 +2,7 @@ package com.commafeed.backend.dao; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -12,7 +13,6 @@ import com.commafeed.backend.model.QUserRole; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole.Role; -import com.google.common.collect.Sets; @Singleton public class UserRoleDAO extends GenericDAO { @@ -25,18 +25,14 @@ public class UserRoleDAO extends GenericDAO { } public List findAll() { - return newQuery().from(role).leftJoin(role.user).fetch().distinct().list(role); + return query().selectFrom(role).leftJoin(role.user).fetchJoin().distinct().fetch(); } public List findAll(User user) { - return newQuery().from(role).where(role.user.eq(user)).distinct().list(role); + return query().selectFrom(role).where(role.user.eq(user)).distinct().fetch(); } public Set findRoles(User user) { - Set list = Sets.newHashSet(); - for (UserRole role : findAll(user)) { - list.add(role.getRole()); - } - return list; + return findAll(user).stream().map(r -> r.getRole()).collect(Collectors.toSet()); } } diff --git a/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java b/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java index 92e8afd5..0553ef84 100644 --- a/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java +++ b/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java @@ -20,6 +20,6 @@ public class UserSettingsDAO extends GenericDAO { } public UserSettings findByUser(User user) { - return newQuery().from(settings).where(settings.user.eq(user)).uniqueResult(settings); + return query().selectFrom(settings).where(settings.user.eq(user)).fetchFirst(); } } diff --git a/src/main/java/com/commafeed/backend/favicon/AbstractFaviconFetcher.java b/src/main/java/com/commafeed/backend/favicon/AbstractFaviconFetcher.java index bd10f197..5d6b0dc0 100644 --- a/src/main/java/com/commafeed/backend/favicon/AbstractFaviconFetcher.java +++ b/src/main/java/com/commafeed/backend/favicon/AbstractFaviconFetcher.java @@ -3,9 +3,11 @@ package com.commafeed.backend.favicon; import java.util.Arrays; import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import com.commafeed.backend.model.Feed; @@ -18,7 +20,7 @@ public abstract class AbstractFaviconFetcher { protected static int TIMEOUT = 4000; - public abstract byte[] fetch(Feed feed); + public abstract Favicon fetch(Feed feed); protected boolean isValidIconResponse(byte[] content, String contentType) { if (content == null) { @@ -48,4 +50,11 @@ public abstract class AbstractFaviconFetcher { return true; } + + @RequiredArgsConstructor + @Getter + public static class Favicon { + private final byte[] icon; + private final String mediaType; + } } diff --git a/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java b/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java index d6b90f8b..65d68364 100644 --- a/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java +++ b/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java @@ -6,7 +6,7 @@ import javax.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; @@ -28,9 +28,15 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { private final HttpGetter getter; @Override - public byte[] fetch(Feed feed) { - String url = feed.getLink() != null ? feed.getLink() : feed.getUrl(); + public Favicon fetch(Feed feed) { + Favicon icon = fetch(feed.getLink()); + if (icon == null) { + icon = fetch(feed.getUrl()); + } + return icon; + } + private Favicon fetch(String url) { if (url == null) { log.debug("url is null"); return null; @@ -47,7 +53,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { url = url.substring(0, firstSlash); } - byte[] icon = getIconAtRoot(url); + Favicon icon = getIconAtRoot(url); if (icon == null) { icon = getIconInPage(url); @@ -56,7 +62,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { return icon; } - private byte[] getIconAtRoot(String url) { + private Favicon getIconAtRoot(String url) { byte[] bytes = null; String contentType = null; @@ -67,23 +73,25 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { bytes = result.getContent(); contentType = result.getContentType(); } catch (Exception e) { - log.debug("Failed to retrieve iconAtRoot for url {}: ", url, e); + log.debug("Failed to retrieve iconAtRoot for url {}: ", url); + log.trace("Failed to retrieve iconAtRoot for url {}: ", url, e); } if (!isValidIconResponse(bytes, contentType)) { - bytes = null; + return null; } - return bytes; + return new Favicon(bytes, contentType); } - private byte[] getIconInPage(String url) { + private Favicon getIconInPage(String url) { Document doc = null; try { HttpResult result = getter.getBinary(url, TIMEOUT); doc = Jsoup.parse(new String(result.getContent()), url); } catch (Exception e) { - log.debug("Failed to retrieve page to find icon", e); + log.debug("Failed to retrieve page to find icon"); + log.trace("Failed to retrieve page to find icon", e); return null; } @@ -109,7 +117,8 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { bytes = result.getContent(); contentType = result.getContentType(); } catch (Exception e) { - log.debug("Failed to retrieve icon found in page {}", href, e); + log.debug("Failed to retrieve icon found in page {}", href); + log.trace("Failed to retrieve icon found in page {}", href, e); return null; } @@ -118,6 +127,6 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { return null; } - return bytes; + return new Favicon(bytes, contentType); } } diff --git a/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java b/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java new file mode 100644 index 00000000..6b1fe956 --- /dev/null +++ b/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java @@ -0,0 +1,79 @@ +package com.commafeed.backend.favicon; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; + +import com.commafeed.backend.HttpGetter; +import com.commafeed.backend.HttpGetter.HttpResult; +import com.commafeed.backend.model.Feed; + +@Slf4j +@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Singleton +public class FacebookFaviconFetcher extends AbstractFaviconFetcher { + + private final HttpGetter getter; + + @Override + public Favicon fetch(Feed feed) { + String url = feed.getUrl(); + + if (!url.toLowerCase().contains("www.facebook.com")) { + return null; + } + + String userName = extractUserName(url); + if (userName == null) { + return null; + } + + String iconUrl = String.format("https://graph.facebook.com/%s/picture?type=square&height=16", userName); + + byte[] bytes = null; + String contentType = null; + + try { + log.debug("Getting Facebook user's icon, {}", url); + + HttpResult iconResult = getter.getBinary(iconUrl, TIMEOUT); + bytes = iconResult.getContent(); + contentType = iconResult.getContentType(); + } catch (Exception e) { + log.debug("Failed to retrieve Facebook icon", e); + } + + if (!isValidIconResponse(bytes, contentType)) { + return null; + } + return new Favicon(bytes, contentType); + } + + private String extractUserName(String url) { + URI uri = null; + try { + uri = new URI(url); + } catch (URISyntaxException e) { + log.debug("could not parse url", e); + return null; + } + List params = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8.name()); + for (NameValuePair param : params) { + if ("id".equals(param.getName())) { + return param.getValue(); + } + } + return null; + } + +} diff --git a/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java b/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java index 7998d03d..5c52f896 100644 --- a/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java +++ b/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java @@ -1,66 +1,91 @@ package com.commafeed.backend.favicon; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; + import javax.inject.Inject; import javax.inject.Singleton; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.select.Elements; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.model.Feed; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.Channel; +import com.google.api.services.youtube.model.ChannelListResponse; +import com.google.api.services.youtube.model.Thumbnail; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { private final HttpGetter getter; + private final CommaFeedConfiguration config; @Override - public byte[] fetch(Feed feed) { + public Favicon fetch(Feed feed) { String url = feed.getUrl(); - if (!url.toLowerCase().contains("://gdata.youtube.com/")) { + if (!url.toLowerCase().contains("youtube.com/feeds/videos.xml")) { return null; } - String userName = extractUserName(url); - if (userName == null) { + String googleAuthKey = config.getApplicationSettings().getGoogleAuthKey(); + if (googleAuthKey == null) { + log.debug("no google auth key configured"); return null; } - String profileUrl = "https://gdata.youtube.com/feeds/users/" + userName; - byte[] bytes = null; String contentType = null; - try { - log.debug("Getting YouTube user's icon, {}", url); - - // initial get to translate username to obscure user thumbnail URL - HttpResult profileResult = getter.getBinary(profileUrl, TIMEOUT); - Document doc = Jsoup.parse(new String(profileResult.getContent()), profileUrl); - - Elements thumbnails = doc.select("media|thumbnail"); - if (thumbnails.isEmpty()) { + List params = URLEncodedUtils.parse(url.substring(url.indexOf("?") + 1), StandardCharsets.UTF_8); + Optional userId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("user")).findFirst(); + Optional channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id")).findFirst(); + if (!userId.isPresent() && !channelId.isPresent()) { return null; } - String thumbnailUrl = thumbnails.get(0).attr("abs:url"); + YouTube youtube = new YouTube.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), + new HttpRequestInitializer() { + @Override + public void initialize(HttpRequest request) throws IOException { + } + }).setApplicationName("CommaFeed").build(); - int thumbnailStart = thumbnailUrl.indexOf("", thumbnailStart); - if (thumbnailStart != -1) { - thumbnailUrl = thumbnailUrl.substring(thumbnailStart + " fromQueryString(String keywords) { + List list = new ArrayList<>(); + if (keywords != null) { + for (String keyword : StringUtils.split(keywords)) { + boolean not = false; + if (keyword.startsWith("-") || keyword.startsWith("!")) { + not = true; + keyword = keyword.substring(1); + } + list.add(new FeedEntryKeyword(keyword, not ? Mode.EXCLUDE : Mode.INCLUDE)); + } + } + return list; + } +} diff --git a/src/main/java/com/commafeed/backend/feed/FeedFetcher.java b/src/main/java/com/commafeed/backend/feed/FeedFetcher.java index c492f7f3..8ba02971 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedFetcher.java +++ b/src/main/java/com/commafeed/backend/feed/FeedFetcher.java @@ -41,16 +41,16 @@ public class FeedFetcher { byte[] content = result.getContent(); try { - fetchedFeed = parser.parse(feedUrl, content); + fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content); } catch (FeedException e) { if (extractFeedUrlFromHtml) { String extractedUrl = extractFeedUrl(StringUtils.newStringUtf8(result.getContent()), feedUrl); - if (org.apache.commons.lang.StringUtils.isNotBlank(extractedUrl)) { + if (org.apache.commons.lang3.StringUtils.isNotBlank(extractedUrl)) { feedUrl = extractedUrl; result = getter.getBinary(extractedUrl, lastModified, eTag, timeout); content = result.getContent(); - fetchedFeed = parser.parse(feedUrl, content); + fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content); } else { throw e; } diff --git a/src/main/java/com/commafeed/backend/feed/FeedParser.java b/src/main/java/com/commafeed/backend/feed/FeedParser.java index baf87413..4b483af4 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedParser.java +++ b/src/main/java/com/commafeed/backend/feed/FeedParser.java @@ -1,9 +1,11 @@ package com.commafeed.backend.feed; import java.io.StringReader; +import java.nio.charset.Charset; import java.text.DateFormat; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -11,8 +13,7 @@ import javax.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.SystemUtils; +import org.apache.commons.lang3.StringUtils; import org.jdom2.Element; import org.jdom2.Namespace; import org.xml.sax.InputSource; @@ -20,10 +21,7 @@ import org.xml.sax.InputSource; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryContent; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; -import com.rometools.rome.feed.synd.SyndContent; import com.rometools.rome.feed.synd.SyndEnclosure; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndFeed; @@ -43,20 +41,13 @@ public class FeedParser { private static final Date START = new Date(86400000); private static final Date END = new Date(1000l * Integer.MAX_VALUE - 86400000); - private static final Function CONTENT_TO_STRING = new Function() { - @Override - public String apply(SyndContent content) { - return content.getValue(); - } - }; - public FetchedFeed parse(String feedUrl, byte[] xml) throws FeedException { FetchedFeed fetchedFeed = new FetchedFeed(); Feed feed = fetchedFeed.getFeed(); List entries = fetchedFeed.getEntries(); try { - String encoding = FeedUtils.guessEncoding(xml); + Charset encoding = FeedUtils.guessEncoding(xml); String xmlString = FeedUtils.trimInvalidXmlCharacters(new String(xml, encoding)); if (xmlString == null) { throw new FeedException("Input string is null for url " + feedUrl); @@ -86,7 +77,7 @@ public class FeedParser { } entry.setGuid(FeedUtils.truncate(guid, 2048)); entry.setUpdated(validateDate(getEntryUpdateDate(item), true)); - entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink(), feed.getUrlAfterRedirect()), 2048)); + entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink(), feedUrl), 2048)); // if link is empty but guid is used as url if (StringUtils.isBlank(entry.getUrl()) && StringUtils.startsWith(entry.getGuid(), "http")) { @@ -95,6 +86,8 @@ public class FeedParser { FeedEntryContent content = new FeedEntryContent(); content.setContent(getContent(item)); + content.setCategories(FeedUtils.truncate( + item.getCategories().stream().map(c -> c.getName()).collect(Collectors.joining(", ")), 4096)); content.setTitle(getTitle(item)); content.setAuthor(StringUtils.trimToNull(item.getAuthor())); SyndEnclosure enclosure = Iterables.getFirst(item.getEnclosures(), null); @@ -173,7 +166,7 @@ public class FeedParser { if (item.getContents().isEmpty()) { content = item.getDescription() == null ? null : item.getDescription().getValue(); } else { - content = StringUtils.join(Collections2.transform(item.getContents(), CONTENT_TO_STRING), SystemUtils.LINE_SEPARATOR); + content = item.getContents().stream().map(c -> c.getValue()).collect(Collectors.joining(System.lineSeparator())); } return StringUtils.trimToNull(content); } diff --git a/src/main/java/com/commafeed/backend/feed/FeedQueues.java b/src/main/java/com/commafeed/backend/feed/FeedQueues.java index c3c90bc8..f7b3a471 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedQueues.java +++ b/src/main/java/com/commafeed/backend/feed/FeedQueues.java @@ -1,40 +1,45 @@ package com.commafeed.backend.feed; +import java.util.ArrayList; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.hibernate.SessionFactory; import com.codahale.metrics.Gauge; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedDAO; +import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.Feed; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Queues; @Singleton public class FeedQueues { + private SessionFactory sessionFactory; private final FeedDAO feedDAO; private final CommaFeedConfiguration config; - private Queue addQueue = Queues.newConcurrentLinkedQueue(); - private Queue takeQueue = Queues.newConcurrentLinkedQueue(); - private Queue giveBackQueue = Queues.newConcurrentLinkedQueue(); + private Queue addQueue = new ConcurrentLinkedQueue<>(); + private Queue takeQueue = new ConcurrentLinkedQueue<>(); + private Queue giveBackQueue = new ConcurrentLinkedQueue<>(); private Meter refill; @Inject - public FeedQueues(FeedDAO feedDAO, CommaFeedConfiguration config, MetricRegistry metrics) { + public FeedQueues(SessionFactory sessionFactory, FeedDAO feedDAO, CommaFeedConfiguration config, MetricRegistry metrics) { + this.sessionFactory = sessionFactory; this.config = config; this.feedDAO = feedDAO; @@ -78,13 +83,7 @@ public class FeedQueues { public void add(Feed feed, boolean urgent) { int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes(); if (feed.getLastUpdated() == null || feed.getLastUpdated().before(DateUtils.addMinutes(new Date(), -1 * refreshInterval))) { - boolean alreadyQueued = false; - for (FeedRefreshContext context : addQueue) { - if (context.getFeed().getId().equals(feed.getId())) { - alreadyQueued = true; - break; - } - } + boolean alreadyQueued = addQueue.stream().anyMatch(c -> c.getFeed().getId().equals(feed.getId())); if (!alreadyQueued) { addQueue.add(new FeedRefreshContext(feed, urgent)); } @@ -97,7 +96,7 @@ public class FeedQueues { private void refill() { refill.mark(); - List contexts = Lists.newArrayList(); + List contexts = new ArrayList<>(); int batchSize = Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads()); // add feeds we got from the add() method @@ -109,7 +108,7 @@ public class FeedQueues { // add feeds that are up to refresh from the database int count = batchSize - contexts.size(); if (count > 0) { - List feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold()); + List feeds = UnitOfWork.call(sessionFactory, () -> feedDAO.findNextUpdatable(count, getLastLoginThreshold())); for (Feed feed : feeds) { contexts.add(new FeedRefreshContext(feed, false)); } @@ -117,7 +116,7 @@ public class FeedQueues { // set the disabledDate as we use it in feedDAO to decide what to refresh next. We also use a map to remove // duplicates. - Map map = Maps.newLinkedHashMap(); + Map map = new LinkedHashMap<>(); for (FeedRefreshContext context : contexts) { Feed feed = context.getFeed(); feed.setDisabledUntil(DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes())); @@ -135,11 +134,8 @@ public class FeedQueues { } // update all feeds in the database - List feeds = Lists.newArrayList(); - for (FeedRefreshContext context : map.values()) { - feeds.add(context.getFeed()); - } - feedDAO.merge(feeds); + List feeds = map.values().stream().map(c -> c.getFeed()).collect(Collectors.toList()); + UnitOfWork.run(sessionFactory, () -> feedDAO.saveOrUpdate(feeds)); } /** @@ -154,7 +150,7 @@ public class FeedQueues { } private Date getLastLoginThreshold() { - if (config.getApplicationSettings().isHeavyLoad()) { + if (config.getApplicationSettings().getHeavyLoad()) { return DateUtils.addDays(new Date(), -30); } else { return null; diff --git a/src/main/java/com/commafeed/backend/feed/FeedRefreshContext.java b/src/main/java/com/commafeed/backend/feed/FeedRefreshContext.java index f2769b64..8408301d 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedRefreshContext.java +++ b/src/main/java/com/commafeed/backend/feed/FeedRefreshContext.java @@ -2,9 +2,14 @@ package com.commafeed.backend.feed; import java.util.List; +import lombok.Getter; +import lombok.Setter; + import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; +@Getter +@Setter public class FeedRefreshContext { private Feed feed; private List entries; @@ -14,29 +19,4 @@ public class FeedRefreshContext { this.feed = feed; this.urgent = isUrgent; } - - public Feed getFeed() { - return feed; - } - - public void setFeed(Feed feed) { - this.feed = feed; - } - - public boolean isUrgent() { - return urgent; - } - - public void setUrgent(boolean urgent) { - this.urgent = urgent; - } - - public List getEntries() { - return entries; - } - - public void setEntries(List entries) { - this.entries = entries; - } - } diff --git a/src/main/java/com/commafeed/backend/feed/FeedRefreshExecutor.java b/src/main/java/com/commafeed/backend/feed/FeedRefreshExecutor.java index b1f7f597..fa12c0af 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedRefreshExecutor.java +++ b/src/main/java/com/commafeed/backend/feed/FeedRefreshExecutor.java @@ -5,11 +5,11 @@ import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import lombok.extern.slf4j.Slf4j; - import com.codahale.metrics.Gauge; import com.codahale.metrics.MetricRegistry; +import lombok.extern.slf4j.Slf4j; + /** * Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using * {@link Task} instead of {@link Runnable} @@ -37,7 +37,14 @@ public class FeedRefreshExecutor { return offerLast(r); } } - }); + }) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + if (t != null) { + log.error("thread from pool {} threw a runtime exception", poolName, t); + } + } + }; pool.setRejectedExecutionHandler(new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { diff --git a/src/main/java/com/commafeed/backend/feed/FeedRefreshTaskGiver.java b/src/main/java/com/commafeed/backend/feed/FeedRefreshTaskGiver.java index 96e264da..f6235886 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedRefreshTaskGiver.java +++ b/src/main/java/com/commafeed/backend/feed/FeedRefreshTaskGiver.java @@ -10,13 +10,10 @@ import javax.inject.Singleton; import lombok.extern.slf4j.Slf4j; -import org.hibernate.SessionFactory; - import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedDAO; -import com.commafeed.backend.dao.UnitOfWork; /** * Infinite loop fetching feeds from @FeedQueues and queuing them to the {@link FeedRefreshWorker} pool. @@ -26,7 +23,6 @@ import com.commafeed.backend.dao.UnitOfWork; @Singleton public class FeedRefreshTaskGiver implements Managed { - private final SessionFactory sessionFactory; private final FeedQueues queues; private final FeedRefreshWorker worker; @@ -36,9 +32,8 @@ public class FeedRefreshTaskGiver implements Managed { private Meter threadWaited; @Inject - public FeedRefreshTaskGiver(SessionFactory sessionFactory, FeedQueues queues, FeedDAO feedDAO, FeedRefreshWorker worker, - CommaFeedConfiguration config, MetricRegistry metrics) { - this.sessionFactory = sessionFactory; + public FeedRefreshTaskGiver(FeedQueues queues, FeedDAO feedDAO, FeedRefreshWorker worker, CommaFeedConfiguration config, + MetricRegistry metrics) { this.queues = queues; this.worker = worker; @@ -68,12 +63,7 @@ public class FeedRefreshTaskGiver implements Managed { public void run() { while (!executor.isShutdown()) { try { - FeedRefreshContext context = new UnitOfWork(sessionFactory) { - @Override - protected FeedRefreshContext runInSession() throws Exception { - return queues.take(); - } - }.run(); + FeedRefreshContext context = queues.take(); if (context != null) { feedRefreshed.mark(); worker.updateFeed(context); diff --git a/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java b/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java index e526a0a1..08765228 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java +++ b/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java @@ -1,23 +1,21 @@ package com.commafeed.backend.feed; -import io.dropwizard.lifecycle.Managed; - +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; -import lombok.extern.slf4j.Slf4j; - import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; import org.hibernate.SessionFactory; import com.codahale.metrics.Meter; @@ -35,9 +33,11 @@ import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedUpdateService; import com.commafeed.backend.service.PubSubService; -import com.google.common.collect.Lists; import com.google.common.util.concurrent.Striped; +import io.dropwizard.lifecycle.Managed; +import lombok.extern.slf4j.Slf4j; + @Slf4j @Singleton public class FeedRefreshUpdater implements Managed { @@ -112,7 +112,7 @@ public class FeedRefreshUpdater implements Managed { feed.setMessage("Feed has no entries"); } else { List lastEntries = cache.getLastEntries(feed); - List currentEntries = Lists.newArrayList(); + List currentEntries = new ArrayList<>(); List subscriptions = null; for (FeedEntry entry : entries) { @@ -120,12 +120,7 @@ public class FeedRefreshUpdater implements Managed { if (!lastEntries.contains(cacheKey)) { log.debug("cache miss for {}", entry.getUrl()); if (subscriptions == null) { - subscriptions = new UnitOfWork>(sessionFactory) { - @Override - protected List runInSession() throws Exception { - return feedSubscriptionDAO.findByFeed(feed); - } - }.run(); + subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed)); } ok &= addEntry(feed, entry, subscriptions); entryCacheMiss.mark(); @@ -143,16 +138,13 @@ public class FeedRefreshUpdater implements Managed { } if (CollectionUtils.isNotEmpty(subscriptions)) { - List users = Lists.newArrayList(); - for (FeedSubscription sub : subscriptions) { - users.add(sub.getUser()); - } + List users = subscriptions.stream().map(s -> s.getUser()).collect(Collectors.toList()); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUserRootCategory(users.toArray(new User[0])); } } - if (config.getApplicationSettings().isPubsubhubbub()) { + if (config.getApplicationSettings().getPubsubhubbub()) { handlePubSub(feed); } if (!ok) { @@ -190,12 +182,7 @@ public class FeedRefreshUpdater implements Managed { locked1 = lock1.tryLock(1, TimeUnit.MINUTES); locked2 = lock2.tryLock(1, TimeUnit.MINUTES); if (locked1 && locked2) { - boolean inserted = new UnitOfWork(sessionFactory) { - @Override - protected Boolean runInSession() throws Exception { - return feedUpdateService.addEntry(feed, entry); - } - }.run(); + boolean inserted = UnitOfWork.call(sessionFactory, () -> feedUpdateService.addEntry(feed, entry, subscriptions)); if (inserted) { entryInserted.mark(); } diff --git a/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java b/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java index 4192d3f1..e044aa1a 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java +++ b/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java @@ -4,6 +4,8 @@ import io.dropwizard.lifecycle.Managed; import java.util.Date; import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -11,8 +13,8 @@ import javax.inject.Singleton; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; @@ -20,7 +22,6 @@ import com.commafeed.backend.HttpGetter.NotModifiedException; import com.commafeed.backend.feed.FeedRefreshExecutor.Task; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; -import com.google.common.base.Optional; /** * Calls {@link FeedFetcher} and handles its outcome @@ -84,13 +85,18 @@ public class FeedRefreshWorker implements Managed { int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes(); Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval); try { - String url = Optional.fromNullable(feed.getUrlAfterRedirect()).or(feed.getUrl()); + String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl()); FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(), feed.getLastPublishedDate(), feed.getLastContentHash()); // stops here if NotModifiedException or any other exception is thrown List entries = fetchedFeed.getEntries(); - if (config.getApplicationSettings().isHeavyLoad()) { + Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity(); + if (maxFeedCapacity > 0) { + entries = entries.stream().limit(maxFeedCapacity).collect(Collectors.toList()); + } + + if (config.getApplicationSettings().getHeavyLoad()) { disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed() .getAverageEntryInterval(), disabledUntil); } @@ -118,7 +124,7 @@ public class FeedRefreshWorker implements Managed { } catch (NotModifiedException e) { log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage()); - if (config.getApplicationSettings().isHeavyLoad()) { + if (config.getApplicationSettings().getHeavyLoad()) { disabledUntil = FeedUtils.buildDisabledUntil(feed.getLastEntryDate(), feed.getAverageEntryInterval(), disabledUntil); } feed.setErrorCount(0); diff --git a/src/main/java/com/commafeed/backend/feed/FeedUtils.java b/src/main/java/com/commafeed/backend/feed/FeedUtils.java index c722a2b9..f6513ecf 100644 --- a/src/main/java/com/commafeed/backend/feed/FeedUtils.java +++ b/src/main/java/com/commafeed/backend/feed/FeedUtils.java @@ -3,19 +3,20 @@ package com.commafeed.backend.feed; import java.io.StringReader; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; - -import lombok.extern.slf4j.Slf4j; +import java.util.stream.Collectors; import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.math3.stat.descriptive.SummaryStatistics; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -25,17 +26,19 @@ import org.jsoup.nodes.Entities.EscapeMode; import org.jsoup.safety.Cleaner; import org.jsoup.safety.Whitelist; import org.jsoup.select.Elements; -import org.mozilla.universalchardet.UniversalDetector; import org.w3c.css.sac.InputSource; import org.w3c.dom.css.CSSStyleDeclaration; +import com.commafeed.backend.feed.FeedEntryKeyword.Mode; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.frontend.model.Entry; -import com.google.common.collect.Lists; +import com.ibm.icu.text.CharsetDetector; +import com.ibm.icu.text.CharsetMatch; import com.steadystate.css.parser.CSSOMParser; import edu.uci.ics.crawler4j.url.URLCanonicalizer; +import lombok.extern.slf4j.Slf4j; /** * Utility methods related to feed handling @@ -97,14 +100,14 @@ public class FeedUtils { * feed * */ - public static String guessEncoding(byte[] bytes) { + public static Charset guessEncoding(byte[] bytes) { String extracted = extractDeclaredEncoding(bytes); if (StringUtils.startsWithIgnoreCase(extracted, "iso-8859-")) { if (StringUtils.endsWith(extracted, "1") == false) { - return extracted; + return Charset.forName(extracted); } } else if (StringUtils.startsWithIgnoreCase(extracted, "windows-")) { - return extracted; + return Charset.forName(extracted); } return detectEncoding(bytes); } @@ -112,27 +115,23 @@ public class FeedUtils { /** * Detect encoding by analyzing characters in the array */ - public static String detectEncoding(byte[] bytes) { - String DEFAULT_ENCODING = "UTF-8"; - UniversalDetector detector = new UniversalDetector(null); - detector.handleData(bytes, 0, bytes.length); - detector.dataEnd(); - String encoding = detector.getDetectedCharset(); - detector.reset(); - if (encoding == null) { - encoding = DEFAULT_ENCODING; - } else if (encoding.equalsIgnoreCase("ISO-8859-1")) { + public static Charset detectEncoding(byte[] bytes) { + String encoding = "UTF-8"; + + CharsetDetector detector = new CharsetDetector(); + detector.setText(bytes); + CharsetMatch match = detector.detect(); + if (match != null) { + encoding = match.getName(); + } + if (encoding.equalsIgnoreCase("ISO-8859-1")) { encoding = "windows-1252"; } - return encoding; + return Charset.forName(encoding); } public static String replaceHtmlEntitiesWithNumericEntities(String source) { - String result = source; - for (String entity : HtmlEntities.NUMERIC_MAPPING.keySet()) { - result = StringUtils.replace(result, entity, HtmlEntities.NUMERIC_MAPPING.get(entity)); - } - return result; + return StringUtils.replaceEach(source, HtmlEntities.HTML_ENTITIES, HtmlEntities.NUMERIC_ENTITIES); } /** @@ -182,7 +181,7 @@ public class FeedUtils { return null; } - String pi = new String(ArrayUtils.subarray(bytes, 0, index + 1)); + String pi = new String(ArrayUtils.subarray(bytes, 0, index + 1)).replace('\'', '"'); index = StringUtils.indexOf(pi, "encoding=\""); if (index == -1) { return null; @@ -227,7 +226,7 @@ public class FeedUtils { String rule = ""; CSSOMParser parser = new CSSOMParser(); try { - List rules = Lists.newArrayList(); + List rules = new ArrayList<>(); CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig))); for (int i = 0; i < decl.getLength(); i++) { @@ -252,7 +251,7 @@ public class FeedUtils { String rule = ""; CSSOMParser parser = new CSSOMParser(); try { - List rules = Lists.newArrayList(); + List rules = new ArrayList<>(); CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig))); for (int i = 0; i < decl.getLength(); i++) { @@ -386,13 +385,7 @@ public class FeedUtils { } public static List getSortedTimestamps(List entries) { - List timestamps = Lists.newArrayList(); - for (FeedEntry entry : entries) { - timestamps.add(entry.getUpdated().getTime()); - } - Collections.sort(timestamps); - Collections.reverse(timestamps); - return timestamps; + return entries.stream().map(t -> t.getUpdated().getTime()).sorted(Collections.reverseOrder()).collect(Collectors.toList()); } public static String removeTrailingSlash(String url) { @@ -436,22 +429,15 @@ public class FeedUtils { } public static boolean isRelative(final String url) { - // the regex means "doesn't start with 'scheme://'" - if ((url != null) && (url.startsWith("/") == false) && (!url.matches("^\\w+\\:\\/\\/.*")) && !(url.startsWith("#"))) { - return true; - } else { - return false; - } + // the regex means "start with 'scheme://'" + return url.startsWith("/") || url.startsWith("#") || !url.matches("^\\w+\\:\\/\\/.*"); } public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) { return removeTrailingSlash(publicUrl) + "/rest/feed/favicon/" + subscription.getId(); } - public static String proxyImages(String content, String publicUrl, boolean proxyImages) { - if (!proxyImages) { - return content; - } + public static String proxyImages(String content, String publicUrl) { if (StringUtils.isBlank(content)) { return content; } @@ -461,7 +447,7 @@ public class FeedUtils { for (Element element : elements) { String href = element.attr("src"); if (href != null) { - String proxy = removeTrailingSlash(publicUrl) + "/rest/server/proxy?u=" + imageProxyEncoder(href); + String proxy = proxyImage(href, publicUrl); element.attr("src", proxy); } } @@ -469,6 +455,13 @@ public class FeedUtils { return doc.body().html(); } + public static String proxyImage(String url, String publicUrl) { + if (StringUtils.isBlank(url)) { + return url; + } + return removeTrailingSlash(publicUrl) + "/rest/server/proxy?u=" + imageProxyEncoder(url); + } + public static String rot13(String msg) { StringBuilder message = new StringBuilder(); @@ -495,19 +488,20 @@ public class FeedUtils { return rot13(new String(Base64.decodeBase64(code))); } - public static void removeUnwantedFromSearch(List entries, String keywords) { - if (StringUtils.isBlank(keywords)) { - return; - } - + public static void removeUnwantedFromSearch(List entries, List keywords) { Iterator it = entries.iterator(); while (it.hasNext()) { Entry entry = it.next(); boolean keep = true; - for (String keyword : keywords.split(" ")) { + for (FeedEntryKeyword keyword : keywords) { String title = Jsoup.parse(entry.getTitle()).text(); String content = Jsoup.parse(entry.getContent()).text(); - if (!StringUtils.containsIgnoreCase(content, keyword) && !StringUtils.containsIgnoreCase(title, keyword)) { + boolean condition = !StringUtils.containsIgnoreCase(content, keyword.getKeyword()) + && !StringUtils.containsIgnoreCase(title, keyword.getKeyword()); + if (keyword.getMode() == Mode.EXCLUDE) { + condition = !condition; + } + if (condition) { keep = false; break; } diff --git a/src/main/java/com/commafeed/backend/feed/FetchedFeed.java b/src/main/java/com/commafeed/backend/feed/FetchedFeed.java index e538b686..e19928e0 100644 --- a/src/main/java/com/commafeed/backend/feed/FetchedFeed.java +++ b/src/main/java/com/commafeed/backend/feed/FetchedFeed.java @@ -1,58 +1,23 @@ package com.commafeed.backend.feed; +import java.util.ArrayList; import java.util.List; +import lombok.Getter; +import lombok.Setter; + import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; -import com.google.common.collect.Lists; +@Getter +@Setter public class FetchedFeed { private Feed feed = new Feed(); - private List entries = Lists.newArrayList(); + private List entries = new ArrayList<>(); private String title; private String urlAfterRedirect; private long fetchDuration; - public Feed getFeed() { - return feed; - } - - public void setFeed(Feed feed) { - this.feed = feed; - } - - public List getEntries() { - return entries; - } - - public void setEntries(List entries) { - this.entries = entries; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public long getFetchDuration() { - return fetchDuration; - } - - public void setFetchDuration(long fetchDuration) { - this.fetchDuration = fetchDuration; - } - - public String getUrlAfterRedirect() { - return urlAfterRedirect; - } - - public void setUrlAfterRedirect(String urlAfterRedirect) { - this.urlAfterRedirect = urlAfterRedirect; - } - } diff --git a/src/main/java/com/commafeed/backend/feed/HtmlEntities.java b/src/main/java/com/commafeed/backend/feed/HtmlEntities.java index 77d592ca..3e927c67 100644 --- a/src/main/java/com/commafeed/backend/feed/HtmlEntities.java +++ b/src/main/java/com/commafeed/backend/feed/HtmlEntities.java @@ -1,15 +1,14 @@ package com.commafeed.backend.feed; -import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; -import com.google.common.collect.Maps; - public class HtmlEntities { - public static final Map NUMERIC_MAPPING = Collections.unmodifiableMap(loadMap()); + public static final String[] HTML_ENTITIES; + public static final String[] NUMERIC_ENTITIES; - private static synchronized Map loadMap() { - Map map = Maps.newLinkedHashMap(); + static { + Map map = new LinkedHashMap<>(); map.put("Á", "Á"); map.put("á", "á"); map.put("Â", "Â"); @@ -261,6 +260,7 @@ public class HtmlEntities { map.put("‍", "‍"); map.put("‌", "‌"); - return map; + HTML_ENTITIES = map.keySet().toArray(new String[map.size()]); + NUMERIC_ENTITIES = map.values().toArray(new String[map.size()]); } } diff --git a/src/main/java/com/commafeed/backend/model/Feed.java b/src/main/java/com/commafeed/backend/model/Feed.java index 8c15a670..9ac4e736 100644 --- a/src/main/java/com/commafeed/backend/model/Feed.java +++ b/src/main/java/com/commafeed/backend/model/Feed.java @@ -1,12 +1,9 @@ package com.commafeed.backend.model; import java.util.Date; -import java.util.Set; -import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; @@ -103,12 +100,6 @@ public class Feed extends AbstractModel { @Column(length = 40) private String lastContentHash; - @OneToMany(mappedBy = "feed", cascade = CascadeType.REMOVE) - private Set entries; - - @OneToMany(mappedBy = "feed") - private Set subscriptions; - /** * detected hub for pubsubhubbub */ diff --git a/src/main/java/com/commafeed/backend/model/FeedEntryContent.java b/src/main/java/com/commafeed/backend/model/FeedEntryContent.java index a1398b7d..7b036d20 100644 --- a/src/main/java/com/commafeed/backend/model/FeedEntryContent.java +++ b/src/main/java/com/commafeed/backend/model/FeedEntryContent.java @@ -11,6 +11,8 @@ import javax.persistence.Table; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.Type; + @Entity @Table(name = "FEEDENTRYCONTENTS") @SuppressWarnings("serial") @@ -26,6 +28,7 @@ public class FeedEntryContent extends AbstractModel { @Lob @Column(length = Integer.MAX_VALUE) + @Type(type = "org.hibernate.type.StringClobType") private String content; @Column(length = 40) @@ -40,6 +43,9 @@ public class FeedEntryContent extends AbstractModel { @Column(length = 255) private String enclosureType; + @Column(length = 4096) + private String categories; + @OneToMany(mappedBy = "content") private Set entries; diff --git a/src/main/java/com/commafeed/backend/model/FeedEntryStatus.java b/src/main/java/com/commafeed/backend/model/FeedEntryStatus.java index 74cdb1c1..a891d4a1 100644 --- a/src/main/java/com/commafeed/backend/model/FeedEntryStatus.java +++ b/src/main/java/com/commafeed/backend/model/FeedEntryStatus.java @@ -1,5 +1,6 @@ package com.commafeed.backend.model; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -16,8 +17,6 @@ import javax.persistence.Transient; import lombok.Getter; import lombok.Setter; -import com.google.common.collect.Lists; - @Entity @Table(name = "FEEDENTRYSTATUSES") @SuppressWarnings("serial") @@ -41,7 +40,7 @@ public class FeedEntryStatus extends AbstractModel { private boolean markable; @Transient - private List tags = Lists.newArrayList(); + private List tags = new ArrayList<>(); /** * Denormalization starts here diff --git a/src/main/java/com/commafeed/backend/model/FeedSubscription.java b/src/main/java/com/commafeed/backend/model/FeedSubscription.java index 363665eb..92a08cec 100644 --- a/src/main/java/com/commafeed/backend/model/FeedSubscription.java +++ b/src/main/java/com/commafeed/backend/model/FeedSubscription.java @@ -40,4 +40,7 @@ public class FeedSubscription extends AbstractModel { private Integer position; + @Column(length = 4096) + private String filter; + } diff --git a/src/main/java/com/commafeed/backend/model/User.java b/src/main/java/com/commafeed/backend/model/User.java index ddd3fab5..93963a3d 100644 --- a/src/main/java/com/commafeed/backend/model/User.java +++ b/src/main/java/com/commafeed/backend/model/User.java @@ -1,13 +1,9 @@ package com.commafeed.backend.model; import java.util.Date; -import java.util.Set; -import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; @@ -15,11 +11,7 @@ import javax.persistence.TemporalType; import lombok.Getter; import lombok.Setter; -import org.apache.commons.lang.time.DateUtils; -import org.hibernate.annotations.Cascade; - -import com.commafeed.backend.model.UserRole.Role; -import com.google.common.collect.Sets; +import org.apache.commons.lang3.time.DateUtils; @Entity @Table(name = "USERS") @@ -58,27 +50,10 @@ public class User extends AbstractModel { @Temporal(TemporalType.TIMESTAMP) private Date recoverPasswordTokenDate; - @OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, CascadeType.REMOVE }) - @Cascade({ org.hibernate.annotations.CascadeType.PERSIST, org.hibernate.annotations.CascadeType.SAVE_UPDATE, - org.hibernate.annotations.CascadeType.REMOVE }) - private Set roles = Sets.newHashSet(); - - @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) - private Set subscriptions; - @Column(name = "last_full_refresh") @Temporal(TemporalType.TIMESTAMP) private Date lastFullRefresh; - public boolean hasRole(Role role) { - for (UserRole userRole : getRoles()) { - if (userRole.getRole() == role) { - return true; - } - } - return false; - } - public boolean shouldRefreshFeedsAt(Date when) { return (lastFullRefresh == null || lastFullRefreshMoreThan30MinutesBefore(when)); } diff --git a/src/main/java/com/commafeed/backend/model/UserSettings.java b/src/main/java/com/commafeed/backend/model/UserSettings.java index 70ee4418..4928ac2b 100644 --- a/src/main/java/com/commafeed/backend/model/UserSettings.java +++ b/src/main/java/com/commafeed/backend/model/UserSettings.java @@ -13,6 +13,8 @@ import javax.persistence.Table; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.Type; + @Entity @Table(name = "USERSETTINGS") @SuppressWarnings("serial") @@ -59,6 +61,7 @@ public class UserSettings extends AbstractModel { @Lob @Column(length = Integer.MAX_VALUE) + @Type(type = "org.hibernate.type.StringClobType") private String customCss; @Column(name = "scroll_speed") diff --git a/src/main/java/com/commafeed/backend/opml/OPMLExporter.java b/src/main/java/com/commafeed/backend/opml/OPMLExporter.java index 49e7011f..8dc733e5 100644 --- a/src/main/java/com/commafeed/backend/opml/OPMLExporter.java +++ b/src/main/java/com/commafeed/backend/opml/OPMLExporter.java @@ -1,7 +1,9 @@ package com.commafeed.backend.opml; +import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -13,6 +15,7 @@ import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; +import com.google.common.base.MoreObjects; import com.rometools.opml.feed.opml.Attribute; import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Outline; @@ -31,39 +34,40 @@ public class OPMLExporter { opml.setCreated(new Date()); List categories = feedCategoryDAO.findAll(user); + Collections.sort(categories, + (e1, e2) -> MoreObjects.firstNonNull(e1.getPosition(), 0) - MoreObjects.firstNonNull(e2.getPosition(), 0)); + List subscriptions = feedSubscriptionDAO.findAll(user); + Collections.sort(subscriptions, + (e1, e2) -> MoreObjects.firstNonNull(e1.getPosition(), 0) - MoreObjects.firstNonNull(e2.getPosition(), 0)); // export root categories - for (FeedCategory cat : categories) { - if (cat.getParent() == null) { - opml.getOutlines().add(buildCategoryOutline(cat, subscriptions)); - } + for (FeedCategory cat : categories.stream().filter(c -> c.getParent() == null).collect(Collectors.toList())) { + opml.getOutlines().add(buildCategoryOutline(cat, categories, subscriptions)); } // export root subscriptions - for (FeedSubscription sub : subscriptions) { - if (sub.getCategory() == null) { - opml.getOutlines().add(buildSubscriptionOutline(sub)); - } + for (FeedSubscription sub : subscriptions.stream().filter(s -> s.getCategory() == null).collect(Collectors.toList())) { + opml.getOutlines().add(buildSubscriptionOutline(sub)); } return opml; } - private Outline buildCategoryOutline(FeedCategory cat, List subscriptions) { + private Outline buildCategoryOutline(FeedCategory cat, List categories, List subscriptions) { Outline outline = new Outline(); outline.setText(cat.getName()); outline.setTitle(cat.getName()); - for (FeedCategory child : cat.getChildren()) { - outline.getChildren().add(buildCategoryOutline(child, subscriptions)); + for (FeedCategory child : categories.stream().filter(c -> c.getParent() != null && c.getParent().getId().equals(cat.getId())) + .collect(Collectors.toList())) { + outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions)); } - for (FeedSubscription sub : subscriptions) { - if (sub.getCategory() != null && sub.getCategory().getId().equals(cat.getId())) { - outline.getChildren().add(buildSubscriptionOutline(sub)); - } + for (FeedSubscription sub : subscriptions.stream() + .filter(s -> s.getCategory() != null && s.getCategory().getId().equals(cat.getId())).collect(Collectors.toList())) { + outline.getChildren().add(buildSubscriptionOutline(sub)); } return outline; } diff --git a/src/main/java/com/commafeed/backend/opml/OPMLImporter.java b/src/main/java/com/commafeed/backend/opml/OPMLImporter.java index 6ff2ddff..9e910f3f 100644 --- a/src/main/java/com/commafeed/backend/opml/OPMLImporter.java +++ b/src/main/java/com/commafeed/backend/opml/OPMLImporter.java @@ -10,7 +10,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; @@ -38,8 +38,8 @@ public class OPMLImporter { try { Opml feed = (Opml) input.build(new StringReader(xml)); List outlines = feed.getOutlines(); - for (Outline outline : outlines) { - handleOutline(user, outline, null); + for (int i = 0; i < outlines.size(); i++) { + handleOutline(user, outlines.get(i), null, i); } } catch (Exception e) { log.error(e.getMessage(), e); @@ -47,7 +47,7 @@ public class OPMLImporter { } - private void handleOutline(User user, Outline outline, FeedCategory parent) { + private void handleOutline(User user, Outline outline, FeedCategory parent, int position) { List children = outline.getChildren(); if (CollectionUtils.isNotEmpty(children)) { String name = FeedUtils.truncate(outline.getText(), 128); @@ -64,11 +64,12 @@ public class OPMLImporter { category.setName(name); category.setParent(parent); category.setUser(user); + category.setPosition(position); feedCategoryDAO.saveOrUpdate(category); } - for (Outline child : children) { - handleOutline(user, child, category); + for (int i = 0; i < children.size(); i++) { + handleOutline(user, children.get(i), category, i); } } else { String name = FeedUtils.truncate(outline.getText(), 128); @@ -80,7 +81,7 @@ public class OPMLImporter { } // make sure we continue with the import process even if a feed failed try { - feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent); + feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position); } catch (FeedSubscriptionException e) { throw e; } catch (Exception e) { diff --git a/src/main/java/com/commafeed/backend/rome/OPML11Parser.java b/src/main/java/com/commafeed/backend/rome/OPML11Parser.java index 995baa1b..8a1eceab 100644 --- a/src/main/java/com/commafeed/backend/rome/OPML11Parser.java +++ b/src/main/java/com/commafeed/backend/rome/OPML11Parser.java @@ -1,9 +1,13 @@ package com.commafeed.backend.rome; +import java.util.Locale; + import org.jdom2.Document; import org.jdom2.Element; import com.rometools.opml.io.impl.OPML10Parser; +import com.rometools.rome.feed.WireFeed; +import com.rometools.rome.io.FeedException; /** * Support for OPML 1.1 parsing @@ -19,12 +23,17 @@ public class OPML11Parser extends OPML10Parser { public boolean isMyType(Document document) { Element e = document.getRootElement(); - if (e.getName().equals("opml") && (e.getChild("head") == null || e.getChild("head").getChild("docs") == null) - && (e.getAttributeValue("version") == null || e.getAttributeValue("version").equals("1.1"))) { + if (e.getName().equals("opml")) { return true; } return false; - }; + } + + @Override + public WireFeed parse(Document document, boolean validate, Locale locale) throws IllegalArgumentException, FeedException { + document.getRootElement().getChildren().add(new Element("head")); + return super.parse(document, validate, locale); + } } diff --git a/src/main/java/com/commafeed/backend/service/DatabaseCleaningService.java b/src/main/java/com/commafeed/backend/service/DatabaseCleaningService.java index 30418bbe..c79fbef0 100644 --- a/src/main/java/com/commafeed/backend/service/DatabaseCleaningService.java +++ b/src/main/java/com/commafeed/backend/service/DatabaseCleaningService.java @@ -1,32 +1,30 @@ package com.commafeed.backend.service; -import java.util.Calendar; import java.util.Date; import java.util.List; -import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Singleton; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import org.hibernate.SessionFactory; import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.dao.FeedEntryDAO; +import com.commafeed.backend.dao.FeedEntryDAO.FeedCapacity; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.Feed; -import com.commafeed.backend.model.FeedEntryStatus; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * Contains utility methods for cleaning the database * */ @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class DatabaseCleaningService { @@ -42,14 +40,18 @@ public class DatabaseCleaningService { log.info("cleaning feeds without subscriptions"); long total = 0; int deleted = 0; + long entriesTotal = 0; do { - deleted = new UnitOfWork(sessionFactory) { - @Override - protected Integer runInSession() throws Exception { - List feeds = feedDAO.findWithoutSubscriptions(1); - return feedDAO.delete(feeds); - }; - }.run(); + List feeds = UnitOfWork.call(sessionFactory, () -> feedDAO.findWithoutSubscriptions(1)); + for (Feed feed : feeds) { + int entriesDeleted = 0; + do { + entriesDeleted = UnitOfWork.call(sessionFactory, () -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE)); + entriesTotal += entriesDeleted; + log.info("removed {} entries for feeds without subscriptions", entriesTotal); + } while (entriesDeleted > 0); + } + deleted = UnitOfWork.call(sessionFactory, () -> feedDAO.delete(feeds)); total += deleted; log.info("removed {} feeds without subscriptions", total); } while (deleted != 0); @@ -62,12 +64,7 @@ public class DatabaseCleaningService { long total = 0; int deleted = 0; do { - deleted = new UnitOfWork(sessionFactory) { - @Override - protected Integer runInSession() throws Exception { - return feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE); - } - }.run(); + deleted = UnitOfWork.call(sessionFactory, () -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE)); total += deleted; log.info("removed {} contents without entries", total); } while (deleted != 0); @@ -75,23 +72,28 @@ public class DatabaseCleaningService { return total; } - public long cleanEntriesOlderThan(long value, TimeUnit unit) { - final Calendar cal = Calendar.getInstance(); - cal.add(Calendar.MINUTE, -1 * (int) unit.toMinutes(value)); - + public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) { long total = 0; - int deleted = 0; - do { - deleted = new UnitOfWork(sessionFactory) { - @Override - protected Integer runInSession() throws Exception { - return feedEntryDAO.delete(cal.getTime(), BATCH_SIZE); - } - }.run(); - total += deleted; - log.info("removed {} entries", total); - } while (deleted != 0); - log.info("cleanup done: {} entries deleted", total); + while (true) { + List feeds = UnitOfWork.call(sessionFactory, + () -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE)); + if (feeds.isEmpty()) { + break; + } + + for (final FeedCapacity feed : feeds) { + long remaining = feed.getCapacity() - maxFeedCapacity; + do { + final long rem = remaining; + int deleted = UnitOfWork.call(sessionFactory, + () -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(BATCH_SIZE, rem))); + total += deleted; + remaining -= deleted; + log.info("removed {} entries for feeds exceeding capacity", total); + } while (remaining > 0); + } + } + log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total); return total; } @@ -100,15 +102,10 @@ public class DatabaseCleaningService { long total = 0; int deleted = 0; do { - deleted = new UnitOfWork(sessionFactory) { - @Override - protected Integer runInSession() throws Exception { - List list = feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE); - return feedEntryStatusDAO.delete(list); - } - }.run(); + deleted = UnitOfWork.call(sessionFactory, + () -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE))); total += deleted; - log.info("cleaned {} old read statuses", total); + log.info("removed {} old read statuses", total); } while (deleted != 0); log.info("cleanup done: {} old read statuses deleted", total); return total; diff --git a/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java b/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java index 2a4e1011..d7bc7fdf 100644 --- a/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java +++ b/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java @@ -6,7 +6,7 @@ import javax.inject.Singleton; import lombok.RequiredArgsConstructor; import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.feed.FeedUtils; diff --git a/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java b/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java new file mode 100644 index 00000000..c576ff45 --- /dev/null +++ b/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java @@ -0,0 +1,119 @@ +package com.commafeed.backend.service; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.RequiredArgsConstructor; + +import org.apache.commons.jexl2.JexlContext; +import org.apache.commons.jexl2.JexlEngine; +import org.apache.commons.jexl2.JexlException; +import org.apache.commons.jexl2.JexlInfo; +import org.apache.commons.jexl2.MapContext; +import org.apache.commons.jexl2.Script; +import org.apache.commons.jexl2.introspection.JexlMethod; +import org.apache.commons.jexl2.introspection.JexlPropertyGet; +import org.apache.commons.jexl2.introspection.Uberspect; +import org.apache.commons.jexl2.introspection.UberspectImpl; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.LogFactory; +import org.jsoup.Jsoup; + +import com.commafeed.backend.model.FeedEntry; + +@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Singleton +public class FeedEntryFilteringService { + + private static final JexlEngine ENGINE = initEngine(); + + private static JexlEngine initEngine() { + // classloader that prevents object creation + ClassLoader cl = new ClassLoader() { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + return null; + } + }; + + // uberspect that prevents access to .class and .getClass() + Uberspect uberspect = new UberspectImpl(LogFactory.getLog(JexlEngine.class)) { + @Override + public JexlPropertyGet getPropertyGet(Object obj, Object identifier, JexlInfo info) { + if ("class".equals(identifier)) { + return null; + } + return super.getPropertyGet(obj, identifier, info); + } + + @Override + public JexlMethod getMethod(Object obj, String method, Object[] args, JexlInfo info) { + if ("getClass".equals(method)) { + return null; + } + return super.getMethod(obj, method, args, info); + } + }; + + JexlEngine engine = new JexlEngine(uberspect, null, null, null); + engine.setStrict(true); + engine.setClassLoader(cl); + return engine; + } + + private ExecutorService executor = Executors.newCachedThreadPool(); + + public boolean filterMatchesEntry(String filter, FeedEntry entry) throws FeedEntryFilterException { + if (StringUtils.isBlank(filter)) { + return true; + } + + Script script = null; + try { + script = ENGINE.createScript(filter); + } catch (JexlException e) { + throw new FeedEntryFilterException("Exception while parsing expression " + filter, e); + } + + JexlContext context = new MapContext(); + context.set("title", entry.getContent().getTitle() == null ? "" : Jsoup.parse(entry.getContent().getTitle()).text().toLowerCase()); + context.set("author", entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor().toLowerCase()); + context.set("content", entry.getContent().getContent() == null ? "" : Jsoup.parse(entry.getContent().getContent()).text() + .toLowerCase()); + context.set("url", entry.getUrl() == null ? "" : entry.getUrl().toLowerCase()); + context.set("categories", entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories().toLowerCase()); + + Callable callable = script.callable(context); + Future future = executor.submit(callable); + Object result = null; + try { + result = future.get(500, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new FeedEntryFilterException("interrupted while evaluating expression " + filter, e); + } catch (ExecutionException e) { + throw new FeedEntryFilterException("Exception while evaluating expression " + filter, e); + } catch (TimeoutException e) { + throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e); + } + try { + return (boolean) result; + } catch (ClassCastException e) { + throw new FeedEntryFilterException(e.getMessage(), e); + } + } + + @SuppressWarnings("serial") + public static class FeedEntryFilterException extends Exception { + public FeedEntryFilterException(String message, Throwable t) { + super(message, t); + } + } +} diff --git a/src/main/java/com/commafeed/backend/service/FeedEntryService.java b/src/main/java/com/commafeed/backend/service/FeedEntryService.java index a3c5fbfc..96402d9d 100644 --- a/src/main/java/com/commafeed/backend/service/FeedEntryService.java +++ b/src/main/java/com/commafeed/backend/service/FeedEntryService.java @@ -1,5 +1,6 @@ package com.commafeed.backend.service; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -12,11 +13,11 @@ import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; +import com.commafeed.backend.feed.FeedEntryKeyword; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; -import com.google.common.collect.Lists; @RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton @@ -65,9 +66,9 @@ public class FeedEntryService { feedEntryStatusDAO.saveOrUpdate(status); } - public void markSubscriptionEntries(User user, List subscriptions, Date olderThan, String keywords) { - List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null, false, - false, null); + public void markSubscriptionEntries(User user, List subscriptions, Date olderThan, List keywords) { + List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null, + false, false, null); markList(statuses, olderThan); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUserRootCategory(user); @@ -79,7 +80,7 @@ public class FeedEntryService { } private void markList(List statuses, Date olderThan) { - List list = Lists.newArrayList(); + List list = new ArrayList<>(); for (FeedEntryStatus status : statuses) { if (!status.isRead()) { Date inserted = status.getEntry().getInserted(); diff --git a/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java b/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java index f640c045..446d027b 100644 --- a/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java +++ b/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java @@ -1,7 +1,8 @@ package com.commafeed.backend.service; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -13,9 +14,6 @@ import com.commafeed.backend.dao.FeedEntryTagDAO; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.User; -import com.google.common.base.Function; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; @RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton @@ -30,29 +28,12 @@ public class FeedEntryTagService { return; } - List tags = feedEntryTagDAO.findByEntry(user, entry); - Map tagMap = Maps.uniqueIndex(tags, new Function() { - @Override - public String apply(FeedEntryTag input) { - return input.getName(); - } - }); + List existingTags = feedEntryTagDAO.findByEntry(user, entry); + Set existingTagNames = existingTags.stream().map(t -> t.getName()).collect(Collectors.toSet()); - List addList = Lists.newArrayList(); - List removeList = Lists.newArrayList(); - - for (String tagName : tagNames) { - FeedEntryTag tag = tagMap.get(tagName); - if (tag == null) { - addList.add(new FeedEntryTag(user, entry, tagName)); - } - } - - for (FeedEntryTag tag : tags) { - if (!tagNames.contains(tag.getName())) { - removeList.add(tag); - } - } + List addList = tagNames.stream().filter(name -> !existingTagNames.contains(name)) + .map(name -> new FeedEntryTag(user, entry, name)).collect(Collectors.toList()); + List removeList = existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).collect(Collectors.toList()); feedEntryTagDAO.saveOrUpdate(addList); feedEntryTagDAO.delete(removeList); diff --git a/src/main/java/com/commafeed/backend/service/FeedService.java b/src/main/java/com/commafeed/backend/service/FeedService.java index 59aaba30..2ee312af 100644 --- a/src/main/java/com/commafeed/backend/service/FeedService.java +++ b/src/main/java/com/commafeed/backend/service/FeedService.java @@ -12,6 +12,7 @@ import org.apache.commons.io.IOUtils; import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.favicon.AbstractFaviconFetcher; +import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.Feed; @@ -21,7 +22,7 @@ public class FeedService { private final FeedDAO feedDAO; private final Set faviconFetchers; - private byte[] defaultFavicon; + private Favicon defaultFavicon; @Inject public FeedService(FeedDAO feedDAO, Set faviconFetchers) { @@ -29,7 +30,7 @@ public class FeedService { this.faviconFetchers = faviconFetchers; try { - defaultFavicon = IOUtils.toByteArray(getClass().getResource("/images/default_favicon.gif")); + defaultFavicon = new Favicon(IOUtils.toByteArray(getClass().getResource("/images/default_favicon.gif")), "image/gif"); } catch (IOException e) { throw new RuntimeException("could not load default favicon", e); } @@ -49,9 +50,9 @@ public class FeedService { return feed; } - public byte[] fetchFavicon(Feed feed) { + public Favicon fetchFavicon(Feed feed) { - byte[] icon = null; + Favicon icon = null; for (AbstractFaviconFetcher faviconFetcher : faviconFetchers) { icon = faviconFetcher.fetch(feed); if (icon != null) { diff --git a/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java b/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java index dbe6b542..f14c1328 100644 --- a/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java +++ b/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java @@ -2,6 +2,7 @@ package com.commafeed.backend.service; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -9,7 +10,7 @@ import javax.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.cache.CacheService; @@ -23,7 +24,6 @@ import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.Models; import com.commafeed.backend.model.User; import com.commafeed.frontend.model.UnreadCount; -import com.google.common.collect.Maps; @Slf4j @RequiredArgsConstructor(onConstructor = @__({ @Inject })) @@ -44,7 +44,15 @@ public class FeedSubscriptionService { private final CacheService cache; private final CommaFeedConfiguration config; - public Feed subscribe(User user, String url, String title, FeedCategory category) { + public Feed subscribe(User user, String url, String title) { + return subscribe(user, url, title, null, 0); + } + + public Feed subscribe(User user, String url, String title, FeedCategory parent) { + return subscribe(user, url, title, parent, 0); + } + + public Feed subscribe(User user, String url, String title, FeedCategory category, int position) { final String pubUrl = config.getApplicationSettings().getPublicUrl(); if (StringUtils.isBlank(pubUrl)) { @@ -63,7 +71,7 @@ public class FeedSubscriptionService { sub.setUser(user); } sub.setCategory(category); - sub.setPosition(0); + sub.setPosition(position); sub.setTitle(FeedUtils.truncate(title, 128)); feedSubscriptionDAO.saveOrUpdate(sub); @@ -92,12 +100,7 @@ public class FeedSubscriptionService { } public Map getUnreadCount(User user) { - Map map = Maps.newHashMap(); - List subs = feedSubscriptionDAO.findAll(user); - for (FeedSubscription sub : subs) { - map.put(sub.getId(), getUnreadCount(user, sub)); - } - return map; + return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(s -> s.getId(), s -> getUnreadCount(user, s))); } private UnreadCount getUnreadCount(User user, FeedSubscription sub) { diff --git a/src/main/java/com/commafeed/backend/service/FeedUpdateService.java b/src/main/java/com/commafeed/backend/service/FeedUpdateService.java index e0c68e17..c96a21eb 100644 --- a/src/main/java/com/commafeed/backend/service/FeedUpdateService.java +++ b/src/main/java/com/commafeed/backend/service/FeedUpdateService.java @@ -1,30 +1,39 @@ package com.commafeed.backend.service; import java.util.Date; +import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import com.commafeed.backend.dao.FeedEntryDAO; +import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryContent; +import com.commafeed.backend.model.FeedEntryStatus; +import com.commafeed.backend.model.FeedSubscription; +import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; +@Slf4j @RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton public class FeedUpdateService { private final FeedEntryDAO feedEntryDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedEntryContentService feedEntryContentService; + private final FeedEntryFilteringService feedEntryFilteringService; /** * this is NOT thread-safe */ - public boolean addEntry(Feed feed, FeedEntry entry) { + public boolean addEntry(Feed feed, FeedEntry entry, List subscriptions) { Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed); if (existing != null) { @@ -36,8 +45,23 @@ public class FeedUpdateService { entry.setContent(content); entry.setInserted(new Date()); entry.setFeed(feed); - feedEntryDAO.saveOrUpdate(entry); + + // if filter does not match the entry, mark it as read + for (FeedSubscription sub : subscriptions) { + boolean matches = true; + try { + matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry); + } catch (FeedEntryFilterException e) { + log.error("could not evaluate filter {}", sub.getFilter(), e); + } + if (!matches) { + FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry); + status.setRead(true); + feedEntryStatusDAO.saveOrUpdate(status); + } + } + return true; } } diff --git a/src/main/java/com/commafeed/backend/service/MailService.java b/src/main/java/com/commafeed/backend/service/MailService.java index 63725079..6eaa59bd 100644 --- a/src/main/java/com/commafeed/backend/service/MailService.java +++ b/src/main/java/com/commafeed/backend/service/MailService.java @@ -1,5 +1,6 @@ package com.commafeed.backend.service; +import java.util.Optional; import java.util.Properties; import javax.inject.Inject; @@ -34,6 +35,7 @@ public class MailService { final String username = settings.getSmtpUserName(); final String password = settings.getSmtpPassword(); + final String fromAddress = Optional.ofNullable(settings.getSmtpFromAddress()).orElse(settings.getSmtpUserName()); String dest = user.getEmail(); @@ -51,7 +53,7 @@ public class MailService { }); Message message = new MimeMessage(session); - message.setFrom(new InternetAddress(username, "CommaFeed")); + message.setFrom(new InternetAddress(fromAddress, "CommaFeed")); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest)); message.setSubject("CommaFeed - " + subject); message.setContent(content, "text/html; charset=utf-8"); diff --git a/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java b/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java index 2a61b3c8..c387a271 100644 --- a/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java +++ b/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java @@ -15,7 +15,7 @@ import javax.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; // taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html @SuppressWarnings("serial") diff --git a/src/main/java/com/commafeed/backend/service/PubSubService.java b/src/main/java/com/commafeed/backend/service/PubSubService.java index 0a93240d..148ead1c 100644 --- a/src/main/java/com/commafeed/backend/service/PubSubService.java +++ b/src/main/java/com/commafeed/backend/service/PubSubService.java @@ -1,5 +1,6 @@ package com.commafeed.backend.service; +import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -10,7 +11,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHeaders; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; @@ -26,7 +27,6 @@ import com.commafeed.backend.feed.FeedQueues; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.Feed; import com.commafeed.frontend.resource.PubSubHubbubCallbackREST; -import com.google.common.collect.Lists; /** * Sends push subscription requests. Callback is handled by {@link PubSubHubbubCallbackREST} @@ -57,7 +57,7 @@ public class PubSubService { log.debug("sending new pubsub subscription to {} for {}", hub, topic); HttpPost post = new HttpPost(hub); - List nvp = Lists.newArrayList(); + List nvp = new ArrayList<>(); nvp.add(new BasicNameValuePair("hub.callback", publicUrl + "/rest/push/callback")); nvp.add(new BasicNameValuePair("hub.topic", topic)); nvp.add(new BasicNameValuePair("hub.mode", "subscribe")); diff --git a/src/main/java/com/commafeed/backend/service/StartupService.java b/src/main/java/com/commafeed/backend/service/StartupService.java index ff3e95b8..426d37d5 100644 --- a/src/main/java/com/commafeed/backend/service/StartupService.java +++ b/src/main/java/com/commafeed/backend/service/StartupService.java @@ -1,7 +1,5 @@ package com.commafeed.backend.service; -import io.dropwizard.lifecycle.Managed; - import java.sql.Connection; import java.util.Arrays; @@ -9,6 +7,18 @@ import javax.inject.Inject; import javax.inject.Singleton; import javax.sql.DataSource; +import org.hibernate.SessionFactory; +import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.internal.SessionFactoryImpl; + +import com.commafeed.CommaFeedApplication; +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.backend.dao.UnitOfWork; +import com.commafeed.backend.dao.UserDAO; +import com.commafeed.backend.model.UserRole.Role; + +import io.dropwizard.lifecycle.Managed; import liquibase.Liquibase; import liquibase.database.Database; import liquibase.database.DatabaseFactory; @@ -20,37 +30,23 @@ import liquibase.structure.DatabaseObject; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.SessionFactory; -import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl; -import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; -import org.hibernate.internal.SessionFactoryImpl; - -import com.commafeed.CommaFeedApplication; -import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; -import com.commafeed.backend.model.UserRole.Role; - @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class StartupService implements Managed { private final SessionFactory sessionFactory; private final UserDAO userDAO; private final UserService userService; + private final CommaFeedConfiguration config; @Override public void start() throws Exception { updateSchema(); - new UnitOfWork(sessionFactory) { - @Override - protected Void runInSession() throws Exception { - if (userDAO.count() == 0) { - initialData(); - } - return null; - } - }.run(); + long count = UnitOfWork.call(sessionFactory, () -> userDAO.count()); + if (count == 0) { + UnitOfWork.run(sessionFactory, () -> initialData()); + } } private void updateSchema() { @@ -95,7 +91,9 @@ public class StartupService implements Managed { try { userService.register(CommaFeedApplication.USERNAME_ADMIN, "admin", "admin@commafeed.com", Arrays.asList(Role.ADMIN, Role.USER), true); - userService.register(CommaFeedApplication.USERNAME_DEMO, "demo", "demo@commafeed.com", Arrays.asList(Role.USER), true); + if (config.getApplicationSettings().getCreateDemoAccount()) { + userService.register(CommaFeedApplication.USERNAME_DEMO, "demo", "demo@commafeed.com", Arrays.asList(Role.USER), true); + } } catch (Exception e) { log.error(e.getMessage(), e); } diff --git a/src/main/java/com/commafeed/backend/service/UserService.java b/src/main/java/com/commafeed/backend/service/UserService.java index 747de4fb..c5601a2f 100644 --- a/src/main/java/com/commafeed/backend/service/UserService.java +++ b/src/main/java/com/commafeed/backend/service/UserService.java @@ -2,6 +2,8 @@ package com.commafeed.backend.service; import java.util.Collection; import java.util.Date; +import java.util.Optional; +import java.util.Set; import java.util.UUID; import javax.inject.Inject; @@ -10,17 +12,18 @@ import javax.inject.Singleton; import lombok.RequiredArgsConstructor; import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedCategoryDAO; +import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.UserDAO; +import com.commafeed.backend.dao.UserRoleDAO; import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.internal.PostLoginActivities; -import com.google.common.base.Optional; import com.google.common.base.Preconditions; @RequiredArgsConstructor(onConstructor = @__({ @Inject })) @@ -28,12 +31,14 @@ import com.google.common.base.Preconditions; public class UserService { private final FeedCategoryDAO feedCategoryDAO; + private final FeedSubscriptionDAO feedSubscriptionDAO; private final UserDAO userDAO; + private final UserRoleDAO userRoleDAO; private final UserSettingsDAO userSettingsDAO; private final PasswordEncryptionService encryptionService; private final CommaFeedConfiguration config; - + private final PostLoginActivities postLoginActivities; /** @@ -41,7 +46,7 @@ public class UserService { */ public Optional login(String nameOrEmail, String password) { if (nameOrEmail == null || password == null) { - return Optional.absent(); + return Optional.empty(); } User user = userDAO.findByName(nameOrEmail); @@ -55,15 +60,15 @@ public class UserService { return Optional.of(user); } } - return Optional.absent(); - } + return Optional.empty(); + } /** * try to log in with given api key */ public Optional login(String apiKey) { if (apiKey == null) { - return Optional.absent(); + return Optional.empty(); } User user = userDAO.findByApiKey(apiKey); @@ -71,7 +76,7 @@ public class UserService { performPostLoginActivities(user); return Optional.of(user); } - return Optional.absent(); + return Optional.empty(); } /** @@ -92,7 +97,7 @@ public class UserService { Preconditions.checkNotNull(password); if (!forceRegistration) { - Preconditions.checkState(config.getApplicationSettings().isAllowRegistrations(), + Preconditions.checkState(config.getApplicationSettings().getAllowRegistrations(), "Registrations are closed on this CommaFeed instance"); Preconditions.checkNotNull(email); @@ -114,16 +119,18 @@ public class UserService { user.setCreated(new Date()); user.setSalt(salt); user.setPassword(encryptionService.getEncryptedPassword(password, salt)); - for (Role role : roles) { - user.getRoles().add(new UserRole(user, role)); - } userDAO.saveOrUpdate(user); + for (Role role : roles) { + userRoleDAO.saveOrUpdate(new UserRole(user, role)); + } return user; } public void unregister(User user) { feedCategoryDAO.delete(feedCategoryDAO.findAll(user)); userSettingsDAO.delete(userSettingsDAO.findByUser(user)); + userRoleDAO.delete(userRoleDAO.findAll(user)); + feedSubscriptionDAO.delete(feedSubscriptionDAO.findAll(user)); userDAO.delete(user); } @@ -131,4 +138,8 @@ public class UserService { byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt()); return DigestUtils.sha1Hex(key); } + + public Set getRoles(User user) { + return userRoleDAO.findRoles(user); + } } diff --git a/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java b/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java index 0466cb99..6bf7e071 100644 --- a/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java +++ b/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java @@ -7,7 +7,7 @@ import javax.inject.Singleton; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.time.DateUtils; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.UserDAO; @@ -17,11 +17,11 @@ import com.commafeed.backend.service.FeedSubscriptionService; @RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton public class PostLoginActivities { - + private final UserDAO userDAO; private final FeedSubscriptionService feedSubscriptionService; private final CommaFeedConfiguration config; - + public void executeFor(User user) { Date lastLogin = user.getLastLogin(); Date now = new Date(); @@ -33,13 +33,13 @@ public class PostLoginActivities { user.setLastLogin(now); saveUser = true; } - if (config.getApplicationSettings().isHeavyLoad() && user.shouldRefreshFeedsAt(now)) { + if (config.getApplicationSettings().getHeavyLoad() && user.shouldRefreshFeedsAt(now)) { feedSubscriptionService.refreshAll(user); user.setLastFullRefresh(now); saveUser = true; } if (saveUser) { - userDAO.merge(user); + userDAO.saveOrUpdate(user); } } diff --git a/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java b/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java new file mode 100644 index 00000000..9826b6d2 --- /dev/null +++ b/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java @@ -0,0 +1,43 @@ +package com.commafeed.backend.task; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.RequiredArgsConstructor; + +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.backend.service.DatabaseCleaningService; + +@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Singleton +public class OldEntriesCleanupTask extends ScheduledTask { + + private final CommaFeedConfiguration config; + private final DatabaseCleaningService cleaner; + + @Override + public void run() { + int maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity(); + if (maxFeedCapacity > 0) { + cleaner.cleanEntriesForFeedsExceedingCapacity(maxFeedCapacity); + } + } + + @Override + public long getInitialDelay() { + return 5; + } + + @Override + public long getPeriod() { + return 60; + } + + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } + +} diff --git a/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java b/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java new file mode 100644 index 00000000..def2b9e2 --- /dev/null +++ b/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java @@ -0,0 +1,38 @@ +package com.commafeed.backend.task; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.RequiredArgsConstructor; + +import com.commafeed.backend.service.DatabaseCleaningService; + +@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Singleton +public class OrphanedContentsCleanupTask extends ScheduledTask { + + private final DatabaseCleaningService cleaner; + + @Override + public void run() { + cleaner.cleanContentsWithoutEntries(); + } + + @Override + public long getInitialDelay() { + return 20; + } + + @Override + public long getPeriod() { + return 60; + } + + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } + +} diff --git a/src/main/java/com/commafeed/backend/task/OrphansCleanupTask.java b/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java similarity index 80% rename from src/main/java/com/commafeed/backend/task/OrphansCleanupTask.java rename to src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java index 6f290dcf..e8bff72d 100644 --- a/src/main/java/com/commafeed/backend/task/OrphansCleanupTask.java +++ b/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java @@ -11,19 +11,18 @@ import com.commafeed.backend.service.DatabaseCleaningService; @RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton -public class OrphansCleanupTask extends ScheduledTask { +public class OrphanedFeedsCleanupTask extends ScheduledTask { private final DatabaseCleaningService cleaner; @Override public void run() { cleaner.cleanFeedsWithoutSubscriptions(); - cleaner.cleanContentsWithoutEntries(); } @Override public long getInitialDelay() { - return 5; + return 15; } @Override diff --git a/src/main/java/com/commafeed/backend/task/ScheduledTask.java b/src/main/java/com/commafeed/backend/task/ScheduledTask.java index bed7c5b5..7976194b 100644 --- a/src/main/java/com/commafeed/backend/task/ScheduledTask.java +++ b/src/main/java/com/commafeed/backend/task/ScheduledTask.java @@ -26,7 +26,8 @@ public abstract class ScheduledTask { } } }; + log.info("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(), + getTimeUnit(), getInitialDelay(), getTimeUnit()); executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit()); } - } diff --git a/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java b/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java new file mode 100644 index 00000000..469a2e9c --- /dev/null +++ b/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java @@ -0,0 +1,99 @@ +package com.commafeed.frontend.auth; + +import java.util.Optional; +import java.util.Set; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import lombok.RequiredArgsConstructor; + +import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.StringUtil; +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; + +import com.commafeed.backend.model.User; +import com.commafeed.backend.model.UserRole.Role; +import com.commafeed.backend.service.UserService; +import com.commafeed.frontend.session.SessionHelper; + +@RequiredArgsConstructor +public class SecurityCheckFactory extends AbstractContainerRequestValueFactory { + + private static final String PREFIX = "Basic"; + + @Context + HttpServletRequest request; + + @Inject + UserService userService; + + private final Role role; + private final boolean apiKeyAllowed; + + @Override + public User provide() { + Optional user = apiKeyLogin(); + if (!user.isPresent()) { + user = basicAuthenticationLogin(); + } + if (!user.isPresent()) { + user = cookieSessionLogin(new SessionHelper(request)); + } + + if (user.isPresent()) { + Set roles = userService.getRoles(user.get()); + if (roles.contains(role)) { + return user.get(); + } else { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity("You don't have the required role to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); + } + } else { + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) + .entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); + } + } + + Optional cookieSessionLogin(SessionHelper sessionHelper) { + Optional loggedInUser = sessionHelper.getLoggedInUser(); + if (loggedInUser.isPresent()) { + userService.performPostLoginActivities(loggedInUser.get()); + } + return loggedInUser; + } + + private Optional basicAuthenticationLogin() { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header != null) { + int space = header.indexOf(' '); + if (space > 0) { + String method = header.substring(0, space); + if (PREFIX.equalsIgnoreCase(method)) { + String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1); + int i = decoded.indexOf(':'); + if (i > 0) { + String username = decoded.substring(0, i); + String password = decoded.substring(i + 1); + return userService.login(username, password); + } + } + } + } + return Optional.empty(); + } + + private Optional apiKeyLogin() { + String apiKey = request.getParameter("apiKey"); + if (apiKey != null && apiKeyAllowed) { + return userService.login(apiKey); + } + return Optional.empty(); + } + +} diff --git a/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java b/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java new file mode 100644 index 00000000..8d389c00 --- /dev/null +++ b/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java @@ -0,0 +1,64 @@ +package com.commafeed.frontend.auth; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.RequiredArgsConstructor; + +import org.glassfish.hk2.api.Factory; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; + +import com.commafeed.backend.model.User; +import com.commafeed.backend.service.UserService; + +@Singleton +public class SecurityCheckFactoryProvider extends AbstractValueFactoryProvider { + + @Inject + public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, final ServiceLocator injector) { + super(extractorProvider, injector, Parameter.Source.UNKNOWN); + } + + @Override + protected Factory createValueFactory(final Parameter parameter) { + final Class classType = parameter.getRawType(); + + SecurityCheck securityCheck = parameter.getAnnotation(SecurityCheck.class); + if (securityCheck == null) + return null; + + if (classType.isAssignableFrom(User.class)) { + return new SecurityCheckFactory(securityCheck.value(), securityCheck.apiKeyAllowed()); + } else { + return null; + } + } + + public static class SecurityCheckInjectionResolver extends ParamInjectionResolver { + public SecurityCheckInjectionResolver() { + super(SecurityCheckFactoryProvider.class); + } + } + + @RequiredArgsConstructor + public static class Binder extends AbstractBinder { + + private final UserService userService; + + @Override + protected void configure() { + bind(SecurityCheckFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); + bind(SecurityCheckInjectionResolver.class).to(new TypeLiteral>() { + }).in(Singleton.class); + bind(userService).to(UserService.class); + } + } +} diff --git a/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java b/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java deleted file mode 100644 index 030187e7..00000000 --- a/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.commafeed.frontend.auth; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import lombok.RequiredArgsConstructor; - -import org.eclipse.jetty.util.B64Code; -import org.eclipse.jetty.util.StringUtil; - -import com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserRole.Role; -import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.session.SessionHelper; -import com.google.common.base.Optional; -import com.sun.jersey.api.core.HttpContext; -import com.sun.jersey.api.model.Parameter; -import com.sun.jersey.core.spi.component.ComponentContext; -import com.sun.jersey.core.spi.component.ComponentScope; -import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable; -import com.sun.jersey.spi.inject.Injectable; -import com.sun.jersey.spi.inject.InjectableProvider; -import com.sun.jersey.spi.inject.SingletonTypeInjectableProvider; - -public class SecurityCheckProvider implements InjectableProvider { - - public static class SecurityCheckUserServiceProvider extends SingletonTypeInjectableProvider { - - public SecurityCheckUserServiceProvider(UserService userService) { - super(UserService.class, userService); - } - } - - @RequiredArgsConstructor - static class SecurityCheckInjectable extends AbstractHttpContextInjectable { - private static final String PREFIX = "Basic"; - - private final SessionHelper sessionHelper; - private final UserService userService; - private final Role role; - private final boolean apiKeyAllowed; - - @Override - public User getValue(HttpContext c) { - Optional user = apiKeyLogin(c); - if (!user.isPresent()) { - user = basicAuthenticationLogin(c); - } - if (!user.isPresent()) { - user = cookieSessionLogin(); - } - - if (user.isPresent()) { - if (user.get().hasRole(role)) { - return user.get(); - } else { - throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) - .entity("You don't have the required role to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); - } - } else { - throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) - .entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); - } - } - - Optional cookieSessionLogin() { - Optional loggedInUser = sessionHelper.getLoggedInUser(); - if (loggedInUser.isPresent()) { - userService.performPostLoginActivities(loggedInUser.get()); - } - return loggedInUser; - } - - private Optional basicAuthenticationLogin(HttpContext c) { - String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION); - if (header != null) { - int space = header.indexOf(' '); - if (space > 0) { - String method = header.substring(0, space); - if (PREFIX.equalsIgnoreCase(method)) { - String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1); - int i = decoded.indexOf(':'); - if (i > 0) { - String username = decoded.substring(0, i); - String password = decoded.substring(i + 1); - return userService.login(username, password); - } - } - } - } - return Optional.absent(); - } - - private Optional apiKeyLogin(HttpContext c) { - String apiKey = c.getUriInfo().getQueryParameters().getFirst("apiKey"); - if (apiKey != null && apiKeyAllowed) { - return userService.login(apiKey); - } - return Optional.absent(); - } - } - - private SessionHelper sessionHelper; - private UserService userService; - - public SecurityCheckProvider(@Context HttpServletRequest req, @Context UserService userService) { - this.sessionHelper = new SessionHelper(req); - this.userService = userService; - } - - @Override - public ComponentScope getScope() { - return ComponentScope.PerRequest; - } - - @Override - public Injectable getInjectable(ComponentContext ic, SecurityCheck sc, Parameter c) { - return new SecurityCheckInjectable(sessionHelper, userService, sc.value(), sc.apiKeyAllowed()); - } -} diff --git a/src/main/java/com/commafeed/frontend/model/Category.java b/src/main/java/com/commafeed/frontend/model/Category.java index 000a0520..a887de3f 100644 --- a/src/main/java/com/commafeed/frontend/model/Category.java +++ b/src/main/java/com/commafeed/frontend/model/Category.java @@ -1,14 +1,13 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.google.common.collect.Lists; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Entry details") @Data @@ -24,10 +23,10 @@ public class Category implements Serializable { private String name; @ApiModelProperty("category children categories") - private List children = Lists.newArrayList(); + private List children = new ArrayList<>(); @ApiModelProperty("category feeds") - private List feeds = Lists.newArrayList(); + private List feeds = new ArrayList<>(); @ApiModelProperty("wether the category is expanded or collapsed") private boolean expanded; diff --git a/src/main/java/com/commafeed/frontend/model/Entries.java b/src/main/java/com/commafeed/frontend/model/Entries.java index b79d33ab..c7f11c50 100644 --- a/src/main/java/com/commafeed/frontend/model/Entries.java +++ b/src/main/java/com/commafeed/frontend/model/Entries.java @@ -1,14 +1,13 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.google.common.collect.Lists; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("List of entries with some metadata") @Data @@ -39,7 +38,7 @@ public class Entries implements Serializable { private int limit; @ApiModelProperty("list of entries") - private List entries = Lists.newArrayList(); + private List entries = new ArrayList<>(); @ApiModelProperty("if true, the unread flag was ignored in the request, all entries are returned regardless of their read status") private boolean ignoredReadStatus; diff --git a/src/main/java/com/commafeed/frontend/model/Entry.java b/src/main/java/com/commafeed/frontend/model/Entry.java index 52705fbb..0cf4fa64 100644 --- a/src/main/java/com/commafeed/frontend/model/Entry.java +++ b/src/main/java/com/commafeed/frontend/model/Entry.java @@ -4,22 +4,25 @@ import java.io.Serializable; import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; -import lombok.Data; +import org.apache.commons.lang3.StringUtils; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryStatus; -import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedSubscription; -import com.google.common.collect.Lists; import com.rometools.rome.feed.synd.SyndContent; import com.rometools.rome.feed.synd.SyndContentImpl; +import com.rometools.rome.feed.synd.SyndEnclosure; +import com.rometools.rome.feed.synd.SyndEnclosureImpl; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndEntryImpl; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; @SuppressWarnings("serial") @ApiModel("Entry details") @@ -46,20 +49,17 @@ public class Entry implements Serializable { entry.setFeedUrl(sub.getFeed().getUrl()); entry.setFeedLink(sub.getFeed().getLink()); entry.setIconUrl(FeedUtils.getFaviconUrl(sub, publicUrl)); - - List tags = Lists.newArrayList(); - for (FeedEntryTag tag : status.getTags()) { - tags.add(tag.getName()); - } - entry.setTags(tags); + entry.setTags(status.getTags().stream().map(t -> t.getName()).collect(Collectors.toList())); if (content != null) { entry.setRtl(FeedUtils.isRTL(feedEntry)); entry.setTitle(content.getTitle()); - entry.setContent(FeedUtils.proxyImages(content.getContent(), publicUrl, proxyImages)); + entry.setContent(proxyImages ? FeedUtils.proxyImages(content.getContent(), publicUrl) : content.getContent()); entry.setAuthor(content.getAuthor()); - entry.setEnclosureUrl(content.getEnclosureUrl()); entry.setEnclosureType(content.getEnclosureType()); + entry.setEnclosureUrl(proxyImages && StringUtils.contains(content.getEnclosureType(), "image") + ? FeedUtils.proxyImage(content.getEnclosureUrl(), publicUrl) : content.getEnclosureUrl()); + entry.setCategories(content.getCategories()); } return entry; @@ -74,6 +74,14 @@ public class Entry implements Serializable { SyndContentImpl content = new SyndContentImpl(); content.setValue(getContent()); entry.setContents(Arrays. asList(content)); + + if (getEnclosureUrl() != null) { + SyndEnclosureImpl enclosure = new SyndEnclosureImpl(); + enclosure.setType(getEnclosureType()); + enclosure.setUrl(getEnclosureUrl()); + entry.setEnclosures(Arrays. asList(enclosure)); + } + entry.setLink(getUrl()); entry.setPublishedDate(getDate()); return entry; @@ -91,6 +99,9 @@ public class Entry implements Serializable { @ApiModelProperty("entry content") private String content; + @ApiModelProperty("comma-separated list of categories") + private String categories; + @ApiModelProperty("wether entry content and title are rtl") private boolean rtl; diff --git a/src/main/java/com/commafeed/frontend/model/FeedInfo.java b/src/main/java/com/commafeed/frontend/model/FeedInfo.java index 26ff914a..bef88c4f 100644 --- a/src/main/java/com/commafeed/frontend/model/FeedInfo.java +++ b/src/main/java/com/commafeed/frontend/model/FeedInfo.java @@ -2,10 +2,9 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.swagger.annotations.ApiModel; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; - @SuppressWarnings("serial") @ApiModel("Feed details") @Data diff --git a/src/main/java/com/commafeed/frontend/model/ServerInfo.java b/src/main/java/com/commafeed/frontend/model/ServerInfo.java index 2a3c24d8..456a69b5 100644 --- a/src/main/java/com/commafeed/frontend/model/ServerInfo.java +++ b/src/main/java/com/commafeed/frontend/model/ServerInfo.java @@ -2,10 +2,9 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.swagger.annotations.ApiModel; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; - @SuppressWarnings("serial") @ApiModel("Server infos") @Data diff --git a/src/main/java/com/commafeed/frontend/model/Settings.java b/src/main/java/com/commafeed/frontend/model/Settings.java index 46e42e64..dffec373 100644 --- a/src/main/java/com/commafeed/frontend/model/Settings.java +++ b/src/main/java/com/commafeed/frontend/model/Settings.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("User settings") @Data diff --git a/src/main/java/com/commafeed/frontend/model/Subscription.java b/src/main/java/com/commafeed/frontend/model/Subscription.java index a395b184..29779759 100644 --- a/src/main/java/com/commafeed/frontend/model/Subscription.java +++ b/src/main/java/com/commafeed/frontend/model/Subscription.java @@ -3,14 +3,14 @@ package com.commafeed.frontend.model; import java.io.Serializable; import java.util.Date; -import lombok.Data; - import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedSubscription; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; @SuppressWarnings("serial") @ApiModel("User information") @@ -35,6 +35,7 @@ public class Subscription implements Serializable { sub.setUnread(unreadCount.getUnreadCount()); sub.setNewestItemTime(unreadCount.getNewestItemTime()); sub.setCategoryId(category == null ? null : String.valueOf(category.getId())); + sub.setFilter(subscription.getFilter()); return sub; } @@ -77,4 +78,7 @@ public class Subscription implements Serializable { @ApiModelProperty("date of the newest item") private Date newestItemTime; + @ApiModelProperty(value = "JEXL string evaluated on new entries to mark them as read if they do not match") + private String filter; + } \ No newline at end of file diff --git a/src/main/java/com/commafeed/frontend/model/UnreadCount.java b/src/main/java/com/commafeed/frontend/model/UnreadCount.java index 3cb0620b..cface277 100644 --- a/src/main/java/com/commafeed/frontend/model/UnreadCount.java +++ b/src/main/java/com/commafeed/frontend/model/UnreadCount.java @@ -3,10 +3,9 @@ package com.commafeed.frontend.model; import java.io.Serializable; import java.util.Date; +import io.swagger.annotations.ApiModel; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; - @SuppressWarnings("serial") @ApiModel("Unread count") @Data diff --git a/src/main/java/com/commafeed/frontend/model/UserModel.java b/src/main/java/com/commafeed/frontend/model/UserModel.java index 0753a6d8..f5c658b0 100644 --- a/src/main/java/com/commafeed/frontend/model/UserModel.java +++ b/src/main/java/com/commafeed/frontend/model/UserModel.java @@ -3,11 +3,10 @@ package com.commafeed.frontend.model; import java.io.Serializable; import java.util.Date; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("User information") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/AddCategoryRequest.java b/src/main/java/com/commafeed/frontend/model/request/AddCategoryRequest.java index 24ff9373..edebc891 100644 --- a/src/main/java/com/commafeed/frontend/model/request/AddCategoryRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/AddCategoryRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Add Category Request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/CategoryModificationRequest.java b/src/main/java/com/commafeed/frontend/model/request/CategoryModificationRequest.java index ef34156a..c645f552 100644 --- a/src/main/java/com/commafeed/frontend/model/request/CategoryModificationRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/CategoryModificationRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Category modification request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/CollapseRequest.java b/src/main/java/com/commafeed/frontend/model/request/CollapseRequest.java index ab24f6b7..d10fd441 100644 --- a/src/main/java/com/commafeed/frontend/model/request/CollapseRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/CollapseRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Mark Request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/FeedInfoRequest.java b/src/main/java/com/commafeed/frontend/model/request/FeedInfoRequest.java index 9936c0d7..b9cb2b90 100644 --- a/src/main/java/com/commafeed/frontend/model/request/FeedInfoRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/FeedInfoRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Feed information request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/FeedMergeRequest.java b/src/main/java/com/commafeed/frontend/model/request/FeedMergeRequest.java index 7f0792c9..0ce76d37 100644 --- a/src/main/java/com/commafeed/frontend/model/request/FeedMergeRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/FeedMergeRequest.java @@ -3,11 +3,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; import java.util.List; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Feed merge Request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java b/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java index c8b44876..b014b9e6 100644 --- a/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Feed modification request") @Data @@ -24,4 +23,7 @@ public class FeedModificationRequest implements Serializable { @ApiModelProperty(value = "new display position, null if not changed") private Integer position; + @ApiModelProperty(value = "JEXL string evaluated on new entries to mark them as read if they do not match") + private String filter; + } diff --git a/src/main/java/com/commafeed/frontend/model/request/IDRequest.java b/src/main/java/com/commafeed/frontend/model/request/IDRequest.java index 88d7f338..66ea36e7 100644 --- a/src/main/java/com/commafeed/frontend/model/request/IDRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/IDRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java b/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java index 5584e498..a6b01660 100644 --- a/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @Data @ApiModel diff --git a/src/main/java/com/commafeed/frontend/model/request/MarkRequest.java b/src/main/java/com/commafeed/frontend/model/request/MarkRequest.java index 2208f069..064f8e37 100644 --- a/src/main/java/com/commafeed/frontend/model/request/MarkRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/MarkRequest.java @@ -3,11 +3,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; import java.util.List; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Mark Request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/MultipleMarkRequest.java b/src/main/java/com/commafeed/frontend/model/request/MultipleMarkRequest.java index 706b065d..d1dcbd26 100644 --- a/src/main/java/com/commafeed/frontend/model/request/MultipleMarkRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/MultipleMarkRequest.java @@ -3,11 +3,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; import java.util.List; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Multiple Mark Request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/PasswordResetRequest.java b/src/main/java/com/commafeed/frontend/model/request/PasswordResetRequest.java index 506df3e8..39454217 100644 --- a/src/main/java/com/commafeed/frontend/model/request/PasswordResetRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/PasswordResetRequest.java @@ -2,13 +2,12 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; -import lombok.Data; - import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; @SuppressWarnings("serial") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java b/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java index e60e7841..359a2d74 100644 --- a/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Profile modification request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java b/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java index 1c1bbd2a..b93af520 100644 --- a/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java @@ -2,14 +2,13 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; -import lombok.Data; - import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotEmpty; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; @SuppressWarnings("serial") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/StarRequest.java b/src/main/java/com/commafeed/frontend/model/request/StarRequest.java index 0661ed75..6836936c 100644 --- a/src/main/java/com/commafeed/frontend/model/request/StarRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/StarRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Star Request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java b/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java index 485b5e0d..260b416d 100644 --- a/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java @@ -2,11 +2,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Subscription request") @Data diff --git a/src/main/java/com/commafeed/frontend/model/request/TagRequest.java b/src/main/java/com/commafeed/frontend/model/request/TagRequest.java index 0ea1e8e2..bdc2777e 100644 --- a/src/main/java/com/commafeed/frontend/model/request/TagRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/TagRequest.java @@ -3,11 +3,10 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; import java.util.List; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import com.wordnik.swagger.annotations.ApiModel; -import com.wordnik.swagger.annotations.ApiModelProperty; - @SuppressWarnings("serial") @ApiModel("Tag Request") @Data diff --git a/src/main/java/com/commafeed/frontend/resource/AdminREST.java b/src/main/java/com/commafeed/frontend/resource/AdminREST.java index 08e43ba5..e9f136ed 100644 --- a/src/main/java/com/commafeed/frontend/resource/AdminREST.java +++ b/src/main/java/com/commafeed/frontend/resource/AdminREST.java @@ -1,7 +1,6 @@ package com.commafeed.frontend.resource; -import io.dropwizard.hibernate.UnitOfWork; - +import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -17,11 +16,10 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import lombok.RequiredArgsConstructor; - -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration.ApplicationSettings; @@ -36,17 +34,19 @@ import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.request.IDRequest; import com.google.common.base.Preconditions; -import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import com.wordnik.swagger.annotations.Api; -import com.wordnik.swagger.annotations.ApiOperation; -import com.wordnik.swagger.annotations.ApiParam; + +import io.dropwizard.hibernate.UnitOfWork; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; @Path("/admin") -@Api(value = "/admin", description = "Operations about application administration") +@Api(value = "/admin") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class AdminREST { @@ -61,6 +61,7 @@ public class AdminREST { @POST @UnitOfWork @ApiOperation(value = "Save or update a user", notes = "Save or update a user. If the id is not specified, a new user will be created") + @Timed public Response save(@SecurityCheck(Role.ADMIN) User user, @ApiParam(required = true) UserModel userModel) { Preconditions.checkNotNull(userModel); Preconditions.checkNotNull(userModel.getName()); @@ -115,6 +116,7 @@ public class AdminREST { @GET @UnitOfWork @ApiOperation(value = "Get user information", notes = "Get user information", response = UserModel.class) + @Timed public Response getUser(@SecurityCheck(Role.ADMIN) User user, @ApiParam(value = "user id", required = true) @PathParam("id") Long id) { Preconditions.checkNotNull(id); User u = userDAO.findById(id); @@ -123,11 +125,7 @@ public class AdminREST { userModel.setName(u.getName()); userModel.setEmail(u.getEmail()); userModel.setEnabled(!u.isDisabled()); - for (UserRole role : userRoleDAO.findAll(u)) { - if (role.getRole() == Role.ADMIN) { - userModel.setAdmin(true); - } - } + userModel.setAdmin(userRoleDAO.findAll(u).stream().anyMatch(r -> r.getRole() == Role.ADMIN)); return Response.ok(userModel).build(); } @@ -135,8 +133,9 @@ public class AdminREST { @GET @UnitOfWork @ApiOperation(value = "Get all users", notes = "Get all users", response = UserModel.class, responseContainer = "List") + @Timed public Response getUsers(@SecurityCheck(Role.ADMIN) User user) { - Map users = Maps.newHashMap(); + Map users = new HashMap<>(); for (UserRole role : userRoleDAO.findAll()) { User u = role.getUser(); Long key = u.getId(); @@ -162,6 +161,7 @@ public class AdminREST { @POST @UnitOfWork @ApiOperation(value = "Delete a user", notes = "Delete a user, and all his subscriptions") + @Timed public Response delete(@SecurityCheck(Role.ADMIN) User user, @ApiParam(required = true) IDRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -181,6 +181,7 @@ public class AdminREST { @GET @UnitOfWork @ApiOperation(value = "Retrieve application settings", notes = "Retrieve application settings", response = ApplicationSettings.class) + @Timed public Response getSettings(@SecurityCheck(Role.ADMIN) User user) { return Response.ok(config.getApplicationSettings()).build(); } @@ -189,6 +190,7 @@ public class AdminREST { @GET @UnitOfWork @ApiOperation(value = "Retrieve server metrics") + @Timed public Response getMetrics(@SecurityCheck(Role.ADMIN) User user) { return Response.ok(metrics).build(); } diff --git a/src/main/java/com/commafeed/frontend/resource/CategoryREST.java b/src/main/java/com/commafeed/frontend/resource/CategoryREST.java index 82996b58..67db7707 100644 --- a/src/main/java/com/commafeed/frontend/resource/CategoryREST.java +++ b/src/main/java/com/commafeed/frontend/resource/CategoryREST.java @@ -1,14 +1,16 @@ package com.commafeed.frontend.resource; -import io.dropwizard.hibernate.UnitOfWork; - import java.io.StringWriter; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -23,18 +25,17 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang.ObjectUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; +import com.commafeed.backend.feed.FeedEntryKeyword; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedEntryStatus; @@ -55,23 +56,25 @@ import com.commafeed.frontend.model.request.CategoryModificationRequest; import com.commafeed.frontend.model.request.CollapseRequest; import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; -import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; -import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.feed.synd.SyndFeedImpl; import com.rometools.rome.io.SyndFeedOutput; -import com.wordnik.swagger.annotations.Api; -import com.wordnik.swagger.annotations.ApiOperation; -import com.wordnik.swagger.annotations.ApiParam; + +import io.dropwizard.hibernate.UnitOfWork; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Path("/category") -@Api(value = "/category", description = "Operations about user categories") +@Api(value = "/category") @Slf4j @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class CategoryREST { @@ -90,10 +93,13 @@ public class CategoryREST { @GET @UnitOfWork @ApiOperation(value = "Get category entries", notes = "Get a list of category entries", response = Entries.class) - public Response getCategoryEntries( - @SecurityCheck User user, + @Timed + public Response getCategoryEntries(@SecurityCheck User user, @ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, - @ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, + @ApiParam( + value = "all entries or only unread ones", + allowableValues = "all,unread", + required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, @ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan, @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, @@ -101,13 +107,15 @@ public class CategoryREST { @ApiParam( value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords, @ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds, - @ApiParam(value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, + @ApiParam( + value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, @ApiParam(value = "keep only entries tagged with this tag") @QueryParam("tag") String tag) { Preconditions.checkNotNull(readType); keywords = StringUtils.trimToNull(keywords); Preconditions.checkArgument(keywords == null || StringUtils.length(keywords) >= 3); + List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); limit = Math.min(limit, 1000); limit = Math.max(0, limit); @@ -124,32 +132,27 @@ public class CategoryREST { List excludedIds = null; if (StringUtils.isNotEmpty(excludedSubscriptionIds)) { - excludedIds = Lists.newArrayList(); - for (String excludedId : excludedSubscriptionIds.split(",")) { - excludedIds.add(Long.valueOf(excludedId)); - } + excludedIds = Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).collect(Collectors.toList()); } if (ALL.equals(id)) { - entries.setName(Optional.fromNullable(tag).or("All")); + entries.setName(Optional.ofNullable(tag).orElse("All")); List subs = feedSubscriptionDAO.findAll(user); removeExcludedSubscriptions(subs, excludedIds); - List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, keywords, newerThanDate, offset, - limit + 1, order, true, onlyIds, tag); + List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, + offset, limit + 1, order, true, onlyIds, tag); for (FeedEntryStatus status : list) { - entries.getEntries().add( - Entry.build(status, config.getApplicationSettings().getPublicUrl(), config.getApplicationSettings() - .isImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getPublicUrl(), + config.getApplicationSettings().getImageProxyEnabled())); } } else if (STARRED.equals(id)) { entries.setName("Starred"); List starred = feedEntryStatusDAO.findStarred(user, newerThanDate, offset, limit + 1, order, !onlyIds); for (FeedEntryStatus status : starred) { - entries.getEntries().add( - Entry.build(status, config.getApplicationSettings().getPublicUrl(), config.getApplicationSettings() - .isImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getPublicUrl(), + config.getApplicationSettings().getImageProxyEnabled())); } } else { FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id)); @@ -157,13 +160,12 @@ public class CategoryREST { List categories = feedCategoryDAO.findAllChildrenCategories(user, parent); List subs = feedSubscriptionDAO.findByCategories(user, categories); removeExcludedSubscriptions(subs, excludedIds); - List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, keywords, newerThanDate, + List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, tag); for (FeedEntryStatus status : list) { - entries.getEntries().add( - Entry.build(status, config.getApplicationSettings().getPublicUrl(), config.getApplicationSettings() - .isImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getPublicUrl(), + config.getApplicationSettings().getImageProxyEnabled())); } entries.setName(parent.getName()); } else { @@ -179,7 +181,7 @@ public class CategoryREST { entries.setTimestamp(System.currentTimeMillis()); entries.setIgnoredReadStatus(STARRED.equals(id) || keywords != null || tag != null); - FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords); + FeedUtils.removeUnwantedFromSearch(entries.getEntries(), entryKeywords); return Response.ok(entries).build(); } @@ -188,10 +190,13 @@ public class CategoryREST { @UnitOfWork @ApiOperation(value = "Get category entries as feed", notes = "Get a feed of category entries") @Produces(MediaType.APPLICATION_XML) - public Response getCategoryEntriesAsFeed( - @SecurityCheck(apiKeyAllowed = true) User user, + @Timed + public Response getCategoryEntriesAsFeed(@SecurityCheck(apiKeyAllowed = true) User user, @ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, - @ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType, + @ApiParam( + value = "all entries or only unread ones", + allowableValues = "all,unread", + required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType, @ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan, @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, @@ -199,7 +204,8 @@ public class CategoryREST { @ApiParam( value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords, @ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds, - @ApiParam(value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, + @ApiParam( + value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, @ApiParam(value = "keep only entries tagged with this tag") @QueryParam("tag") String tag) { Response response = getCategoryEntries(user, id, readType, newerThan, offset, limit, order, keywords, onlyIds, @@ -213,14 +219,8 @@ public class CategoryREST { feed.setFeedType("rss_2.0"); feed.setTitle("CommaFeed - " + entries.getName()); feed.setDescription("CommaFeed - " + entries.getName()); - String publicUrl = config.getApplicationSettings().getPublicUrl(); - feed.setLink(publicUrl); - - List children = Lists.newArrayList(); - for (Entry entry : entries.getEntries()) { - children.add(entry.asRss()); - } - feed.setEntries(children); + feed.setLink(config.getApplicationSettings().getPublicUrl()); + feed.setEntries(entries.getEntries().stream().map(e -> e.asRss()).collect(Collectors.toList())); SyndFeedOutput output = new SyndFeedOutput(); StringWriter writer = new StringWriter(); @@ -237,6 +237,7 @@ public class CategoryREST { @POST @UnitOfWork @ApiOperation(value = "Mark category entries", notes = "Mark feed entries of this category as read") + @Timed public Response markCategoryEntries(@SecurityCheck User user, @ApiParam(value = "category id, or 'all'", required = true) MarkRequest req) { Preconditions.checkNotNull(req); @@ -244,11 +245,12 @@ public class CategoryREST { Date olderThan = req.getOlderThan() == null ? null : new Date(req.getOlderThan()); String keywords = req.getKeywords(); + List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); if (ALL.equals(req.getId())) { List subs = feedSubscriptionDAO.findAll(user); removeExcludedSubscriptions(subs, req.getExcludedSubscriptions()); - feedEntryService.markSubscriptionEntries(user, subs, olderThan, keywords); + feedEntryService.markSubscriptionEntries(user, subs, olderThan, entryKeywords); } else if (STARRED.equals(req.getId())) { feedEntryService.markStarredEntries(user, olderThan); } else { @@ -256,7 +258,7 @@ public class CategoryREST { List categories = feedCategoryDAO.findAllChildrenCategories(user, parent); List subs = feedSubscriptionDAO.findByCategories(user, categories); removeExcludedSubscriptions(subs, req.getExcludedSubscriptions()); - feedEntryService.markSubscriptionEntries(user, subs, olderThan, keywords); + feedEntryService.markSubscriptionEntries(user, subs, olderThan, entryKeywords); } return Response.ok().build(); } @@ -277,6 +279,7 @@ public class CategoryREST { @POST @UnitOfWork @ApiOperation(value = "Add a category", notes = "Add a new feed category", response = Long.class) + @Timed public Response addCategory(@SecurityCheck User user, @ApiParam(required = true) AddCategoryRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getName()); @@ -300,6 +303,7 @@ public class CategoryREST { @Path("/delete") @UnitOfWork @ApiOperation(value = "Delete a category", notes = "Delete an existing feed category") + @Timed public Response deleteCategory(@SecurityCheck User user, @ApiParam(required = true) IDRequest req) { Preconditions.checkNotNull(req); @@ -332,6 +336,7 @@ public class CategoryREST { @Path("/modify") @UnitOfWork @ApiOperation(value = "Rename a category", notes = "Rename an existing feed category") + @Timed public Response modifyCategory(@SecurityCheck User user, @ApiParam(required = true) CategoryModificationRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -360,7 +365,7 @@ public class CategoryREST { int existingIndex = -1; for (int i = 0; i < categories.size(); i++) { - if (ObjectUtils.equals(categories.get(i).getId(), category.getId())) { + if (Objects.equals(categories.get(i).getId(), category.getId())) { existingIndex = i; } } @@ -386,6 +391,7 @@ public class CategoryREST { @Path("/collapse") @UnitOfWork @ApiOperation(value = "Collapse a category", notes = "Save collapsed or expanded status for a category") + @Timed public Response collapse(@SecurityCheck User user, @ApiParam(required = true) CollapseRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -404,6 +410,7 @@ public class CategoryREST { @Path("/unreadCount") @UnitOfWork @ApiOperation(value = "Get unread count for feed subscriptions", response = UnreadCount.class, responseContainer = "List") + @Timed public Response getUnreadCount(@SecurityCheck User user) { Map unreadCount = feedSubscriptionService.getUnreadCount(user); return Response.ok(Lists.newArrayList(unreadCount.values())).build(); @@ -413,6 +420,7 @@ public class CategoryREST { @Path("/get") @UnitOfWork @ApiOperation(value = "Get feed categories", notes = "Get all categories and subscriptions of the user", response = Category.class) + @Timed public Response getSubscriptions(@SecurityCheck User user) { Category root = cache.getUserRootCategory(user); if (root == null) { @@ -437,7 +445,7 @@ public class CategoryREST { category.setExpanded(true); for (FeedCategory c : categories) { - if ((id == null && c.getParent() == null) || (c.getParent() != null && ObjectUtils.equals(c.getParent().getId(), id))) { + if ((id == null && c.getParent() == null) || (c.getParent() != null && Objects.equals(c.getParent().getId(), id))) { Category child = buildCategory(c.getId(), categories, subscriptions, unreadCount); child.setId(String.valueOf(c.getId())); child.setName(c.getName()); @@ -458,7 +466,7 @@ public class CategoryREST { for (FeedSubscription subscription : subscriptions) { if ((id == null && subscription.getCategory() == null) - || (subscription.getCategory() != null && ObjectUtils.equals(subscription.getCategory().getId(), id))) { + || (subscription.getCategory() != null && Objects.equals(subscription.getCategory().getId(), id))) { UnreadCount uc = unreadCount.get(subscription.getId()); Subscription sub = Subscription.build(subscription, config.getApplicationSettings().getPublicUrl(), uc); category.getFeeds().add(sub); diff --git a/src/main/java/com/commafeed/frontend/resource/EntryREST.java b/src/main/java/com/commafeed/frontend/resource/EntryREST.java index 9a6afe60..57591f4e 100644 --- a/src/main/java/com/commafeed/frontend/resource/EntryREST.java +++ b/src/main/java/com/commafeed/frontend/resource/EntryREST.java @@ -1,7 +1,5 @@ package com.commafeed.frontend.resource; -import io.dropwizard.hibernate.UnitOfWork; - import java.util.List; import javax.inject.Inject; @@ -14,8 +12,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import lombok.RequiredArgsConstructor; - +import com.codahale.metrics.annotation.Timed; import com.commafeed.backend.dao.FeedEntryTagDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedEntryService; @@ -26,15 +23,18 @@ import com.commafeed.frontend.model.request.MultipleMarkRequest; import com.commafeed.frontend.model.request.StarRequest; import com.commafeed.frontend.model.request.TagRequest; import com.google.common.base.Preconditions; -import com.wordnik.swagger.annotations.Api; -import com.wordnik.swagger.annotations.ApiOperation; -import com.wordnik.swagger.annotations.ApiParam; + +import io.dropwizard.hibernate.UnitOfWork; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; @Path("/entry") -@Api(value = "/entry", description = "Operations about feed entries") +@Api(value = "/entry") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class EntryREST { @@ -46,6 +46,7 @@ public class EntryREST { @POST @UnitOfWork @ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread") + @Timed public Response markFeedEntry(@SecurityCheck User user, @ApiParam(value = "Mark Request", required = true) MarkRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -58,6 +59,7 @@ public class EntryREST { @POST @UnitOfWork @ApiOperation(value = "Mark multiple feed entries", notes = "Mark feed entries as read/unread") + @Timed public Response markFeedEntries(@SecurityCheck User user, @ApiParam(value = "Multiple Mark Request", required = true) MultipleMarkRequest req) { Preconditions.checkNotNull(req); @@ -74,6 +76,7 @@ public class EntryREST { @POST @UnitOfWork @ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread") + @Timed public Response starFeedEntry(@SecurityCheck User user, @ApiParam(value = "Star Request", required = true) StarRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -88,6 +91,7 @@ public class EntryREST { @GET @UnitOfWork @ApiOperation(value = "Get list of tags for the user", notes = "Get list of tags for the user") + @Timed public Response getTags(@SecurityCheck User user) { List tags = feedEntryTagDAO.findByUser(user); return Response.ok(tags).build(); @@ -97,6 +101,7 @@ public class EntryREST { @POST @UnitOfWork @ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread") + @Timed public Response tagFeedEntry(@SecurityCheck User user, @ApiParam(value = "Tag Request", required = true) TagRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getEntryId()); diff --git a/src/main/java/com/commafeed/frontend/resource/FeedREST.java b/src/main/java/com/commafeed/frontend/resource/FeedREST.java index bb794661..58e25581 100644 --- a/src/main/java/com/commafeed/frontend/resource/FeedREST.java +++ b/src/main/java/com/commafeed/frontend/resource/FeedREST.java @@ -1,7 +1,5 @@ package com.commafeed.frontend.resource; -import io.dropwizard.hibernate.UnitOfWork; - import java.io.InputStream; import java.io.StringWriter; import java.net.URI; @@ -11,6 +9,9 @@ import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -29,25 +30,28 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.ObjectUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.glassfish.jersey.media.multipart.FormDataParam; +import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; +import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon; +import com.commafeed.backend.feed.FeedEntryKeyword; import com.commafeed.backend.feed.FeedFetcher; import com.commafeed.backend.feed.FeedQueues; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FetchedFeed; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedCategory; +import com.commafeed.backend.model.FeedEntry; +import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; @@ -55,6 +59,8 @@ import com.commafeed.backend.model.UserSettings.ReadingMode; import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.opml.OPMLExporter; import com.commafeed.backend.opml.OPMLImporter; +import com.commafeed.backend.service.FeedEntryFilteringService; +import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedService; import com.commafeed.backend.service.FeedSubscriptionService; @@ -71,27 +77,42 @@ import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.SubscribeRequest; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; -import com.google.common.collect.Lists; import com.rometools.opml.feed.opml.Opml; -import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.feed.synd.SyndFeedImpl; import com.rometools.rome.io.SyndFeedOutput; import com.rometools.rome.io.WireFeedOutput; -import com.sun.jersey.multipart.FormDataParam; -import com.wordnik.swagger.annotations.Api; -import com.wordnik.swagger.annotations.ApiOperation; -import com.wordnik.swagger.annotations.ApiParam; + +import io.dropwizard.hibernate.UnitOfWork; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Path("/feed") -@Api(value = "/feed", description = "Operations about feeds") +@Api(value = "/feed") @Slf4j @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class FeedREST { + private static final FeedEntry TEST_ENTRY = initTestEntry(); + + private static FeedEntry initTestEntry() { + FeedEntry entry = new FeedEntry(); + entry.setUrl("https://github.com/Athou/commafeed"); + + FeedEntryContent content = new FeedEntryContent(); + content.setAuthor("Athou"); + content.setTitle("Merge pull request #662 from Athou/dw8"); + content.setContent("Merge pull request #662 from Athou/dw8"); + entry.setContent(content); + return entry; + } + private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedCategoryDAO feedCategoryDAO; private final FeedEntryStatusDAO feedEntryStatusDAO; @@ -99,6 +120,7 @@ public class FeedREST { private final FeedService feedService; private final FeedEntryService feedEntryService; private final FeedSubscriptionService feedSubscriptionService; + private final FeedEntryFilteringService feedEntryFilteringService; private final FeedQueues queues; private final OPMLImporter opmlImporter; private final OPMLExporter opmlExporter; @@ -109,10 +131,13 @@ public class FeedREST { @GET @UnitOfWork @ApiOperation(value = "Get feed entries", notes = "Get a list of feed entries", response = Entries.class) - public Response getFeedEntries( - @SecurityCheck User user, + @Timed + public Response getFeedEntries(@SecurityCheck User user, @ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id, - @ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, + @ApiParam( + value = "all entries or only unread ones", + allowableValues = "all,unread", + required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, @ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan, @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, @@ -126,6 +151,7 @@ public class FeedREST { keywords = StringUtils.trimToNull(keywords); Preconditions.checkArgument(keywords == null || StringUtils.length(keywords) >= 3); + List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); limit = Math.min(limit, 1000); limit = Math.max(0, limit); @@ -145,13 +171,12 @@ public class FeedREST { entries.setErrorCount(subscription.getFeed().getErrorCount()); entries.setFeedLink(subscription.getFeed().getLink()); - List list = feedEntryStatusDAO.findBySubscriptions(user, Arrays.asList(subscription), unreadOnly, keywords, - newerThanDate, offset, limit + 1, order, true, onlyIds, null); + List list = feedEntryStatusDAO.findBySubscriptions(user, Arrays.asList(subscription), unreadOnly, + entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null); for (FeedEntryStatus status : list) { - entries.getEntries().add( - Entry.build(status, config.getApplicationSettings().getPublicUrl(), config.getApplicationSettings() - .isImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getPublicUrl(), + config.getApplicationSettings().getImageProxyEnabled())); } boolean hasMore = entries.getEntries().size() > limit; @@ -165,7 +190,7 @@ public class FeedREST { entries.setTimestamp(System.currentTimeMillis()); entries.setIgnoredReadStatus(keywords != null); - FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords); + FeedUtils.removeUnwantedFromSearch(entries.getEntries(), entryKeywords); return Response.ok(entries).build(); } @@ -174,10 +199,13 @@ public class FeedREST { @UnitOfWork @ApiOperation(value = "Get feed entries as a feed", notes = "Get a feed of feed entries") @Produces(MediaType.APPLICATION_XML) - public Response getFeedEntriesAsFeed( - @SecurityCheck(apiKeyAllowed = true) User user, + @Timed + public Response getFeedEntriesAsFeed(@SecurityCheck(apiKeyAllowed = true) User user, @ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id, - @ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType, + @ApiParam( + value = "all entries or only unread ones", + allowableValues = "all,unread", + required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType, @ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan, @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, @@ -196,14 +224,8 @@ public class FeedREST { feed.setFeedType("rss_2.0"); feed.setTitle("CommaFeed - " + entries.getName()); feed.setDescription("CommaFeed - " + entries.getName()); - String publicUrl = config.getApplicationSettings().getPublicUrl(); - feed.setLink(publicUrl); - - List children = Lists.newArrayList(); - for (Entry entry : entries.getEntries()) { - children.add(entry.asRss()); - } - feed.setEntries(children); + feed.setLink(config.getApplicationSettings().getPublicUrl()); + feed.setEntries(entries.getEntries().stream().map(e -> e.asRss()).collect(Collectors.toList())); SyndFeedOutput output = new SyndFeedOutput(); StringWriter writer = new StringWriter(); @@ -237,6 +259,7 @@ public class FeedREST { @Path("/fetch") @UnitOfWork @ApiOperation(value = "Fetch a feed", notes = "Fetch a feed by its url", response = FeedInfo.class) + @Timed public Response fetchFeed(@SecurityCheck User user, @ApiParam(value = "feed url", required = true) FeedInfoRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getUrl()); @@ -246,7 +269,7 @@ public class FeedREST { info = fetchFeedInternal(req.getUrl()); } catch (Exception e) { return Response.status(Status.INTERNAL_SERVER_ERROR).entity(Throwables.getStackTraceAsString(Throwables.getRootCause(e))) - .build(); + .type(MediaType.TEXT_PLAIN).build(); } return Response.ok(info).build(); } @@ -255,6 +278,7 @@ public class FeedREST { @GET @UnitOfWork @ApiOperation(value = "Queue all feeds of the user for refresh", notes = "Manually add all feeds of the user to the refresh queue") + @Timed public Response queueAllForRefresh(@SecurityCheck User user) { feedSubscriptionService.refreshAll(user); return Response.ok().build(); @@ -264,6 +288,7 @@ public class FeedREST { @POST @UnitOfWork @ApiOperation(value = "Queue a feed for refresh", notes = "Manually add a feed to the refresh queue") + @Timed public Response queueForRefresh(@SecurityCheck User user, @ApiParam(value = "Feed id") IDRequest req) { Preconditions.checkNotNull(req); @@ -282,16 +307,18 @@ public class FeedREST { @POST @UnitOfWork @ApiOperation(value = "Mark feed entries", notes = "Mark feed entries as read (unread is not supported)") + @Timed public Response markFeedEntries(@SecurityCheck User user, @ApiParam(value = "Mark request") MarkRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); Date olderThan = req.getOlderThan() == null ? null : new Date(req.getOlderThan()); String keywords = req.getKeywords(); + List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId())); if (subscription != null) { - feedEntryService.markSubscriptionEntries(user, Arrays.asList(subscription), olderThan, keywords); + feedEntryService.markSubscriptionEntries(user, Arrays.asList(subscription), olderThan, entryKeywords); } return Response.ok().build(); } @@ -300,6 +327,7 @@ public class FeedREST { @Path("/get/{id}") @UnitOfWork @ApiOperation(value = "", notes = "") + @Timed public Response get(@SecurityCheck User user, @ApiParam(value = "user id", required = true) @PathParam("id") Long id) { Preconditions.checkNotNull(id); @@ -315,6 +343,7 @@ public class FeedREST { @Path("/favicon/{id}") @UnitOfWork @ApiOperation(value = "Fetch a feed's icon", notes = "Fetch a feed's icon") + @Timed public Response getFavicon(@SecurityCheck User user, @ApiParam(value = "subscription id") @PathParam("id") Long id) { Preconditions.checkNotNull(id); @@ -323,9 +352,9 @@ public class FeedREST { return Response.status(Status.NOT_FOUND).build(); } Feed feed = subscription.getFeed(); - byte[] icon = feedService.fetchFavicon(feed); + Favicon icon = feedService.fetchFavicon(feed); - ResponseBuilder builder = Response.ok(icon, "image/x-icon"); + ResponseBuilder builder = Response.ok(icon.getIcon(), Optional.ofNullable(icon.getMediaType()).orElse("image/x-icon")); CacheControl cacheControl = new CacheControl(); cacheControl.setMaxAge(2592000); @@ -344,6 +373,7 @@ public class FeedREST { @Path("/subscribe") @UnitOfWork @ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed") + @Timed public Response subscribe(@SecurityCheck User user, @ApiParam(value = "subscription request", required = true) SubscribeRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getTitle()); @@ -370,6 +400,7 @@ public class FeedREST { @Path("/subscribe") @UnitOfWork @ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed") + @Timed public Response subscribe(@SecurityCheck User user, @ApiParam(value = "feed url", required = true) @QueryParam("url") String url) { try { @@ -379,7 +410,7 @@ public class FeedREST { url = fetchFeedInternal(url).getUrl(); FeedInfo info = fetchFeedInternal(url); - feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null); + feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle()); } catch (Exception e) { log.info("Could not subscribe to url {} : {}", url, e.getMessage()); } @@ -397,6 +428,7 @@ public class FeedREST { @Path("/unsubscribe") @UnitOfWork @ApiOperation(value = "Unsubscribe from a feed", notes = "Unsubscribe from a feed") + @Timed public Response unsubscribe(@SecurityCheck User user, @ApiParam(required = true) IDRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -413,11 +445,19 @@ public class FeedREST { @Path("/modify") @UnitOfWork @ApiOperation(value = "Modify a subscription", notes = "Modify a feed subscription") + @Timed public Response modify(@SecurityCheck User user, @ApiParam(value = "subscription id", required = true) FeedModificationRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); + try { + feedEntryFilteringService.filterMatchesEntry(req.getFilter(), TEST_ENTRY); + } catch (FeedEntryFilterException e) { + return Response.status(Status.BAD_REQUEST).entity(e.getCause().getMessage()).type(MediaType.TEXT_PLAIN).build(); + } + FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId()); + subscription.setFilter(StringUtils.lowerCase(req.getFilter())); if (StringUtils.isNotBlank(req.getName())) { subscription.setTitle(req.getName()); @@ -440,7 +480,7 @@ public class FeedREST { int existingIndex = -1; for (int i = 0; i < subs.size(); i++) { - if (ObjectUtils.equals(subs.get(i).getId(), subscription.getId())) { + if (Objects.equals(subs.get(i).getId(), subscription.getId())) { existingIndex = i; } } @@ -465,12 +505,13 @@ public class FeedREST { @UnitOfWork @Consumes(MediaType.MULTIPART_FORM_DATA) @ApiOperation(value = "OPML import", notes = "Import an OPML file, posted as a FORM with the 'file' name") + @Timed public Response importOpml(@SecurityCheck User user, @FormDataParam("file") InputStream input) { String publicUrl = config.getApplicationSettings().getPublicUrl(); if (StringUtils.isBlank(publicUrl)) { - throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR) - .entity("Set the public URL in the admin section.").build()); + throw new WebApplicationException( + Response.status(Status.INTERNAL_SERVER_ERROR).entity("Set the public URL in the admin section.").build()); } if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) { @@ -491,6 +532,7 @@ public class FeedREST { @UnitOfWork @Produces(MediaType.APPLICATION_XML) @ApiOperation(value = "OPML export", notes = "Export an OPML file of the user's subscriptions") + @Timed public Response exportOpml(@SecurityCheck User user) { Opml opml = opmlExporter.export(user); WireFeedOutput output = new WireFeedOutput(); diff --git a/src/main/java/com/commafeed/frontend/resource/PubSubHubbubCallbackREST.java b/src/main/java/com/commafeed/frontend/resource/PubSubHubbubCallbackREST.java index cf6750ca..f90e0aa2 100644 --- a/src/main/java/com/commafeed/frontend/resource/PubSubHubbubCallbackREST.java +++ b/src/main/java/com/commafeed/frontend/resource/PubSubHubbubCallbackREST.java @@ -1,7 +1,5 @@ package com.commafeed.frontend.resource; -import io.dropwizard.hibernate.UnitOfWork; - import java.util.Date; import java.util.List; @@ -19,14 +17,12 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.feed.FeedParser; @@ -35,9 +31,13 @@ import com.commafeed.backend.feed.FetchedFeed; import com.commafeed.backend.model.Feed; import com.google.common.base.Preconditions; +import io.dropwizard.hibernate.UnitOfWork; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + @Path("/push") @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class PubSubHubbubCallbackREST { @@ -54,10 +54,11 @@ public class PubSubHubbubCallbackREST { @GET @UnitOfWork @Produces(MediaType.TEXT_PLAIN) + @Timed public Response verify(@QueryParam("hub.mode") String mode, @QueryParam("hub.topic") String topic, @QueryParam("hub.challenge") String challenge, @QueryParam("hub.lease_seconds") String leaseSeconds, @QueryParam("hub.verify_token") String verifyToken) { - if (!config.getApplicationSettings().isPubsubhubbub()) { + if (!config.getApplicationSettings().getPubsubhubbub()) { return Response.status(Status.FORBIDDEN).entity("pubsubhubbub is disabled").build(); } @@ -85,9 +86,10 @@ public class PubSubHubbubCallbackREST { @POST @UnitOfWork @Consumes({ MediaType.APPLICATION_ATOM_XML, "application/rss+xml" }) + @Timed public Response callback() { - if (!config.getApplicationSettings().isPubsubhubbub()) { + if (!config.getApplicationSettings().getPubsubhubbub()) { return Response.status(Status.FORBIDDEN).entity("pubsubhubbub is disabled").build(); } diff --git a/src/main/java/com/commafeed/frontend/resource/ServerREST.java b/src/main/java/com/commafeed/frontend/resource/ServerREST.java index e3623f5e..eeebc160 100644 --- a/src/main/java/com/commafeed/frontend/resource/ServerREST.java +++ b/src/main/java/com/commafeed/frontend/resource/ServerREST.java @@ -1,7 +1,5 @@ package com.commafeed.frontend.resource; -import io.dropwizard.hibernate.UnitOfWork; - import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.Consumes; @@ -13,10 +11,9 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import lombok.RequiredArgsConstructor; - -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; +import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter.HttpResult; @@ -24,14 +21,17 @@ import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.User; import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.ServerInfo; -import com.wordnik.swagger.annotations.Api; -import com.wordnik.swagger.annotations.ApiOperation; + +import io.dropwizard.hibernate.UnitOfWork; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; @Path("/server") -@Api(value = "/server", description = "Operations about server infos") +@Api(value = "/server") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class ServerREST { @@ -42,12 +42,13 @@ public class ServerREST { @GET @UnitOfWork @ApiOperation(value = "Get server infos", notes = "Get server infos", response = ServerInfo.class) + @Timed public Response get() { ServerInfo infos = new ServerInfo(); infos.setAnnouncement(config.getApplicationSettings().getAnnouncement()); infos.setVersion(config.getVersion()); infos.setGitCommit(config.getGitCommit()); - infos.setAllowRegistrations(config.getApplicationSettings().isAllowRegistrations()); + infos.setAllowRegistrations(config.getApplicationSettings().getAllowRegistrations()); infos.setGoogleAnalyticsCode(config.getApplicationSettings().getGoogleAnalyticsTrackingCode()); infos.setSmtpEnabled(StringUtils.isNotBlank(config.getApplicationSettings().getSmtpHost())); return Response.ok(infos).build(); @@ -58,8 +59,9 @@ public class ServerREST { @UnitOfWork @ApiOperation(value = "proxy image") @Produces("image/png") + @Timed public Response get(@SecurityCheck User user, @QueryParam("u") String url) { - if (!config.getApplicationSettings().isImageProxyEnabled()) { + if (!config.getApplicationSettings().getImageProxyEnabled()) { return Response.status(Status.FORBIDDEN).build(); } diff --git a/src/main/java/com/commafeed/frontend/resource/UserREST.java b/src/main/java/com/commafeed/frontend/resource/UserREST.java index cbc450e0..85a62d97 100644 --- a/src/main/java/com/commafeed/frontend/resource/UserREST.java +++ b/src/main/java/com/commafeed/frontend/resource/UserREST.java @@ -1,16 +1,12 @@ package com.commafeed.frontend.resource; -import io.dropwizard.hibernate.UnitOfWork; -import io.dropwizard.jersey.validation.ValidationErrorMessage; - import java.util.Arrays; -import java.util.Collections; import java.util.Date; +import java.util.Optional; import java.util.UUID; import javax.inject.Inject; import javax.inject.Singleton; -import javax.validation.ConstraintViolation; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -23,15 +19,13 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.RandomStringUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; import org.apache.http.client.utils.URIBuilder; +import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.UserDAO; @@ -56,19 +50,23 @@ import com.commafeed.frontend.model.request.PasswordResetRequest; import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.model.request.RegistrationRequest; import com.commafeed.frontend.session.SessionHelper; -import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import com.wordnik.swagger.annotations.Api; -import com.wordnik.swagger.annotations.ApiOperation; -import com.wordnik.swagger.annotations.ApiParam; + +import io.dropwizard.hibernate.UnitOfWork; +import io.dropwizard.jersey.validation.ValidationErrorMessage; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Path("/user") -@Api(value = "/user", description = "Operations about the user") +@Api(value = "/user") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class UserREST { @@ -84,6 +82,7 @@ public class UserREST { @GET @UnitOfWork @ApiOperation(value = "Retrieve user settings", notes = "Retrieve user settings", response = Settings.class) + @Timed public Response getSettings(@SecurityCheck User user) { Settings s = new Settings(); UserSettings settings = userSettingsDAO.findByUser(user); @@ -138,6 +137,7 @@ public class UserREST { @POST @UnitOfWork @ApiOperation(value = "Save user settings", notes = "Save user settings") + @Timed public Response saveSettings(@SecurityCheck User user, @ApiParam(required = true) Settings settings) { Preconditions.checkNotNull(settings); @@ -176,6 +176,7 @@ public class UserREST { @GET @UnitOfWork @ApiOperation(value = "Retrieve user's profile", response = UserModel.class) + @Timed public Response get(@SecurityCheck User user) { UserModel userModel = new UserModel(); userModel.setId(user.getId()); @@ -195,6 +196,7 @@ public class UserREST { @POST @UnitOfWork @ApiOperation(value = "Save user's profile") + @Timed public Response save(@SecurityCheck User user, @ApiParam(required = true) ProfileModificationRequest request) { Preconditions.checkArgument(StringUtils.isBlank(request.getPassword()) || request.getPassword().length() >= 6); if (StringUtils.isNotBlank(request.getEmail())) { @@ -215,7 +217,7 @@ public class UserREST { if (request.isNewApiKey()) { user.setApiKey(userService.generateApiKey(user)); } - userDAO.merge(user); + userDAO.saveOrUpdate(user); return Response.ok().build(); } @@ -223,6 +225,7 @@ public class UserREST { @POST @UnitOfWork @ApiOperation(value = "Register a new account") + @Timed public Response register(@Valid @ApiParam(required = true) RegistrationRequest req, @Context SessionHelper sessionHelper) { try { User registeredUser = userService.register(req.getName(), req.getPassword(), req.getEmail(), Arrays.asList(Role.USER)); @@ -230,12 +233,8 @@ public class UserREST { sessionHelper.setLoggedInUser(registeredUser); return Response.ok().build(); } catch (final IllegalArgumentException e) { - return Response.status(422).entity(new ValidationErrorMessage(Collections.> emptySet()) { - @Override - public ImmutableList getErrors() { - return ImmutableList.of(e.getMessage()); - } - }).build(); + return Response.status(422).entity(new ValidationErrorMessage(ImmutableList.of(e.getMessage()))).type(MediaType.TEXT_PLAIN) + .build(); } } @@ -243,13 +242,14 @@ public class UserREST { @POST @UnitOfWork @ApiOperation(value = "Login and create a session") + @Timed public Response login(@ApiParam(required = true) LoginRequest req, @Context SessionHelper sessionHelper) { Optional user = userService.login(req.getName(), req.getPassword()); if (user.isPresent()) { sessionHelper.setLoggedInUser(user.get()); return Response.ok().build(); } else { - return Response.status(Response.Status.UNAUTHORIZED).entity("wrong username or password").build(); + return Response.status(Response.Status.UNAUTHORIZED).entity("wrong username or password").type(MediaType.TEXT_PLAIN).build(); } } @@ -257,10 +257,11 @@ public class UserREST { @POST @UnitOfWork @ApiOperation(value = "send a password reset email") + @Timed public Response sendPasswordReset(@Valid PasswordResetRequest req) { User user = userDAO.findByEmail(req.getEmail()); if (user == null) { - return Response.status(Status.PRECONDITION_FAILED).entity("Email not found.").build(); + return Response.status(Status.PRECONDITION_FAILED).entity("Email not found.").type(MediaType.TEXT_PLAIN).build(); } try { user.setRecoverPasswordToken(DigestUtils.sha1Hex(UUID.randomUUID().toString())); @@ -270,16 +271,17 @@ public class UserREST { return Response.ok().build(); } catch (Exception e) { log.error(e.getMessage(), e); - return Response.status(Status.INTERNAL_SERVER_ERROR).entity("could not send email: " + e.getMessage()).build(); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("could not send email: " + e.getMessage()) + .type(MediaType.TEXT_PLAIN).build(); } } private String buildEmailContent(User user) throws Exception { String publicUrl = FeedUtils.removeTrailingSlash(config.getApplicationSettings().getPublicUrl()); publicUrl += "/rest/user/passwordResetCallback"; - return String - .format("You asked for password recovery for account '%s', follow this link to change your password. Ignore this if you didn't request a password recovery.", - user.getName(), callbackUrl(user, publicUrl)); + return String.format( + "You asked for password recovery for account '%s', follow this link to change your password. Ignore this if you didn't request a password recovery.", + user.getName(), callbackUrl(user, publicUrl)); } private String callbackUrl(User user, String publicUrl) throws Exception { @@ -291,6 +293,7 @@ public class UserREST { @GET @UnitOfWork @Produces(MediaType.TEXT_HTML) + @Timed public Response passwordRecoveryCallback(@QueryParam("email") String email, @QueryParam("token") String token) { Preconditions.checkNotNull(email); Preconditions.checkNotNull(token); @@ -326,6 +329,7 @@ public class UserREST { @POST @UnitOfWork @ApiOperation(value = "Delete the user account") + @Timed public Response delete(@SecurityCheck User user) { if (CommaFeedApplication.USERNAME_ADMIN.equals(user.getName()) || CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) { return Response.status(Status.FORBIDDEN).build(); diff --git a/src/main/java/com/commafeed/frontend/servlet/AnalyticsServlet.java b/src/main/java/com/commafeed/frontend/servlet/AnalyticsServlet.java index da51d07d..787576e6 100644 --- a/src/main/java/com/commafeed/frontend/servlet/AnalyticsServlet.java +++ b/src/main/java/com/commafeed/frontend/servlet/AnalyticsServlet.java @@ -9,7 +9,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import com.commafeed.CommaFeedConfiguration; diff --git a/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java b/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java index 7d487548..d3218653 100644 --- a/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java +++ b/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java @@ -1,6 +1,7 @@ package com.commafeed.frontend.servlet; import java.io.IOException; +import java.util.Optional; import javax.inject.Inject; import javax.inject.Singleton; @@ -9,8 +10,6 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; - import org.hibernate.SessionFactory; import com.commafeed.backend.dao.UnitOfWork; @@ -18,10 +17,11 @@ import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings; import com.commafeed.frontend.session.SessionHelper; -import com.google.common.base.Optional; + +import lombok.RequiredArgsConstructor; @SuppressWarnings("serial") -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class CustomCssServlet extends HttpServlet { @@ -32,23 +32,12 @@ public class CustomCssServlet extends HttpServlet { protected void doGet(final HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/css"); - final Optional user = new UnitOfWork>(sessionFactory) { - @Override - protected Optional runInSession() throws Exception { - return new SessionHelper(req).getLoggedInUser(); - } - }.run(); + final Optional user = new SessionHelper(req).getLoggedInUser(); if (!user.isPresent()) { return; } - UserSettings settings = new UnitOfWork(sessionFactory) { - @Override - protected UserSettings runInSession() { - return userSettingsDAO.findByUser(user.get()); - } - }.run(); - + UserSettings settings = UnitOfWork.call(sessionFactory, () -> userSettingsDAO.findByUser(user.get())); if (settings == null || settings.getCustomCss() == null) { return; } diff --git a/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java b/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java index fe743b5f..4ea60674 100644 --- a/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java +++ b/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java @@ -2,6 +2,7 @@ package com.commafeed.frontend.servlet; import java.io.IOException; import java.util.List; +import java.util.Optional; import javax.inject.Inject; import javax.inject.Singleton; @@ -10,9 +11,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; - -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.hibernate.SessionFactory; import com.commafeed.CommaFeedConfiguration; @@ -28,11 +27,12 @@ import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.service.UserService; import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.frontend.session.SessionHelper; -import com.google.common.base.Optional; import com.google.common.collect.Iterables; +import lombok.RequiredArgsConstructor; + @SuppressWarnings("serial") -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor(onConstructor = @__({ @Inject }) ) @Singleton public class NextUnreadServlet extends HttpServlet { @@ -51,17 +51,11 @@ public class NextUnreadServlet extends HttpServlet { final String categoryId = req.getParameter(PARAM_CATEGORYID); String orderParam = req.getParameter(PARAM_READINGORDER); - final Optional user = new UnitOfWork>(sessionFactory) { - @Override - protected Optional runInSession() throws Exception { - SessionHelper sessionHelper = new SessionHelper(req); - Optional loggedInUser = sessionHelper.getLoggedInUser(); - if (loggedInUser.isPresent()) { - userService.performPostLoginActivities(loggedInUser.get()); - } - return loggedInUser; - } - }.run(); + SessionHelper sessionHelper = new SessionHelper(req); + Optional user = sessionHelper.getLoggedInUser(); + if (user.isPresent()) { + UnitOfWork.run(sessionFactory, () -> userService.performPostLoginActivities(user.get())); + } if (!user.isPresent()) { resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); return; @@ -69,32 +63,29 @@ public class NextUnreadServlet extends HttpServlet { final ReadingOrder order = StringUtils.equals(orderParam, "asc") ? ReadingOrder.asc : ReadingOrder.desc; - FeedEntryStatus status = new UnitOfWork(sessionFactory) { - @Override - protected FeedEntryStatus runInSession() throws Exception { - FeedEntryStatus status = null; - if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { - List subs = feedSubscriptionDAO.findAll(user.get()); - List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, - order, true, false, null); - status = Iterables.getFirst(statuses, null); - } else { - FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId)); - if (category != null) { - List children = feedCategoryDAO.findAllChildrenCategories(user.get(), category); - List subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children); - List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, - null, 0, 1, order, true, false, null); - status = Iterables.getFirst(statuses, null); - } + FeedEntryStatus status = UnitOfWork.call(sessionFactory, () -> { + FeedEntryStatus s = null; + if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { + List subs = feedSubscriptionDAO.findAll(user.get()); + List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order, + true, false, null); + s = Iterables.getFirst(statuses, null); + } else { + FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId)); + if (category != null) { + List children = feedCategoryDAO.findAllChildrenCategories(user.get(), category); + List subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children); + List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0, + 1, order, true, false, null); + s = Iterables.getFirst(statuses, null); } - if (status != null) { - status.setRead(true); - feedEntryStatusDAO.saveOrUpdate(status); - } - return status; } - }.run(); + if (s != null) { + s.setRead(true); + feedEntryStatusDAO.saveOrUpdate(s); + } + return s; + }); if (status == null) { resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); @@ -103,5 +94,4 @@ public class NextUnreadServlet extends HttpServlet { resp.sendRedirect(resp.encodeRedirectURL(url)); } } - } diff --git a/src/main/java/com/commafeed/frontend/session/SessionHelper.java b/src/main/java/com/commafeed/frontend/session/SessionHelper.java index 7ce71ceb..0fb7f3a5 100644 --- a/src/main/java/com/commafeed/frontend/session/SessionHelper.java +++ b/src/main/java/com/commafeed/frontend/session/SessionHelper.java @@ -1,39 +1,40 @@ package com.commafeed.frontend.session; +import java.util.Optional; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import com.commafeed.backend.model.User; -import com.google.common.base.Optional; @RequiredArgsConstructor() public class SessionHelper { - + private static final String SESSION_KEY_USER = "user"; - + private final HttpServletRequest request; - + public Optional getLoggedInUser() { Optional session = getSession(false); - + if (session.isPresent()) { User user = (User) session.get().getAttribute(SESSION_KEY_USER); - return Optional.fromNullable(user); + return Optional.ofNullable(user); } - - return Optional.absent(); + + return Optional.empty(); } - + public void setLoggedInUser(User user) { Optional session = getSession(true); session.get().setAttribute(SESSION_KEY_USER, user); } - + private Optional getSession(boolean force) { HttpSession session = request.getSession(force); - return Optional.fromNullable(session); + return Optional.ofNullable(session); } } diff --git a/src/main/java/com/commafeed/frontend/session/SessionHelperFactory.java b/src/main/java/com/commafeed/frontend/session/SessionHelperFactory.java new file mode 100644 index 00000000..a6ecad6b --- /dev/null +++ b/src/main/java/com/commafeed/frontend/session/SessionHelperFactory.java @@ -0,0 +1,17 @@ +package com.commafeed.frontend.session; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; + +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; + +public class SessionHelperFactory extends AbstractContainerRequestValueFactory { + + @Context + HttpServletRequest request; + + @Override + public SessionHelper provide() { + return new SessionHelper(request); + } +} \ No newline at end of file diff --git a/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java b/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java new file mode 100644 index 00000000..7eb0cbe9 --- /dev/null +++ b/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java @@ -0,0 +1,56 @@ +package com.commafeed.frontend.session; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.core.Context; + +import org.glassfish.hk2.api.Factory; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; + +@Singleton +public class SessionHelperFactoryProvider extends AbstractValueFactoryProvider { + + @Inject + public SessionHelperFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, final ServiceLocator injector) { + super(extractorProvider, injector, Parameter.Source.CONTEXT); + } + + @Override + protected Factory createValueFactory(final Parameter parameter) { + final Class classType = parameter.getRawType(); + + Context context = parameter.getAnnotation(Context.class); + if (context == null) + return null; + + if (classType.isAssignableFrom(SessionHelper.class)) { + return new SessionHelperFactory(); + } else { + return null; + } + } + + public static class SessionHelperInjectionResolver extends ParamInjectionResolver { + public SessionHelperInjectionResolver() { + super(SessionHelperFactoryProvider.class); + } + } + + public static class Binder extends AbstractBinder { + + @Override + protected void configure() { + bind(SessionHelperFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); + bind(SessionHelperInjectionResolver.class).to(new TypeLiteral>() { + }).in(Singleton.class); + } + } +} diff --git a/src/main/java/com/commafeed/frontend/session/SessionHelperProvider.java b/src/main/java/com/commafeed/frontend/session/SessionHelperProvider.java deleted file mode 100644 index aba77faf..00000000 --- a/src/main/java/com/commafeed/frontend/session/SessionHelperProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.commafeed.frontend.session; - -import java.lang.reflect.Type; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.Context; -import javax.ws.rs.ext.Provider; - -import com.sun.jersey.core.spi.component.ComponentContext; -import com.sun.jersey.core.spi.component.ComponentScope; -import com.sun.jersey.spi.inject.Injectable; -import com.sun.jersey.spi.inject.InjectableProvider; - -@Provider -public class SessionHelperProvider implements InjectableProvider { - - private final ThreadLocal request; - - public SessionHelperProvider(@Context ThreadLocal request) { - this.request = request; - } - - @Override - public ComponentScope getScope() { - return ComponentScope.PerRequest; - } - - @Override - public Injectable getInjectable(ComponentContext ic, final Context session, Type type) { - if (type.equals(SessionHelper.class)) { - return new Injectable() { - @Override - public SessionHelper getValue() { - final HttpServletRequest req = request.get(); - if (req != null) { - return new SessionHelper(req); - } - return null; - } - }; - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/edu/uci/ics/crawler4j/url/URLCanonicalizer.java b/src/main/java/edu/uci/ics/crawler4j/url/URLCanonicalizer.java index d78502d6..aaa57daf 100644 --- a/src/main/java/edu/uci/ics/crawler4j/url/URLCanonicalizer.java +++ b/src/main/java/edu/uci/ics/crawler4j/url/URLCanonicalizer.java @@ -28,11 +28,11 @@ import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; /** - * See http://en.wikipedia.org/wiki/URL_normalization for a reference Note: some - * parts of the code are adapted from: http://stackoverflow.com/a/4057470/405418 + * See http://en.wikipedia.org/wiki/URL_normalization for a reference Note: some parts of the code are adapted from: + * http://stackoverflow.com/a/4057470/405418 * * @author Yasser Ganjisaffar */ @@ -46,7 +46,7 @@ public class URLCanonicalizer { try { URL canonicalURL = new URL(UrlResolver.resolveUrl(context == null ? "" : context, href)); - + String host = canonicalURL.getHost().toLowerCase(); if (StringUtils.isBlank(host)) { // This is an invalid Url. @@ -113,7 +113,7 @@ public class URLCanonicalizer { URL result = new URL(protocol, host, port, pathAndQueryString); return result.toExternalForm(); - + } catch (MalformedURLException ex) { return null; } catch (URISyntaxException ex) { @@ -122,8 +122,7 @@ public class URLCanonicalizer { } /** - * Takes a query string, separates the constituent name-value pairs, and - * stores them in a SortedMap ordered by lexicographical order. + * Takes a query string, separates the constituent name-value pairs, and stores them in a SortedMap ordered by lexicographical order. * * @return Null if there is no query string. */ @@ -149,7 +148,7 @@ public class URLCanonicalizer { params.put(tokens[0], ""); } break; - case 2: + case 2: params.put(tokens[0], tokens[1]); break; } @@ -169,7 +168,7 @@ public class URLCanonicalizer { return ""; } - final StringBuffer sb = new StringBuffer(100); + final StringBuilder sb = new StringBuilder(100); for (Map.Entry pair : sortedParamMap.entrySet()) { final String key = pair.getKey().toLowerCase(); if (key.equals("jsessionid") || key.equals("phpsessid") || key.equals("aspsessionid")) { @@ -188,8 +187,7 @@ public class URLCanonicalizer { } /** - * Percent-encode values according the RFC 3986. The built-in Java - * URLEncoder does not encode according to the RFC, so we make the extra + * Percent-encode values according the RFC 3986. The built-in Java URLEncoder does not encode according to the RFC, so we make the extra * replacements. * * @param string diff --git a/src/main/resources/changelogs/db.changelog-1.0.xml b/src/main/resources/changelogs/db.changelog-1.0.xml index 00f4a385..13b38032 100644 --- a/src/main/resources/changelogs/db.changelog-1.0.xml +++ b/src/main/resources/changelogs/db.changelog-1.0.xml @@ -5,6 +5,7 @@ 7:6d3ad493d25dd9c50067e804efc9ffcc + 7:896a68c1651397288c40f717ce0397b4 @@ -56,6 +57,7 @@ 7:eccd6b37116ab35ee963aa46152e1ae5 + 7:ac622ab04aec79a7e5854d25511abaef @@ -76,6 +78,7 @@ 7:93155e15f0feabe936e1de35711bf85b + 7:c52f258e54d34156208cbfd2d8547fbd @@ -104,6 +107,7 @@ 7:2d9e82da5573ac551df31a13f3bc40e5 + 7:c3cc179801e812635b53849301a1a1d1 @@ -133,6 +137,7 @@ 7:a2d83b0f7d1bf97a7553e94dd6100edf + 7:1c45f6b6a6e7583dd4c090a4a3930758 @@ -154,6 +159,7 @@ 7:a9cf194a01c16b937a897aea934f09ae + 7:6a386e0b08e98bdba9ce55e26ab90eba @@ -181,6 +187,7 @@ 7:e3a44d2e0f774dcb4efe36702c8d5f3f + 7:604b2bb0b62b7f0529e50e63c2b2cf0c @@ -221,6 +228,7 @@ 7:36e92eac052c7d2ce0ef75e3ec2cdf8d + 7:248affcafffd2243f8b0d16750e17af0 @@ -249,6 +257,7 @@ 7:6d68765b2116ba88680d69c03b3cefd2 + 7:6112f92b437b4d0ecfcdf038fd04ed2f @@ -265,6 +274,7 @@ 7:eefc98cfa1b9bbf51fa6acd7a0d49c1b + 7:abbff58b88c8cebfb4548d17730a262d @@ -287,6 +297,7 @@ + 7:750e0990a8edebd0252df7d4adc7aa7c @@ -321,6 +332,7 @@ 7:985d6607a4350e032ea345d9a2f2f0c0 + 7:722eaff49d04d43c5b26da0929d3f707 diff --git a/src/main/resources/changelogs/db.changelog-1.2.xml b/src/main/resources/changelogs/db.changelog-1.2.xml index 2139d0ce..3bbe49c6 100644 --- a/src/main/resources/changelogs/db.changelog-1.2.xml +++ b/src/main/resources/changelogs/db.changelog-1.2.xml @@ -44,6 +44,8 @@ + 7:9bf9357b47d8666dc7916f9a318138ad + 7:625f651e4c4d8e0aa9576da291baf6a4 diff --git a/src/main/resources/changelogs/db.changelog-2.1.xml b/src/main/resources/changelogs/db.changelog-2.1.xml new file mode 100644 index 00000000..df158560 --- /dev/null +++ b/src/main/resources/changelogs/db.changelog-2.1.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/main/resources/changelogs/db.changelog-2.2.xml b/src/main/resources/changelogs/db.changelog-2.2.xml new file mode 100644 index 00000000..1af324a6 --- /dev/null +++ b/src/main/resources/changelogs/db.changelog-2.2.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/main/resources/migrations.xml b/src/main/resources/migrations.xml index acc1dafa..05b0e223 100644 --- a/src/main/resources/migrations.xml +++ b/src/main/resources/migrations.xml @@ -9,5 +9,7 @@ + + \ No newline at end of file diff --git a/src/test/java/com/commafeed/backend/FixedSizeSortedSetTest.java b/src/test/java/com/commafeed/backend/FixedSizeSortedSetTest.java index 5a81d395..7af2aff8 100644 --- a/src/test/java/com/commafeed/backend/FixedSizeSortedSetTest.java +++ b/src/test/java/com/commafeed/backend/FixedSizeSortedSetTest.java @@ -2,7 +2,7 @@ package com.commafeed.backend; import java.util.Comparator; -import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang3.ObjectUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/com/commafeed/backend/feed/FeedUtilsTest.java b/src/test/java/com/commafeed/backend/feed/FeedUtilsTest.java index 488b90c3..846cd485 100644 --- a/src/test/java/com/commafeed/backend/feed/FeedUtilsTest.java +++ b/src/test/java/com/commafeed/backend/feed/FeedUtilsTest.java @@ -3,8 +3,6 @@ package com.commafeed.backend.feed; import org.junit.Assert; import org.junit.Test; -import com.commafeed.backend.feed.FeedUtils; - public class FeedUtilsTest { @Test @@ -46,13 +44,38 @@ public class FeedUtilsTest { @Test public void testToAbsoluteUrl() { String expected = "http://a.com/blog/entry/1"; + + // usual cases Assert.assertEquals(expected, FeedUtils.toAbsoluteUrl("http://a.com/blog/entry/1", "http://a.com/feed/", "http://a.com/feed/")); Assert.assertEquals(expected, FeedUtils.toAbsoluteUrl("http://a.com/blog/entry/1", "http://a.com/feed", "http://a.com/feed")); + + // relative links Assert.assertEquals(expected, FeedUtils.toAbsoluteUrl("../blog/entry/1", "http://a.com/feed/", "http://a.com/feed/")); Assert.assertEquals(expected, FeedUtils.toAbsoluteUrl("../blog/entry/1", "feed.xml", "http://a.com/feed/feed.xml")); + // root-relative links + Assert.assertEquals(expected, FeedUtils.toAbsoluteUrl("/blog/entry/1", "/feed", "http://a.com/feed")); + + // real cases + Assert.assertEquals("https://github.com/erusev/parsedown/releases/tag/1.3.0", FeedUtils.toAbsoluteUrl( + "/erusev/parsedown/releases/tag/1.3.0", "/erusev/parsedown/releases", "https://github.com/erusev/parsedown/tags.atom")); Assert.assertEquals("http://ergoemacs.org/emacs/elisp_all_about_lines.html", FeedUtils.toAbsoluteUrl("elisp_all_about_lines.html", "blog.xml", "http://ergoemacs.org/emacs/blog.xml")); } + + @Test + public void testExtractDeclaredEncoding() { + Assert.assertNull(FeedUtils.extractDeclaredEncoding("".getBytes())); + Assert.assertNull(FeedUtils.extractDeclaredEncoding("".getBytes())); + Assert.assertEquals("UTF-8", FeedUtils.extractDeclaredEncoding("".getBytes())); + Assert.assertEquals("UTF-8", FeedUtils.extractDeclaredEncoding("".getBytes())); + Assert.assertEquals("UTF-8", FeedUtils.extractDeclaredEncoding("".getBytes())); + } + + @Test + public void testReplaceHtmlEntitiesWithNumericEntities() { + String source = "T´l´phone ′"; + Assert.assertEquals("T´l´phone ′", FeedUtils.replaceHtmlEntitiesWithNumericEntities(source)); + } } diff --git a/src/test/java/com/commafeed/backend/opml/OPMLExporterTest.java b/src/test/java/com/commafeed/backend/opml/OPMLExporterTest.java new file mode 100644 index 00000000..873e5d1e --- /dev/null +++ b/src/test/java/com/commafeed/backend/opml/OPMLExporterTest.java @@ -0,0 +1,141 @@ +package com.commafeed.backend.opml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.commafeed.backend.dao.FeedCategoryDAO; +import com.commafeed.backend.dao.FeedSubscriptionDAO; +import com.commafeed.backend.model.Feed; +import com.commafeed.backend.model.FeedCategory; +import com.commafeed.backend.model.FeedSubscription; +import com.commafeed.backend.model.User; +import com.rometools.opml.feed.opml.Opml; +import com.rometools.opml.feed.opml.Outline; + +public class OPMLExporterTest { + + @Mock + private FeedCategoryDAO feedCategoryDAO; + @Mock + private FeedSubscriptionDAO feedSubscriptionDAO; + + private User user = new User(); + + private FeedCategory cat1 = new FeedCategory(); + private FeedCategory cat2 = new FeedCategory(); + + private FeedSubscription rootFeed = newFeedSubscription("rootFeed", "rootFeed.com"); + private FeedSubscription cat1Feed = newFeedSubscription("cat1Feed", "cat1Feed.com"); + private FeedSubscription cat2Feed = newFeedSubscription("cat2Feed", "cat2Feed.com"); + + private List categories = new ArrayList<>(); + private List subscriptions = new ArrayList<>(); + + @Before + public void before_each_test() { + MockitoAnnotations.initMocks(this); + + user.setName("John Doe"); + + cat1.setId(1l); + cat1.setName("cat1"); + cat1.setParent(null); + cat1.setChildren(new HashSet()); + cat1.setSubscriptions(new HashSet()); + + cat2.setId(2l); + cat2.setName("cat2"); + cat2.setParent(cat1); + cat2.setChildren(new HashSet()); + cat2.setSubscriptions(new HashSet()); + + cat1.getChildren().add(cat2); + + rootFeed.setCategory(null); + cat1Feed.setCategory(cat1); + cat2Feed.setCategory(cat2); + + cat1.getSubscriptions().add(cat1Feed); + cat2.getSubscriptions().add(cat2Feed); + + categories.add(cat1); + categories.add(cat2); + + subscriptions.add(rootFeed); + subscriptions.add(cat1Feed); + subscriptions.add(cat2Feed); + } + + private Feed newFeed(String url) { + Feed feed = new Feed(); + feed.setUrl(url); + return feed; + } + + private FeedSubscription newFeedSubscription(String title, String url) { + FeedSubscription feedSubscription = new FeedSubscription(); + feedSubscription.setTitle(title); + feedSubscription.setFeed(newFeed(url)); + return feedSubscription; + } + + @Test + public void generates_OPML_correctly() { + when(feedCategoryDAO.findAll(user)).thenReturn(categories); + when(feedSubscriptionDAO.findAll(user)).thenReturn(subscriptions); + + Opml opml = new OPMLExporter(feedCategoryDAO, feedSubscriptionDAO).export(user); + + List rootOutlines = opml.getOutlines(); + assertEquals(2, rootOutlines.size()); + assertTrue(containsCategory(rootOutlines, "cat1")); + assertTrue(containsFeed(rootOutlines, "rootFeed", "rootFeed.com")); + + Outline cat1Outline = getCategoryOutline(rootOutlines, "cat1"); + List cat1Children = cat1Outline.getChildren(); + assertEquals(2, cat1Children.size()); + assertTrue(containsCategory(cat1Children, "cat2")); + assertTrue(containsFeed(cat1Children, "cat1Feed", "cat1Feed.com")); + + Outline cat2Outline = getCategoryOutline(cat1Children, "cat2"); + List cat2Children = cat2Outline.getChildren(); + assertEquals(1, cat2Children.size()); + assertTrue(containsFeed(cat2Children, "cat2Feed", "cat2Feed.com")); + } + + private boolean containsCategory(List outlines, String category) { + for (Outline o : outlines) + if (!"rss".equals(o.getType())) + if (category.equals(o.getTitle())) + return true; + + return false; + } + + private boolean containsFeed(List outlines, String title, String url) { + for (Outline o : outlines) + if ("rss".equals(o.getType())) + if (title.equals(o.getTitle()) && o.getAttributeValue("xmlUrl").equals(url)) + return true; + + return false; + } + + private Outline getCategoryOutline(List outlines, String title) { + for (Outline o : outlines) + if (o.getTitle().equals(title)) + return o; + + return null; + } +} \ No newline at end of file diff --git a/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java b/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java new file mode 100644 index 00000000..14f75260 --- /dev/null +++ b/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java @@ -0,0 +1,52 @@ +package com.commafeed.backend.opml; + +import java.io.IOException; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.mockito.Mockito; + +import com.commafeed.backend.cache.CacheService; +import com.commafeed.backend.dao.FeedCategoryDAO; +import com.commafeed.backend.model.FeedCategory; +import com.commafeed.backend.model.User; +import com.commafeed.backend.service.FeedSubscriptionService; + +public class OPMLImporterTest { + + @Test + public void testOpmlV10() throws IOException { + testOpmlVersion("/opml/opml_v1.0.xml"); + } + + @Test + public void testOpmlV11() throws IOException { + testOpmlVersion("/opml/opml_v1.1.xml"); + } + + @Test + public void testOpmlV20() throws IOException { + testOpmlVersion("/opml/opml_v2.0.xml"); + } + + @Test + public void testOpmlNoVersion() throws IOException { + testOpmlVersion("/opml/opml_noversion.xml"); + } + + private void testOpmlVersion(String fileName) throws IOException { + FeedCategoryDAO feedCategoryDAO = Mockito.mock(FeedCategoryDAO.class); + FeedSubscriptionService feedSubscriptionService = Mockito.mock(FeedSubscriptionService.class); + CacheService cacheService = Mockito.mock(CacheService.class); + User user = Mockito.mock(User.class); + + String xml = IOUtils.toString(getClass().getResourceAsStream(fileName)); + + OPMLImporter importer = new OPMLImporter(feedCategoryDAO, feedSubscriptionService, cacheService); + importer.importOpml(user, xml); + + Mockito.verify(feedSubscriptionService).subscribe(Mockito.eq(user), Mockito.anyString(), Mockito.anyString(), + Mockito.any(FeedCategory.class), Mockito.anyInt()); + } + +} diff --git a/src/test/java/com/commafeed/backend/service/FeedEntryFilteringServiceTest.java b/src/test/java/com/commafeed/backend/service/FeedEntryFilteringServiceTest.java new file mode 100644 index 00000000..8feda310 --- /dev/null +++ b/src/test/java/com/commafeed/backend/service/FeedEntryFilteringServiceTest.java @@ -0,0 +1,85 @@ +package com.commafeed.backend.service; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.commafeed.backend.model.FeedEntry; +import com.commafeed.backend.model.FeedEntryContent; +import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; + +public class FeedEntryFilteringServiceTest { + + private FeedEntryFilteringService service; + + private FeedEntry entry; + private FeedEntryContent content; + + @Before + public void init() { + service = new FeedEntryFilteringService(); + + entry = new FeedEntry(); + entry.setUrl("https://github.com/Athou/commafeed"); + + content = new FeedEntryContent(); + content.setAuthor("Athou"); + content.setTitle("Merge pull request #662 from Athou/dw8"); + content.setContent("Merge pull request #662 from Athou/dw8"); + entry.setContent(content); + + } + + @Test + public void emptyFilterMatchesFilter() throws FeedEntryFilterException { + Assert.assertTrue(service.filterMatchesEntry(null, entry)); + } + + @Test + public void blankFilterMatchesFilter() throws FeedEntryFilterException { + Assert.assertTrue(service.filterMatchesEntry("", entry)); + } + + @Test + public void simpleExpression() throws FeedEntryFilterException { + Assert.assertTrue(service.filterMatchesEntry("author.toString() eq 'athou'", entry)); + } + + @Test(expected = FeedEntryFilterException.class) + public void newIsDisabled() throws FeedEntryFilterException { + service.filterMatchesEntry("null eq new ('java.lang.String', 'athou')", entry); + } + + @Test(expected = FeedEntryFilterException.class) + public void getClassMethodIsDisabled() throws FeedEntryFilterException { + service.filterMatchesEntry("null eq ''.getClass()", entry); + } + + @Test + public void dotClassIsDisabled() throws FeedEntryFilterException { + Assert.assertTrue(service.filterMatchesEntry("null eq ''.class", entry)); + } + + @Test(expected = FeedEntryFilterException.class) + public void cannotLoopForever() throws FeedEntryFilterException { + service.filterMatchesEntry("while(true) {}", entry); + } + + @Test + public void handlesNullCorrectly() throws FeedEntryFilterException { + entry.setUrl(null); + entry.setContent(new FeedEntryContent()); + service.filterMatchesEntry("author eq 'athou'", entry); + } + + @Test(expected = FeedEntryFilterException.class) + public void incorrectScriptThrowsException() throws FeedEntryFilterException { + service.filterMatchesEntry("aa eqz bb", entry); + } + + @Test(expected = FeedEntryFilterException.class) + public void incorrectReturnTypeThrowsException() throws FeedEntryFilterException { + service.filterMatchesEntry("1", entry); + } + +} diff --git a/src/test/java/com/commafeed/backend/service/UserServiceTest.java b/src/test/java/com/commafeed/backend/service/UserServiceTest.java index 58a19e2d..9e132df1 100644 --- a/src/test/java/com/commafeed/backend/service/UserServiceTest.java +++ b/src/test/java/com/commafeed/backend/service/UserServiceTest.java @@ -9,6 +9,8 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Optional; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -16,165 +18,177 @@ import org.mockito.MockitoAnnotations; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedCategoryDAO; +import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.UserDAO; +import com.commafeed.backend.dao.UserRoleDAO; import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.internal.PostLoginActivities; -import com.google.common.base.Optional; public class UserServiceTest { - - private static final byte[] SALT = new byte[]{1,2,3}; - private static final byte[] ENCRYPTED_PASSWORD = new byte[]{5,6,7}; - - @Mock private CommaFeedConfiguration commaFeedConfiguration; - @Mock private FeedCategoryDAO feedCategoryDAO; - @Mock private UserDAO userDAO; - @Mock private UserSettingsDAO userSettingsDAO; - @Mock private PasswordEncryptionService passwordEncryptionService; - @Mock private PostLoginActivities postLoginActivities; - + + private static final byte[] SALT = new byte[] { 1, 2, 3 }; + private static final byte[] ENCRYPTED_PASSWORD = new byte[] { 5, 6, 7 }; + + @Mock + private CommaFeedConfiguration commaFeedConfiguration; + @Mock + private FeedCategoryDAO feedCategoryDAO; + @Mock + private FeedSubscriptionDAO feedSubscriptionDAO; + @Mock + private UserDAO userDAO; + @Mock + private UserSettingsDAO userSettingsDAO; + @Mock + private UserRoleDAO userRoleDAO; + @Mock + private PasswordEncryptionService passwordEncryptionService; + @Mock + private PostLoginActivities postLoginActivities; + private User disabledUser; private User normalUser; - + private UserService userService; - @Before public void - before_each_test() { + @Before + public void before_each_test() { MockitoAnnotations.initMocks(this); - - userService = new UserService(feedCategoryDAO, userDAO, userSettingsDAO, passwordEncryptionService, commaFeedConfiguration, postLoginActivities); - + + userService = new UserService(feedCategoryDAO, feedSubscriptionDAO, userDAO, userRoleDAO, userSettingsDAO, + passwordEncryptionService, commaFeedConfiguration, postLoginActivities); + disabledUser = new User(); disabledUser.setDisabled(true); - + normalUser = new User(); normalUser.setDisabled(false); normalUser.setSalt(SALT); normalUser.setPassword(ENCRYPTED_PASSWORD); } - - @Test public void - calling_login_should_not_return_user_object_when_given_null_nameOrEmail() { + + @Test + public void calling_login_should_not_return_user_object_when_given_null_nameOrEmail() { Optional user = userService.login(null, "password"); assertFalse(user.isPresent()); } - - @Test public void - calling_login_should_not_return_user_object_when_given_null_password() { + + @Test + public void calling_login_should_not_return_user_object_when_given_null_password() { Optional user = userService.login("testusername", null); assertFalse(user.isPresent()); } - - @Test public void - calling_login_should_lookup_user_by_name() { + + @Test + public void calling_login_should_lookup_user_by_name() { userService.login("test", "password"); verify(userDAO).findByName("test"); } - - @Test public void - calling_login_should_lookup_user_by_email_if_lookup_by_name_failed() { + + @Test + public void calling_login_should_lookup_user_by_email_if_lookup_by_name_failed() { when(userDAO.findByName("test@test.com")).thenReturn(null); userService.login("test@test.com", "password"); verify(userDAO).findByEmail("test@test.com"); } - - @Test public void - calling_login_should_not_return_user_object_if_could_not_find_user_by_name_or_email() { + + @Test + public void calling_login_should_not_return_user_object_if_could_not_find_user_by_name_or_email() { when(userDAO.findByName("test@test.com")).thenReturn(null); when(userDAO.findByEmail("test@test.com")).thenReturn(null); - + Optional user = userService.login("test@test.com", "password"); - + assertFalse(user.isPresent()); } - - @Test public void - calling_login_should_not_return_user_object_if_user_is_disabled() { + + @Test + public void calling_login_should_not_return_user_object_if_user_is_disabled() { when(userDAO.findByName("test")).thenReturn(disabledUser); Optional user = userService.login("test", "password"); assertFalse(user.isPresent()); } - - @Test public void - calling_login_should_try_to_authenticate_user_who_is_not_disabled() { + + @Test + public void calling_login_should_try_to_authenticate_user_who_is_not_disabled() { when(userDAO.findByName("test")).thenReturn(normalUser); when(passwordEncryptionService.authenticate(anyString(), any(byte[].class), any(byte[].class))).thenReturn(false); - + userService.login("test", "password"); - + verify(passwordEncryptionService).authenticate("password", ENCRYPTED_PASSWORD, SALT); } - - @Test public void - calling_login_should_not_return_user_object_on_unsuccessful_authentication() { + + @Test + public void calling_login_should_not_return_user_object_on_unsuccessful_authentication() { when(userDAO.findByName("test")).thenReturn(normalUser); when(passwordEncryptionService.authenticate(anyString(), any(byte[].class), any(byte[].class))).thenReturn(false); - + Optional authenticatedUser = userService.login("test", "password"); - + assertFalse(authenticatedUser.isPresent()); } - - @Test public void - calling_login_should_execute_post_login_activities_for_user_on_successful_authentication() { + + @Test + public void calling_login_should_execute_post_login_activities_for_user_on_successful_authentication() { when(userDAO.findByName("test")).thenReturn(normalUser); when(passwordEncryptionService.authenticate(anyString(), any(byte[].class), any(byte[].class))).thenReturn(true); doNothing().when(postLoginActivities).executeFor(any(User.class)); - + userService.login("test", "password"); - + verify(postLoginActivities).executeFor(normalUser); } - - @Test public void - calling_login_should_return_user_object_on_successful_authentication() { + + @Test + public void calling_login_should_return_user_object_on_successful_authentication() { when(userDAO.findByName("test")).thenReturn(normalUser); when(passwordEncryptionService.authenticate(anyString(), any(byte[].class), any(byte[].class))).thenReturn(true); doNothing().when(postLoginActivities).executeFor(any(User.class)); - + Optional authenticatedUser = userService.login("test", "password"); - + assertTrue(authenticatedUser.isPresent()); assertEquals(normalUser, authenticatedUser.get()); } - - @Test public void - api_login_should_not_return_user_if_apikey_null() { + + @Test + public void api_login_should_not_return_user_if_apikey_null() { Optional user = userService.login(null); assertFalse(user.isPresent()); } - - @Test public void - api_login_should_lookup_user_by_apikey() { + + @Test + public void api_login_should_lookup_user_by_apikey() { when(userDAO.findByApiKey("apikey")).thenReturn(null); userService.login("apikey"); verify(userDAO).findByApiKey("apikey"); } - - @Test public void - api_login_should_not_return_user_if_user_not_found_from_lookup_by_apikey() { + + @Test + public void api_login_should_not_return_user_if_user_not_found_from_lookup_by_apikey() { when(userDAO.findByApiKey("apikey")).thenReturn(null); Optional user = userService.login("apikey"); assertFalse(user.isPresent()); } - - @Test public void - api_login_should_not_return_user_if_user_found_from_apikey_lookup_is_disabled() { + + @Test + public void api_login_should_not_return_user_if_user_found_from_apikey_lookup_is_disabled() { when(userDAO.findByApiKey("apikey")).thenReturn(disabledUser); Optional user = userService.login("apikey"); assertFalse(user.isPresent()); } - - @Test public void - api_login_should_perform_post_login_activities_if_user_found_from_apikey_lookup_not_disabled() { + + @Test + public void api_login_should_perform_post_login_activities_if_user_found_from_apikey_lookup_not_disabled() { when(userDAO.findByApiKey("apikey")).thenReturn(normalUser); userService.login("apikey"); verify(postLoginActivities).executeFor(normalUser); } - - @Test public void - api_login_should_return_user_if_user_found_from_apikey_lookup_not_disabled() { + + @Test + public void api_login_should_return_user_if_user_found_from_apikey_lookup_not_disabled() { when(userDAO.findByApiKey("apikey")).thenReturn(normalUser); Optional returnedUser = userService.login("apikey"); assertEquals(normalUser, returnedUser.get()); diff --git a/src/test/java/com/commafeed/frontend/auth/SecurityCheckInjectableTest.java b/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java similarity index 72% rename from src/test/java/com/commafeed/frontend/auth/SecurityCheckInjectableTest.java rename to src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java index 429b055d..75bfc513 100644 --- a/src/test/java/com/commafeed/frontend/auth/SecurityCheckInjectableTest.java +++ b/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java @@ -4,16 +4,16 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Optional; + import org.junit.Test; import com.commafeed.backend.model.User; import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.internal.PostLoginActivities; -import com.commafeed.frontend.auth.SecurityCheckProvider.SecurityCheckInjectable; import com.commafeed.frontend.session.SessionHelper; -import com.google.common.base.Optional; -public class SecurityCheckInjectableTest { +public class SecurityCheckFactoryTest { @Test public void cookie_login_should_perform_post_login_activities_if_user_is_logged_in() { @@ -24,10 +24,11 @@ public class SecurityCheckInjectableTest { PostLoginActivities postLoginActivities = mock(PostLoginActivities.class); - UserService service = new UserService(null, null, null, null, null, postLoginActivities); + UserService service = new UserService(null, null, null, null, null, null, null, postLoginActivities); - SecurityCheckInjectable injectable = new SecurityCheckInjectable(sessionHelper, service, null, false); - injectable.cookieSessionLogin(); + SecurityCheckFactory factory = new SecurityCheckFactory(null, false); + factory.userService = service; + factory.cookieSessionLogin(sessionHelper); verify(postLoginActivities).executeFor(userInSession); } diff --git a/src/test/java/com/commafeed/frontend/resource/UserRestTest.java b/src/test/java/com/commafeed/frontend/resource/UserRestTest.java index 0811aa1a..ec3c53d8 100644 --- a/src/test/java/com/commafeed/frontend/resource/UserRestTest.java +++ b/src/test/java/com/commafeed/frontend/resource/UserRestTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.Optional; import org.junit.Test; import org.mockito.InOrder; @@ -19,93 +20,92 @@ import com.commafeed.backend.service.UserService; import com.commafeed.frontend.model.request.LoginRequest; import com.commafeed.frontend.model.request.RegistrationRequest; import com.commafeed.frontend.session.SessionHelper; -import com.google.common.base.Optional; public class UserRestTest { - - @Test public void - login_should_not_populate_http_session_if_unsuccessfull() { + + @Test + public void login_should_not_populate_http_session_if_unsuccessfull() { // Absent user - Optional absentUser = Optional.absent(); - + Optional absentUser = Optional.empty(); + // Create UserService partial mock UserService service = mock(UserService.class); when(service.login("user", "password")).thenReturn(absentUser); - + UserREST userREST = new UserREST(null, null, null, service, null, null, null); SessionHelper sessionHelper = mock(SessionHelper.class); - + LoginRequest req = new LoginRequest(); req.setName("user"); req.setPassword("password"); - + userREST.login(req, sessionHelper); - + verify(sessionHelper, never()).setLoggedInUser(any(User.class)); } - - @Test public void - login_should_populate_http_session_if_successfull() { + + @Test + public void login_should_populate_http_session_if_successfull() { // Create a user User user = new User(); - + // Create UserService mock UserService service = mock(UserService.class); when(service.login("user", "password")).thenReturn(Optional.of(user)); - + LoginRequest req = new LoginRequest(); req.setName("user"); req.setPassword("password"); - + UserREST userREST = new UserREST(null, null, null, service, null, null, null); SessionHelper sessionHelper = mock(SessionHelper.class); - + userREST.login(req, sessionHelper); - + verify(sessionHelper).setLoggedInUser(user); } - - @Test public void - register_should_register_and_then_login() { + + @Test + public void register_should_register_and_then_login() { // Create UserService mock UserService service = mock(UserService.class); - + RegistrationRequest req = new RegistrationRequest(); req.setName("user"); req.setPassword("password"); req.setEmail("test@test.com"); - + InOrder inOrder = inOrder(service); - + SessionHelper sessionHelper = mock(SessionHelper.class); UserREST userREST = new UserREST(null, null, null, service, null, null, null); - + userREST.register(req, sessionHelper); - + inOrder.verify(service).register("user", "password", "test@test.com", Arrays.asList(Role.USER)); inOrder.verify(service).login("user", "password"); } - - @Test public void - register_should_populate_http_session() { + + @Test + public void register_should_populate_http_session() { // Create a user User user = new User(); - + // Create UserService mock UserService service = mock(UserService.class); when(service.register(any(String.class), any(String.class), any(String.class), Matchers.anyListOf(Role.class))).thenReturn(user); when(service.login(any(String.class), any(String.class))).thenReturn(Optional.of(user)); - + RegistrationRequest req = new RegistrationRequest(); req.setName("user"); req.setPassword("password"); req.setEmail("test@test.com"); - + SessionHelper sessionHelper = mock(SessionHelper.class); UserREST userREST = new UserREST(null, null, null, service, null, null, null); - + userREST.register(req, sessionHelper); - + verify(sessionHelper).setLoggedInUser(user); } diff --git a/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java b/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java index 34cb19b8..45496151 100644 --- a/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java +++ b/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java @@ -4,6 +4,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Optional; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @@ -11,7 +13,6 @@ import org.junit.Assert; import org.junit.Test; import com.commafeed.backend.model.User; -import com.google.common.base.Optional; public class SessionHelperTest { diff --git a/src/test/resources/opml/opml_noversion.xml b/src/test/resources/opml/opml_noversion.xml new file mode 100644 index 00000000..4bcdbe0e --- /dev/null +++ b/src/test/resources/opml/opml_noversion.xml @@ -0,0 +1,11 @@ + + + + subscriptions + + + + + + + diff --git a/src/test/resources/opml/opml_v1.0.xml b/src/test/resources/opml/opml_v1.0.xml new file mode 100644 index 00000000..61ac899e --- /dev/null +++ b/src/test/resources/opml/opml_v1.0.xml @@ -0,0 +1,11 @@ + + + + subscriptions + + + + + + + diff --git a/src/test/resources/opml/opml_v1.1.xml b/src/test/resources/opml/opml_v1.1.xml new file mode 100644 index 00000000..4e2bedea --- /dev/null +++ b/src/test/resources/opml/opml_v1.1.xml @@ -0,0 +1,11 @@ + + + + subscriptions + + + + + + + diff --git a/src/test/resources/opml/opml_v2.0.xml b/src/test/resources/opml/opml_v2.0.xml new file mode 100644 index 00000000..29c70d83 --- /dev/null +++ b/src/test/resources/opml/opml_v2.0.xml @@ -0,0 +1,11 @@ + + + + subscriptions + + + + + + +