Merge pull request #4 from Athou/master

pull latest commits from Athou's main branch to my fork
This commit is contained in:
Hubcapp
2015-08-19 01:52:13 -05:00
166 changed files with 3360 additions and 1765 deletions

View File

@@ -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

View File

@@ -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 &

View File

@@ -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

0
.openshift/markers/java8 Normal file
View File

View File

@@ -1,3 +1,41 @@
<settings>
<localRepository>$OPENSHIFT_DATA_DIR</localRepository>
<mirrors>
<mirror>
<id>nexus</id>
<mirrorOf>central</mirrorOf>
<url>http://mirror1.ops.rhcloud.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>nexus</id>
<repositories>
<repository>
<id>central</id>
<url>http://central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<url>http://central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>nexus</activeProfile>
</activeProfiles>
</settings>

View File

@@ -1,5 +1,3 @@
language: java
jdk:
- openjdk7
- oraclejdk7
- oraclejdk8

View File

@@ -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

View File

@@ -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/_<theme>.scss`. Your styles should be wrapped in a `#theme-<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/_<theme>.scss`. Your styles should be wrapped in a `#theme-<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.

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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:

View File

@@ -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']);
gulp.task('default', ['build']);

BIN
maven/maven-wrapper.jar Normal file

Binary file not shown.

View File

@@ -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

234
mvnw vendored Executable file
View File

@@ -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} "$@"

141
mvnw.bat Normal file
View File

@@ -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%

View File

@@ -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"
}
}

256
pom.xml
View File

@@ -4,19 +4,20 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>2.0.3</version>
<version>2.3.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>CommaFeed</name>
<prerequisites>
<maven>3.0.0</maven>
<maven>3.1.0</maven>
</prerequisites>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<dropwizard.version>0.7.1</dropwizard.version>
<guice.version>3.0</guice.version>
<querydsl.version>3.5.0</querydsl.version>
<java.version>1.8</java.version>
<dropwizard.version>0.9.0-rc2</dropwizard.version>
<guice.version>4.0</guice.version>
<querydsl.version>4.0.2</querydsl.version>
<rome.version>1.5.0</rome.version>
</properties>
@@ -34,20 +35,53 @@
<filtering>true</filtering>
</resource>
</resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<versionRange>[0.0.22,)</versionRange>
<goals>
<goal>npm</goal>
<goal>gulp</goal>
<goal>bower</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute>
<runOnIncremental>false</runOnIncremental>
</execute>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.1.11</version>
<version>2.1.13</version>
<executions>
<execution>
<goals>
@@ -95,20 +129,50 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.kongchen</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<apiSources>
<apiSource>
<locations>com.commafeed.frontend.resource;com.commafeed.frontend.model;com.commafeed.frontend.model.request</locations>
<swaggerDirectory>target/swagger</swaggerDirectory>
<basePath>/rest</basePath>
<info>
<title>CommaFeed</title>
<version>${project.version}</version>
</info>
<!-- TODO uncomment when 3.1.1 is released -->
<!-- <typesToSkip> -->
<!-- <typeToSkip>com.commafeed.backend.model.User</typeToSkip> -->
<!-- </typesToSkip> -->
</apiSource>
</apiSources>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>0.0.16</version>
<version>0.0.24</version>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<phase>generate-resources</phase>
<phase>compile</phase>
<configuration>
<nodeVersion>v0.10.30</nodeVersion>
<npmVersion>1.3.8</npmVersion>
<nodeVersion>v0.10.39</nodeVersion>
<npmVersion>2.12.1</npmVersion>
</configuration>
</execution>
<execution>
@@ -116,21 +180,31 @@
<goals>
<goal>npm</goal>
</goals>
<phase>generate-resources</phase>
<phase>compile</phase>
</execution>
<execution>
<id>bower install</id>
<goals>
<goal>bower</goal>
</goals>
<phase>compile</phase>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>gulp build</id>
<goals>
<goal>gulp</goal>
</goals>
<phase>generate-resources</phase>
<phase>compile</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.5</version>
<version>2.6</version>
<configuration>
<archive>
<manifest>
@@ -139,29 +213,6 @@
</archive>
</configuration>
</plugin>
<plugin>
<groupId>com.jamierf.dropwizard</groupId>
<artifactId>dropwizard-debpkg-maven-plugin</artifactId>
<version>0.7</version>
<configuration>
<configTemplate>${basedir}/config.yml.example</configTemplate>
<jvm>
<packageName>openjdk-7-jdk</packageName>
<server>true</server>
</jvm>
<unix>
<user>commafeed</user>
</unix>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>dwpackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -169,13 +220,13 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.14.8</version>
<version>1.16.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.7</version>
<version>1.7.12</version>
</dependency>
<dependency>
@@ -193,17 +244,22 @@
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>${dropwizard.version}</version>
<exclusions>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>aopalliance-repackaged</artifactId>
</exclusion>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-hibernate</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-client</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
@@ -218,34 +274,50 @@
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-forms</artifactId>
<version>${dropwizard.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.wordnik</groupId>
<artifactId>swagger-jaxrs_2.10</artifactId>
<version>1.3.10</version>
<exclusions>
<exclusion>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.mysema.querydsl</groupId>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
<classifier>hibernate</classifier>
</dependency>
<dependency>
<groupId>com.mysema.querydsl</groupId>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
@@ -259,65 +331,101 @@
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
<version>1.10</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.3</version>
<version>3.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jexl</artifactId>
<version>2.1.1</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.6.0</version>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.5.2</version>
<version>1.5.3</version>
</dependency>
<!-- upgrade jdom to 2.0.5 for performance reasons (https://github.com/hunterhacker/jdom/issues/112) -->
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>${rome.version}</version>
<exclusions>
<exclusion>
<artifactId>jdom</artifactId>
<groupId>org.jdom</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome-opml</artifactId>
<version>${rome.version}</version>
</dependency>
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom2</artifactId>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.8.1</version>
<version>1.8.2</version>
</dependency>
<dependency>
<groupId>com.googlecode.juniversalchardet</groupId>
<artifactId>juniversalchardet</artifactId>
<version>1.0.3</version>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>55.1</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cssparser</groupId>
<artifactId>cssparser</artifactId>
<version>0.9.14</version>
<version>0.9.16</version>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-youtube</artifactId>
<version>v3-rev139-1.20.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava-jdk5</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.182</version>
<version>1.4.187</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.33</version>
<version>5.1.35</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.3-1102-jdbc41</version>
<version>9.4-1201-jdbc41</version>
</dependency>
<dependency>
<groupId>net.sourceforge.jtds</groupId>
@@ -328,13 +436,13 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.8</version>
<version>2.0.11-beta</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -2,7 +2,7 @@
<html>
<head>
<title>Swagger UI</title>
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'/>
<link href='css/typography.css' media='screen' rel='stylesheet' type='text/css'/>
<link href='css/reset.css' media='screen' rel='stylesheet' type='text/css'/>
<link href='css/screen.css' media='screen' rel='stylesheet' type='text/css'/>
<link href='css/reset.css' media='print' rel='stylesheet' type='text/css'/>
@@ -12,25 +12,23 @@
<script src='lib/jquery.slideto.min.js' type='text/javascript'></script>
<script src='lib/jquery.wiggle.min.js' type='text/javascript'></script>
<script src='lib/jquery.ba-bbq.min.js' type='text/javascript'></script>
<script src='lib/handlebars-1.0.0.js' type='text/javascript'></script>
<script src='lib/handlebars-2.0.0.js' type='text/javascript'></script>
<script src='lib/underscore-min.js' type='text/javascript'></script>
<script src='lib/backbone-min.js' type='text/javascript'></script>
<script src='lib/swagger.js' type='text/javascript'></script>
<script src='lib/swagger-client.js' type='text/javascript'></script>
<script src='swagger-ui.js' type='text/javascript'></script>
<script src='lib/highlight.7.3.pack.js' type='text/javascript'></script>
<script src='lib/marked.js' type='text/javascript'></script>
<!-- enabling this will enable oauth2 implicit scope support -->
<script src='lib/swagger-oauth.js' type='text/javascript'></script>
<script type="text/javascript">
$(function () {
window.swaggerUi = new SwaggerUi({
url: "../rest/api-docs",
url: "./swagger.json",
dom_id: "swagger-ui-container",
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
onComplete: function(swaggerApi, swaggerUi){
log("Loaded SwaggerUI");
if(typeof initOAuth == "function") {
/*
initOAuth({

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -98,6 +98,8 @@
"next_refresh" : "Next refresh",
"queued_for_refresh" : "Queued for refresh",
"feed_url" : "Feed URL",
"filtering_expression" : "Filtering expression",
"filtering_expression_help" : "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically.\nAvailable variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison.\nExample: url.contains('youtube') or (author eq 'athou' and title.contains('github').\nComplete available syntax is available <a href='http://commons.apache.org/proper/commons-jexl/reference/syntax.html' target='_blank'>here</a>.",
"generate_api_key_first" : "Generate an API key in your profile first.",
"unsubscribe" : "Unsubscribe",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed?",

View File

@@ -31,7 +31,7 @@
},
"new_category" : {
"name" : "Nome",
"parent" : "Pai"
"parent" : "Subcategoría de "
},
"toolbar" : {
"unread" : "Sen Ler",
@@ -39,12 +39,12 @@
"previous_entry" : "Entrada Anterior",
"next_entry" : "Próxima Entrada",
"refresh" : "Actualizar",
"refresh_all" : "Force refresh all my feeds ",
"refresh_all" : "Forzar a actualización de todas as fontes ",
"sort_by_asc_desc" : "Ordenar por data asc/desc",
"titles_only" : "Só títulos",
"expanded_view" : "Vista expandida",
"mark_all_as_read" : "Marcar todos como lidos",
"mark_all_older_12_hours" : "Items older than 12 hours ",
"mark_all_older_12_hours" : "Elementos anteriores a 12 h. ",
"mark_all_older_day" : "Artigos anteriores a un día",
"mark_all_older_week" : "Artigos de máis de unha semana",
"mark_all_older_two_weeks" : "Artigos de máis de dúas semanas",
@@ -56,14 +56,14 @@
"donate" : "Doar"
},
"view" : {
"entry_source" : "from ",
"entry_author" : "by ",
"entry_source" : "desde ",
"entry_author" : "por ",
"error_while_loading_feed" : "Erro mentras se cargaba esta fonte",
"keep_unread" : "Gardar non lidos",
"no_unread_items" : "non ten elementos sen ler.",
"mark_up_to_here" : "Mark as read up to here ",
"search_for" : "searching for: ",
"no_search_results" : "No match found for the requested keywords "
"mark_up_to_here" : "Marcar como lidos ate aquí ",
"search_for" : "buscando por: ",
"no_search_results" : "Sen coincidencias para as palabras introducidas "
},
"feedsearch" : {
"hint" : "Escriba unha suscrición...",
@@ -80,8 +80,8 @@
"scroll_marks" : "En vista expandida, o desplazamento polas entradas márcaas como lidas."
},
"appearance" : "Aspecto",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ",
"scroll_speed_help" : "set to 0 to disable ",
"scroll_speed" : "Velocidade de desplazamento navegando entre entradas (en milisegundos) ",
"scroll_speed_help" : "escriba 0 para deshabilitar ",
"theme" : "Decorado",
"submit_your_theme" : "Envíe o seu decorado",
"custom_css" : "CSS Personalizado"
@@ -89,21 +89,21 @@
"details" : {
"feed_details" : "Detalles de fontes",
"url" : "URL",
"website" : "Website ",
"website" : "Sitio web ",
"name" : "Nome",
"category" : "Categoría",
"position" : "Position ",
"position" : "Posición ",
"last_refresh" : "Última actualización",
"message" : "Last refresh message ",
"message" : "Última mensaxe da actualización ",
"next_refresh" : "Próxima actualización",
"queued_for_refresh" : "En cola para actualizar",
"feed_url" : "URL da fonte",
"generate_api_key_first" : "Antes debes xerar unha chave API no teu perfil.",
"unsubscribe" : "Rematar suscripción",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ",
"delete_category_confirmation" : "Are you sure you want to delete this category? ",
"unsubscribe_confirmation" : "Seguro que queres desuscribirte de esta fonte? ",
"delete_category_confirmation" : "Seguro que queres eliminar esta categoría? ",
"category_details" : "Detalles da categoría",
"tag_details" : "Tag details ",
"tag_details" : "Detalles da etiqueta ",
"parent_category" : "Categoría principal"
},
"profile" : {
@@ -119,7 +119,7 @@
"generate_new_api_key_info" : "Ao cambiar o contrasinal xerarase unha nova chave API",
"opml_export" : "Exportación de OPML",
"delete_account" : "Eliminar conta",
"delete_account_confirmation" : "Delete your acount? There's no turning back! "
"delete_account_confirmation" : "Eliminar conta? Non hai volta atrás! "
},
"about" : {
"rest_api" : {
@@ -158,7 +158,7 @@
"open_next_entry" : "abrir próxima entrada",
"open_previous_entry" : "abrir entrada anterior",
"spacebar" : "space/shift+space ",
"move_page_down_up" : "moves the page down/up ",
"move_page_down_up" : "move a páxina arriba/abaixo ",
"focus_next_entry" : "Establecer o foco na próxima entrada sen abrila",
"focus_previous_entry" : "Establecer o foco na entrada anterior sen abrila",
"open_next_feed" : "abrir a seguinte fonte ou categoría",
@@ -170,11 +170,11 @@
"mark_current_entry" : "marcar como lida/non lida a entrada actual",
"mark_all_as_read" : "marcar todas as entradas como lidas",
"open_in_new_tab_mark_as_read" : "abrir entrada nunha nova lapela e marcar como lida",
"fullscreen" : "toggle full screen mode ",
"font_size" : "increase/decrease font size of the current entry ",
"go_to_all" : "go to the All view ",
"go_to_starred" : "go to the Starred view ",
"fullscreen" : "habilita a pantalla completa ",
"font_size" : "aumenta/diminúe o tamaño da letra da entrada activa ",
"go_to_all" : "ir a vista TODOS",
"go_to_starred" : "ir a vista Destacados ",
"feed_search" : "navegue ate unha suscrición introducindo o nome da suscrición"
}
}
}
}

View File

@@ -3,15 +3,15 @@
"save" : "Salva",
"cancel" : "Cancella",
"delete" : "Elimina",
"required" : "Required",
"required" : "Richiesto",
"download" : "Download",
"link" : "Link",
"bookmark" : "Segnalibro ",
"bookmark" : "Segnalibro",
"close" : "Chiudi",
"tags" : "Tags "
"tags" : "Etichette "
},
"tree" : {
"subscribe" : "Iscriviti",
"subscribe" : "Abbonati",
"import" : "Importa",
"new_category" : "Nuova categoria",
"all" : "Tutto",
@@ -19,19 +19,19 @@
},
"subscribe" : {
"feed_url" : "Feed URL",
"feed_name" : "Nome Feed",
"feed_name" : "Nome feed",
"category" : "Categoria"
},
"import" : {
"google_reader_prefix" : "Importa i tuoi feed dal tuo ",
"google_reader_prefix" : "Permettimi di importare i tuoi feed dal tuo ",
"google_reader_suffix" : " account.",
"google_download" : "Oppure, carica il tuo subscriptions.xml",
"google_download_link" : "Scaricalo da qui",
"google_download" : "Oppure, carica il tuo file subscriptions.xml.",
"google_download_link" : "Scaricalo da qui.",
"xml_file" : "OPML File"
},
"new_category" : {
"name" : "Nome",
"parent" : "Parent"
"parent" : "Gruppo"
},
"toolbar" : {
"unread" : "Non letti",
@@ -39,12 +39,12 @@
"previous_entry" : "Precedente",
"next_entry" : "Successivo",
"refresh" : "Ricarica",
"refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "Sort by date asc/desc",
"titles_only" : "Solo titoli",
"refresh_all" : "Forza l'aggiornamento di tutte i miei feed",
"sort_by_asc_desc" : "Ordina per data ascendente/decrescente",
"titles_only" : "Solo i titoli",
"expanded_view" : "Espandi",
"mark_all_as_read" : "Contrassegna tutto come già letto",
"mark_all_older_12_hours" : "Items older than 12 hours ",
"mark_all_as_read" : "Segna tutto come già letto",
"mark_all_older_12_hours" : "Elementi più vecchi di 12 ore",
"mark_all_older_day" : "Elementi più vecchi di un giorno",
"mark_all_older_week" : "Elementi più vecchi di una settimana",
"mark_all_older_two_weeks" : "Elementi più vecchi di due settimane",
@@ -52,129 +52,131 @@
"profile" : "Profilo",
"admin" : "Admin",
"about" : "Informazioni",
"logout" : "Logout",
"logout" : "Esci",
"donate" : "Dona"
},
"view" : {
"entry_source" : "from ",
"entry_author" : "by ",
"error_while_loading_feed" : "Si è verificato un errore, mentre caricavo il feed",
"keep_unread" : "Mantiene come da leggere",
"entry_source" : "da ",
"entry_author" : "di ",
"error_while_loading_feed" : "Si è verificato un errore durante il caricamento di questo feed",
"keep_unread" : "Mantiene come non leggere",
"no_unread_items" : "Non ci sono elementi da leggere.",
"mark_up_to_here" : "Mark as read up to here ",
"search_for" : "searching for: ",
"no_search_results" : "No match found for the requested keywords "
"mark_up_to_here" : "Segna come letto fino qui",
"search_for" : "cercando: ",
"no_search_results" : "Nessun risultato trovato per le parole chiave cercate"
},
"feedsearch" : {
"hint" : "Type in a subscription... ",
"help" : "Use the return key to select and arrow keys to navigate. ",
"result_prefix" : "Le tue sottoscrizioni"
"hint" : "Digita in una sottoscrizione... ",
"help" : "Usa il tasto invio per selezionare e le frecce per navigare.",
"result_prefix" : "Le tue sottoscrizioni:"
},
"settings" : {
"general" : {
"value" : "Generali",
"language" : "Lingua",
"language_contribute" : "Contribuisci con le traduzioni",
"show_unread" : "Show feeds and categories with no unread entries",
"social_buttons" : "Visualizza i social button",
"scroll_marks" : "Marca come letto quando scorri"
"language_contribute" : "Contribuisci nelle traduzioni",
"show_unread" : "Mostra i feed e le categorie con elementi non letti",
"social_buttons" : "Mostra i pulsanti social network di condivisione",
"scroll_marks" : "In modalità estesa, segna come letto le voci quando scorri"
},
"appearance" : "Appearance ",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ",
"scroll_speed_help" : "set to 0 to disable ",
"theme" : "Tema ",
"submit_your_theme" : "Sottoponi il tuo tema ",
"custom_css" : "Css modificato"
"appearance" : "Aspetto",
"scroll_speed" : "Velocità dello scorrimento durante la navigazione tra i feed (in millisecondi) ",
"scroll_speed_help" : "Imposta 0 per disabilitare",
"theme" : "Tema",
"submit_your_theme" : "Proponi il tuo tema",
"custom_css" : "CSS personalizzato"
},
"details" : {
"feed_details" : "Dettagli feed",
"url" : "URL",
"website" : "Website ",
"url" : "URL ",
"website" : "Sito Web",
"name" : "Nome",
"category" : "Categoria",
"position" : "Posizione ",
"position" : "Posizione",
"last_refresh" : "Ultimo aggiornamento",
"message" : "Last refresh message ",
"next_refresh" : "Next refresh ",
"queued_for_refresh" : "In attesa per l'aggiornamento ",
"feed_url" : "Feed URL",
"generate_api_key_first" : "Generate an API key in your profile first.",
"unsubscribe" : "Annulla l\"'\"iscrizione",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ",
"delete_category_confirmation" : "Are you sure you want to delete this category? ",
"message" : "Ultimo messaggio di aggiornamento",
"next_refresh" : "Prossimo aggiornamento",
"queued_for_refresh" : "In attesa per l'aggiornamento",
"feed_url" : "URL del feed ",
"filtering_expression" : "Espressione del filtro",
"filtering_expression_help" : "Se non è vuoto, una espressione viene misurata in 'true' o 'false'. Se falsa, i nuovi elementi di questo feed verranno segnati automaticamente come letti.\nLe variabili accettate sono 'title', 'content', 'url' 'author' e 'categories' e il loro contenuto è convertito in minuscolo per una facile confronto di stringhe.\Esempio: url.contains('youtube') o (author eq 'athou' and title.contains('github').\nLa sintassi completa è disponibile <a href='http://commons.apache.org/proper/commons-jexl/reference/syntax.html' target='_blank'>qui</a>.",
"generate_api_key_first" : "Genera prima una chiave API nelle impostazioni del tuo profilo.",
"unsubscribe" : "Annulla la sottoscrizione",
"unsubscribe_confirmation" : "Sei sicuro di voler annullare la sottoscrizione da questo feed?",
"delete_category_confirmation" : "Sei sicuro di voler eliminare questa categoria?",
"category_details" : "Dettagli categoria",
"tag_details" : "Tag details ",
"parent_category" : "Parent category"
"tag_details" : "Dettagli etichette ",
"parent_category" : "Categoria principale"
},
"profile" : {
"user_name" : "User name",
"user_name" : "Nome utente",
"email" : "E-mail",
"change_password" : "Cambia password",
"confirm_password" : "Conferma password",
"minimum_6_chars" : "Minimo 6 caratteri",
"passwords_do_not_match" : "Le password non corrispondono",
"api_key" : "API key",
"api_key_not_generated" : "Non generata ancora",
"generate_new_api_key" : "Genera una nuova chiave API",
"generate_new_api_key_info" : "Cambiando la password sarà generata una nuova chiave API",
"api_key" : "chiave API",
"api_key_not_generated" : "Non ancora generata",
"generate_new_api_key" : "Genera una nuova chiave API ",
"generate_new_api_key_info" : "Cambiando la password sarà generata una nuova chiave API ì",
"opml_export" : "Esporta OPML",
"delete_account" : "Elimina account",
"delete_account_confirmation" : "Delete your acount? There's no turning back! "
"delete_account" : "Elimina il profilo",
"delete_account_confirmation" : "Eliminare il tuo profilo? Non si può tornare indietro!"
},
"about" : {
"rest_api" : {
"value" : "REST API",
"line1" : "CommaFeed is built on top of JAX-RS and AngularJS. As such, a REST API is available.",
"link_to_documentation" : "Link alla documentazione."
"line1" : "CommaFeed è costruito sopra JAX-RS e AngularJS. Ed ovviamente, una REST API è disponibile.",
"link_to_documentation" : "Collegamento alla documentazione."
},
"keyboard_shortcuts" : "Scorciatoie da tastiera",
"version" : "CommaFeed version ",
"line1_prefix" : "Commefeed è un progetto open-source. I codici sono hostati su ",
"keyboard_shortcuts" : "Scorciatoie da tastiera",
"version" : "Versione di CommaFeed",
"line1_prefix" : "CommaFeed è un progetto open source. I codici sono ospitati su ",
"line1_suffix" : ".",
"line2_prefix" : "Se hai qualche problema, segnalalo sulla pagina del ",
"line2_suffix" : " progetto.",
"line3" : "Se ti piace il progetto, prendi in considerazione una donazione per supportare lo sviluppatore e contribuire a coprire i costi di mantenenimento di questo sito on-line.",
"line4" : "Per chi preferisce Bitcoin, questo è l\"'\"indirizzo ",
"line3" : "Se ti piace il progetto, considera una donazione per supportare lo sviluppatore ed a aiutare per coprire i costi di mantenenimento di questo sito online.",
"line4" : "Se preferisci i Bitcoin, questo è l'indirizzo",
"goodies" : {
"value" : "Goodies",
"android_app" : "Android app ",
"subscribe_url" : "Subscribe URL ",
"chrome_extension" : "Estenzione per Chrome ",
"android_app" : "Applicazione Android",
"subscribe_url" : "Sottoscrivi URL",
"chrome_extension" : "Estensione per Chrome",
"firefox_extension" : "Estensione per Firefox",
"opera_extension" : "Estensione per Opera",
"subscribe_bookmarklet" : "Add subscription bookmarklet (click) ",
"subscribe_bookmarklet_asc" : "Oldest first ",
"subscribe_bookmarklet_desc" : "Newest first ",
"next_unread_bookmarklet" : "Next unread item bookmarklet (drag to bookmark bar) "
"subscribe_bookmarklet" : "Aggiungi la sottoscrizione ai segnalibri (clicca)",
"subscribe_bookmarklet_asc" : "I più vecchi prima",
"subscribe_bookmarklet_desc" : "I più nuovi prima",
"next_unread_bookmarklet" : "Prossimo elemento non letto nei segnalibri (trascinali nella barra dei segnalibri)"
},
"translation" : {
"value" : "Traduzioni",
"message" : "Abbiamo bisogno del tuo aiuto per tradurre CommaFeed.",
"link" : "Vedi come aiutare con le traduzioni."
"link" : "Vedi come aiutarci nella traduzioni."
},
"announcements" : "Annunci",
"shortcuts" : {
"mouse_middleclick" : "mouse middleclick",
"open_next_entry" : "open next entry",
"open_previous_entry" : "open previous entry",
"spacebar" : "space/shift+space ",
"move_page_down_up" : "moves the page down/up ",
"focus_next_entry" : "set focus on next entry without opening it ",
"focus_previous_entry" : "set focus on previous entry without opening it ",
"open_next_feed" : "open next feed or category ",
"open_previous_feed" : "open previous feed or category ",
"open_close_current_entry" : "open/close current entry",
"open_current_entry_in_new_window" : "open current entry in a new window",
"open_current_entry_in_new_window_background" : "open current entry in a new window in the background ",
"star_unstar" : "star/unstar current entry",
"mark_current_entry" : "mark as read/unread current entry",
"mark_all_as_read" : "mark all entries as read",
"open_in_new_tab_mark_as_read" : "open entry in new tab and mark as read",
"fullscreen" : "toggle full screen mode ",
"font_size" : "increase/decrease font size of the current entry ",
"go_to_all" : "go to the All view ",
"go_to_starred" : "go to the Starred view ",
"feed_search" : "navigate to a subscription by entering the subscription name "
"mouse_middleclick" : "click centrale del mouse",
"open_next_entry" : "apri l'elemento successivo",
"open_previous_entry" : "apri l'elemento precedente",
"spacebar" : "spazio/shift+spazio",
"move_page_down_up" : "muovi la pagina sopra/sotto",
"focus_next_entry" : "imposta il fuoco sull'elemento successivo senza aprirlo",
"focus_previous_entry" : "imposta il fuoco sull'elemento precedente senza aprirlo",
"open_next_feed" : "apri il feed successivo od una categoria",
"open_previous_feed" : "apri il feed precedente od una categoria",
"open_close_current_entry" : "apri/chiusi la categoria corrente",
"open_current_entry_in_new_window" : "apri il corrente elemento in una nuova finestra",
"open_current_entry_in_new_window_background" : "apri il corrente elemento in una nuova finestra in secondo piano",
"star_unstar" : "segna/togli il segno all'elemento corrente",
"mark_current_entry" : "segna come letto/non letto l'elemento corrente",
"mark_all_as_read" : "segna come letti tutti gli elementi",
"open_in_new_tab_mark_as_read" : "apri l'elemento in una nuova finestra e segnala come letta",
"fullscreen" : "alterna la modalità a schermo intero",
"font_size" : "aumenta/decrementa la grandezza del font dell'elemento corrente",
"go_to_all" : "vai nella visione totale",
"go_to_starred" : "vai nella visione dei preferiti",
"feed_search" : "naviga in una sottoscrizione scrivendo il suo nome"
}
}
}
}

View File

@@ -8,7 +8,7 @@
"link" : "Bağlantı",
"bookmark" : "Yer imi",
"close" : "Kapat",
"tags" : "Tags "
"tags" : "Etiketler "
},
"tree" : {
"subscribe" : "Abone ol",
@@ -24,7 +24,7 @@
},
"import" : {
"google_reader_prefix" : "Aboneliklerinizi ",
"google_reader_suffix" : " hesabınızdan aktarmama izin verin.",
"google_reader_suffix" : "Hesabınızdan aktarmama izin verin.",
"google_download" : "Veya, subscriptions.xml dosyanızı yükleyin.",
"google_download_link" : "Buradan indirebilirsiniz.",
"xml_file" : "OPML dosyası"
@@ -39,15 +39,15 @@
"previous_entry" : "Önceki ileti",
"next_entry" : "Sonraki ileti",
"refresh" : "Yenile",
"refresh_all" : "Force refresh all my feeds ",
"refresh_all" : "Tüm yayınları yenilemek için zorla",
"sort_by_asc_desc" : "Tarihe göre sırala artan/azalan",
"titles_only" : "Sadece başlıklar",
"expanded_view" : "Genişletilmiş görünüm",
"mark_all_as_read" : "Tümünü okundu işaretle",
"mark_all_older_12_hours" : "Items older than 12 hours ",
"mark_all_older_day" : "Bir günden eski yazılar",
"mark_all_older_week" : "Bir haftadan eski yazılar",
"mark_all_older_two_weeks" : "İki haftadan eski yazılar",
"mark_all_older_12_hours" : "12 saatten daha eski yayınlar ",
"mark_all_older_day" : "Bir günden eski yayınlar",
"mark_all_older_week" : "Bir haftadan eski yayınlar",
"mark_all_older_two_weeks" : "İki haftadan eski yayınlar",
"settings" : "Ayarlar",
"profile" : "Profil",
"admin" : "Yönetim",
@@ -56,14 +56,14 @@
"donate" : "Bağış"
},
"view" : {
"entry_source" : "from ",
"entry_author" : "by ",
"entry_source" : "kaynak: ",
"entry_author" : "yazar: ",
"error_while_loading_feed" : "Bu aboneliği çekerken hata oluştu.",
"keep_unread" : "Okunmadı olarak sakla",
"no_unread_items" : "okunmamış ileti yok.",
"mark_up_to_here" : "Mark as read up to here ",
"no_unread_items" : "Okunmamış ileti yok.",
"mark_up_to_here" : "Buraya kadar olan bütün yayınları okundu olarak işaretle!",
"search_for" : "searching for: ",
"no_search_results" : "No match found for the requested keywords "
"no_search_results" : "İstenen anahtar kelimeler için eşleşme bulunamadı"
},
"feedsearch" : {
"hint" : "Bir abonelik yazın...",
@@ -80,8 +80,8 @@
"scroll_marks" : "Genişletilmiş görünümde götüntülenen iletileri okunmuş işaretle"
},
"appearance" : "Görünüm",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ",
"scroll_speed_help" : "set to 0 to disable ",
"scroll_speed" : "İçerikler arasında gezinirken kaydırma hızı (milisaniye cinsinden)",
"scroll_speed_help" : "ayarı kapatmak için 0 yazınız",
"theme" : "Tema",
"submit_your_theme" : "Tema gönder",
"custom_css" : "Kişiselleştirilmiş CSS"
@@ -100,10 +100,10 @@
"feed_url" : "Yayın URL'si",
"generate_api_key_first" : "Öncelikle profilinizden bir API anahtarı oluşturun.",
"unsubscribe" : "Aboneliği iptal et",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ",
"delete_category_confirmation" : "Are you sure you want to delete this category? ",
"unsubscribe_confirmation" : "Bu yayından çıkmak istediğinizden emin misiniz? ",
"delete_category_confirmation" : "Bu kategoriyi silmek istediğinizden emin misiniz? ",
"category_details" : "Kategori detayları",
"tag_details" : "Tag details ",
"tag_details" : "Etiket detayları ",
"parent_category" : "Üst kategori"
},
"profile" : {
@@ -116,10 +116,10 @@
"api_key" : "API anahtarı",
"api_key_not_generated" : "Henüz oluşturulmadı",
"generate_new_api_key" : "Yeni bir API anahtarı oluştur",
"generate_new_api_key_info" : "Şifre değiştirmek API anahtarının da değiştirilmesine neden olcak.",
"generate_new_api_key_info" : "Şifreyi değiştirmek API anahtarının da değiştirilmesine neden olcak.",
"opml_export" : "OPML dışa aktar",
"delete_account" : "Hesabı sil",
"delete_account_confirmation" : "Delete your acount? There's no turning back! "
"delete_account_confirmation" : "Hesabı silmek istediğinize emin misiniz? Bu işlemde geri dönüş yoktur! "
},
"about" : {
"rest_api" : {
@@ -128,24 +128,24 @@
"link_to_documentation" : "Dökümantasyon için tıklayın."
},
"keyboard_shortcuts" : "Klavye kısayolları",
"version" : "CommaFeed version ",
"version" : "CommaFeed versiyon ",
"line1_prefix" : "CommaFeed bir açık kaynak projedir. Kaynak dosyaları ",
"line1_suffix" : " adresinde yayınlanır.",
"line1_suffix" : "adresinde yayınlanır.",
"line2_prefix" : "Lütfen, bir hata ile karşılaşırsanız bunu ",
"line2_suffix" : " projesinde hatalar sayfasından rapor edin.",
"line2_suffix" : "projesinde hatalar sayfasından rapor edin.",
"line3" : "Eğer bu projeyi beğendiyseniz, lütfen bağış yaparak geliştiriciye bu sayfayı ayakta tutmasında yardımcı olun.",
"line4" : "Bitcoin'i tercih edenler için adres ",
"goodies" : {
"value" : "Extralar",
"android_app" : "Android app ",
"value" : "Ekstralar",
"android_app" : "Android eklentisi",
"subscribe_url" : "Abonelik URL'si",
"chrome_extension" : "Chrome eklentisi",
"firefox_extension" : "Firefox eklentisi",
"opera_extension" : "Opera eklentisi",
"subscribe_bookmarklet" : "Bookmarklet'a abonelik ekle (tıklayın)",
"subscribe_bookmarklet_asc" : "Oldest first ",
"subscribe_bookmarklet_desc" : "Newest first ",
"next_unread_bookmarklet" : "Bookmarklet'daki en son okunmamış ileti (Sık kullanılan çubuğuna sürükleyin)"
"subscribe_bookmarklet" : "Yer imilerine abonelik ekle (tıklayın)",
"subscribe_bookmarklet_asc" : "Eskiler önce",
"subscribe_bookmarklet_desc" : "Yeniler önce ",
"next_unread_bookmarklet" : "Yer imilerindeki en son okunmamış ileti (Sık kullanılan çubuğuna sürükleyin)"
},
"translation" : {
"value" : "Çeviri",
@@ -158,7 +158,7 @@
"open_next_entry" : "sonraki öğeyi görüntüle",
"open_previous_entry" : "önceki öğeyi görüntüle",
"spacebar" : "space/shift+space ",
"move_page_down_up" : "moves the page down/up ",
"move_page_down_up" : "sayfayı aşağı/yukarı hareket ettir",
"focus_next_entry" : "sonraki öğeyi görüntülemeden işaretle",
"focus_previous_entry" : "önceki öğeyi görüntülemeden işaretle",
"open_next_feed" : "sonraki aboneliği veya kategoriyi görüntüle",
@@ -170,11 +170,11 @@
"mark_current_entry" : "görüntülenen öğeyi okundu/okunmadı işaretle",
"mark_all_as_read" : "tümünü okundu işaretle",
"open_in_new_tab_mark_as_read" : "öğeyi yeni bir sekmede aç ve okundu işaretle",
"fullscreen" : "toggle full screen mode ",
"font_size" : "increase/decrease font size of the current entry ",
"go_to_all" : "go to the All view ",
"go_to_starred" : "go to the Starred view ",
"fullscreen" : "tam ekran moduna geç ",
"font_size" : "mevcut içerik için yazı boyunutunu arttır/azalt",
"go_to_all" : "Tüm öğeleri görüntüle",
"go_to_starred" : "yıldızlı öğerleri görüntüle",
"feed_search" : "abonelik ismini yazarak aboneliğe git"
}
}
}
}

View File

@@ -4,6 +4,10 @@
<title>CommaFeed</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json">
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="app-icon-57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="app-icon-72.png" />
@@ -12,7 +16,9 @@
<link rel="icon" sizes="32x32" href="app-icon-32.png" />
<link rel="icon" sizes="64x64" href="app-icon-64.png" />
<link rel="icon" sizes="128x128" href="app-icon-128.png" />
<link rel="icon" sizes="192x192" href="app-icon-192.png" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<meta name="theme-color" content="#F88A14" />
<meta name="application-name" content="CommaFeed" />
<meta name="msapplication-navbutton-color" content="#F88A14" />
<meta name="msapplication-starturl" content="/" />
@@ -39,7 +45,7 @@
</div>
<!-- build:js js/app.js -->
<script type="text/javascript" src="lib/lodash/dist/lodash.js"></script>
<script type="text/javascript" src="lib/lodash/lodash.js"></script>
<script type="text/javascript" src="lib/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="lib/jquery-ui/ui/jquery-ui.js"></script>
<script type="text/javascript" src="lib/jquery-mousewheel/jquery.mousewheel.js"></script>
@@ -62,8 +68,8 @@
<script type="text/javascript" src="lib/angular-ui-select2/src/select2.js"></script>
<script type="text/javascript" src="lib/select2/select2.js"></script>
<script type="text/javascript" src="lib/mousetrap/mousetrap.js"></script>
<script type="text/javascript" src="lib/momentjs/min/moment-with-langs.js"></script>
<script type="text/javascript" src="lib/device.js/lib/device.js"></script>
<script type="text/javascript" src="lib/momentjs/min/moment-with-locales.js"></script>
<script type="text/javascript" src="lib/devicejs/lib/device.js"></script>
<script type="text/javascript" src="js/controllers.js"></script>
<script type="text/javascript" src="js/directives.js"></script>

View File

@@ -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) {

View File

@@ -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'
};
});

View File

@@ -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,

View File

@@ -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;

View File

@@ -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"
}

View File

@@ -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";

View File

@@ -13,8 +13,4 @@
content: "\e018";
font-family: "readabilicons";
-webkit-font-smoothing: antialiased;
font-size: 21px;
top: 5px;
position: relative;
line-height: 0px;
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -6,7 +6,7 @@
<button type="button" class="close" ng-click="close()">&times;</button>
<h4>
<input ng-model="filter" class="filter-input"
ui-keydown="{'up': 'focusPrevious($event)', 'down': 'focusNext($event)', 'enter': 'openFocused()' }" placeholder="'feedsearch.hint' | translate"
ui-keydown="{'up': 'focusPrevious($event)', 'down': 'focusNext($event)', 'enter': 'openFocused()' }" placeholder="{{'feedsearch.hint' | translate}}"
focus="feedSearchModal">
</h4>
<small>{{ 'feedsearch.help' | translate }}</small>

View File

@@ -4,14 +4,8 @@
<dt>Mean</dt>
<dd>{{metric.meanRate | number:2}}</dd>
<dt>1 min</dt>
<dd>{{metric.oneMinuteRate | number:2}}</dd>
<dt>5 min</dt>
<dd>{{metric.fiveMinuteRate | number:2}}</dd>
<dt>15 min</dt>
<dd>{{metric.fifteenMinuteRate | number:2}}</dd>
<dt>1/5/15 min</dt>
<dd>{{metric.oneMinuteRate | number:2}} {{metric.fiveMinuteRate | number:2}} {{metric.fifteenMinuteRate | number:2}}</dd>
<dt>Total</dt>
<dd>{{metric.count}}</dd>

View File

@@ -0,0 +1,17 @@
<div>
<span>{{label}}</span>
<dl class="dl-horizontal">
<dt>Mean</dt>
<dd>{{metric.meanRate | number:2}}</dd>
<dt>1/5/15 min</dt>
<dd>{{metric.oneMinuteRate | number:2}} {{metric.fiveMinuteRate | number:2}} {{metric.fifteenMinuteRate | number:2}}</dd>
<dt>Total</dt>
<dd>{{metric.count}}</dd>
<dt>min/max/mean (ms)</dt>
<dd>{{metric.snapshot.min/1000000 | number:0}} {{metric.snapshot.max/1000000 | number:0}} {{metric.snapshot.mean/1000000 | number:0}}</dd>
</dl>
</div>

View File

@@ -1,12 +1,12 @@
<span>
<a ng-click="edit_mode=!edit_mode" class="nolink pointer">
<span ng-click="edit_mode=!edit_mode" class="nolink pointer">
<i class="icon-tags"></i>
{{ 'global.tags' | translate }}
</a>
</span>
<span ng-if="!edit_mode">
<span class="label label-info" ng-repeat="tag in entry.tags">{{tag}}</span>
</span>
<span ng-if="edit_mode">
<input type="text" ui-select2="select2Options" ng-model="entry.tags" class="tag-input" autofocus />
<input type="hidden" ui-select2="select2Options" ng-model="entry.tags" class="tag-input" autofocus />
</span>
</span>

View File

@@ -124,9 +124,9 @@
</form>
</div>
<div class="btn-group donate">
<a class="btn btn-success" type="button" ng-click="toHelp()" title="{{ 'toolbar.about' | translate }} / {{ 'toolbar.donate' | translate }}">
<button class="btn btn-success" type="button" ng-click="toHelp()" title="{{ 'toolbar.about' | translate }} / {{ 'toolbar.donate' | translate }}">
<i class="icon-info-sign"></i>
</a>
</button>
</div>
</div>

View File

@@ -1,21 +1,28 @@
<div>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedQueues.refill']" label="'Refresh queue refill rate (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshTaskGiver.feedRefreshed']" label="'Feed refreshed (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated']" label="'Feed updated (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit']" label="'Entry cache hit (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss']" label="'Entry cache miss (/sec)'"></metric-meter>
<div class="col-md-6">
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedQueues.refill']" label="'Refresh queue refill rate (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshTaskGiver.feedRefreshed']" label="'Feed refreshed (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated']" label="'Feed updated (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit']" label="'Entry cache hit (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss']" label="'Entry cache miss (/sec)'"></metric-meter>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.active']"
label="'Feed Updater active'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.pending']"
label="'Feed Updater queued'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.active']"
label="'Feed Updater active'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.pending']"
label="'Feed Updater queued'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.active']"
label="'Feed Worker active'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.pending']"
label="'Feed Worker queued'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.active']"
label="'Feed Worker active'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.pending']"
label="'Feed Worker queued'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.addQueue']" label="'Task Giver Add Queue'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.takeQueue']" label="'Task Giver Take Queue'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.giveBackQueue']" label="'Task Giver Give Back Queue'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.addQueue']" label="'Task Giver Add Queue'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.takeQueue']" label="'Task Giver Take Queue'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.giveBackQueue']" label="'Task Giver Give Back Queue'"></metric-gauge>
</div>
<div class="col-md-6">
<div ng-repeat="(name, timer) in metrics.timers">
<metric-timer metric="timer" label="name"></metric-gauge>
</div>
</div>
</div>

View File

@@ -30,7 +30,7 @@
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'details.feed_url' | translate }}</label>
<div class="col-sm-10 checkbox">
<div class="col-sm-10 form-control-static">
<a ng-show="user.apiKey" href="{{'rest/category/entriesAsFeed?id=' + category.id + '&apiKey=' + user.apiKey}}" target="_blank">{{ 'global.link' | translate }}</a>
<span ng-show="!user.apiKey">{{ 'details.generate_api_key_first' | translate }}</span>
</div>

View File

@@ -3,15 +3,16 @@
<h3>{{ 'details.feed_details' | translate }}</h3>
</div>
<form name="form" class="form-horizontal" ng-submit="save()">
<div class="alert alert-danger" ng-if="error">{{ error }}</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'details.url' | translate }}</label>
<div class="col-sm-10 checkbox">
<div class="col-sm-10 form-control-static">
<a href="{{sub.feedUrl}}" target="_blank">{{sub.feedUrl}}</a>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'details.website' | translate }}</label>
<div class="col-sm-10 checkbox">
<div class="col-sm-10 form-control-static">
<a href="{{sub.feedLink}}" target="_blank">{{sub.feedLink}}</a>
</div>
</div>
@@ -49,26 +50,34 @@
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'details.next_refresh' | translate }}</label>
<div class="col-sm-10 checkbox">
<div class="col-sm-10 form-control-static">
<span>{{sub.nextRefresh|entryDate:('details.queued_for_refresh' | translate) }}</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'details.message' | translate }}</label>
<div class="col-sm-10 checkbox">
<div class="col-sm-10 form-control-static">
<span>{{sub.message}}</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'details.feed_url' | translate }}</label>
<div class="col-sm-10 checkbox">
<div class="col-sm-10 form-control-static">
<a ng-show="user.apiKey" href="{{'rest/feed/entriesAsFeed?id=' + sub.id + '&apiKey=' + user.apiKey}}" target="_blank">{{ 'global.link' | translate }}</a>
<span ng-show="!user.apiKey">{{ 'details.generate_api_key_first' | translate }}</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{ 'details.filtering_expression' | translate }}</label>
<div class="col-sm-10">
<input type="text" name="filter" ng-model="sub.filter" class="form-control"></input>
<p class="help-block pre-wrap" ng-bind-html="'details.filtering_expression_help' | translate"></p>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">{{ 'global.save' | translate }}</button>

View File

@@ -51,6 +51,9 @@
<span class="entry-author-prefix">{{ 'view.entry_author' | translate }}</span>
<span class="entry-author-name">{{entry.author}}</span>
</span>
<span class="entry-categories" ng-if="entry.categories">
<span class="entry-categories-name">({{entry.categories}})</span>
</span>
</div>
</div>
</div>
@@ -58,8 +61,7 @@
<div class="entry-body-content">
<div ng-if="!MobileService.mobile" ng-bind-html="entry.content | iframeHttpsRewrite| highlight:keywords | trustHtml"></div>
<div ng-if="MobileService.mobile" ng-bind-html="entry.content | iframeHttpsRewrite| highlight:keywords | appendImageTitles | trustHtml"></div>
<div class="entry-enclosure" ng-if="entry.enclosureType">
<div class="entry-enclosure" ng-if="entry.enclosureType && (entry.enclosureUrl && entry.content && entry.content.indexOf(entry.enclosureUrl) == -1)">
<video controls ng-if="entry.enclosureType && entry.enclosureType.indexOf('video') == 0">
<source ng-src="{{entry.enclosureUrl | trustUrl}}" type="{{entry.enclosureType}}" />
</video>

View File

@@ -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<CommaFeedConfiguration> {
@@ -89,36 +83,44 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
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<CommaFeedConfiguration>() {
@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<CommaFeedConfiguration> {
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<ScheduledTask> tasks = injector.getInstance(Key.get(new TypeLiteral<Set<ScheduledTask>>() {
}));
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<CommaFeedConfiguration> {
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<CommaFeedConfiguration> {
}
}).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*");
// enable wadl
environment.jersey().disable(ResourceConfig.FEATURE_DISABLE_WADL);
}
public static void main(String[] args) throws Exception {

View File

@@ -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() {

View File

@@ -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<AbstractFaviconFetcher> multibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
multibinder.addBinding().to(YoutubeFaviconFetcher.class);
multibinder.addBinding().to(DefaultFaviconFetcher.class);
Multibinder<AbstractFaviconFetcher> faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class);
faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class);
faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class);
Multibinder<ScheduledTask> 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);
}
}

View File

@@ -16,7 +16,7 @@ import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.protocol.HttpContext;
class ContentEncodingInterceptor implements HttpResponseInterceptor {
private static final Set<String> 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);
}

View File

@@ -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);

View File

@@ -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<String> getLastEntries(Feed feed) {
List<String> list = Lists.newArrayList();
List<String> list = new ArrayList<>();
try (Jedis jedis = pool.getResource()) {
String key = buildRedisEntryKey(feed);
Set<String> members = jedis.smembers(key);

View File

@@ -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;

View File

@@ -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<FeedCategory> {
@@ -26,11 +26,11 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
}
public List<FeedCategory> 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<FeedCategory> {
} 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<FeedCategory> findByParent(User user, FeedCategory parent) {
@@ -50,18 +50,11 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} 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<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {
List<FeedCategory> list = Lists.newArrayList();
List<FeedCategory> 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<FeedCategory> {
}
boolean isChild = false;
while (child != null) {
if (ObjectUtils.equals(child.getId(), parent.getId())) {
if (Objects.equals(child.getId(), parent.getId())) {
isChild = true;
break;
}

View File

@@ -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<Feed> {
@@ -29,24 +30,23 @@ public class FeedDAO extends GenericDAO<Feed> {
}
public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
BooleanBuilder disabledDatePredicate = new BooleanBuilder();
disabledDatePredicate.or(feed.disabledUntil.isNull());
disabledDatePredicate.or(feed.disabledUntil.lt(new Date()));
HibernateQuery<Feed> 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<Integer> 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<Feed> feeds = newQuery().from(feed).where(feed.normalizedUrlHash.eq(DigestUtils.sha1Hex(normalizedUrl))).list(feed);
List<Feed> 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<Feed> {
}
public List<Feed> 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<Feed> 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();
}
}

View File

@@ -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<FeedEntryContent> {
private QFeedEntryContent content = QFeedEntryContent.feedEntryContent;
private QFeedEntry entry = QFeedEntry.feedEntry;
@Inject
public FeedEntryContentDAO(SessionFactory sessionFactory) {
@@ -23,15 +25,15 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
}
public Long findExisting(String contentHash, String titleHash) {
List<Long> 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<FeedEntryContent> list = newQuery().from(content).leftJoin(content.entries, entry).where(entry.id.isNull()).limit(max)
.list(content);
JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id));
List<FeedEntryContent> list = query().selectFrom(content).where(subQuery.notExists()).limit(max).fetch();
int deleted = list.size();
delete(list);
return deleted;

View File

@@ -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<FeedEntry> {
@@ -27,22 +29,32 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
}
public Long findExisting(String guid, Feed feed) {
List<Long> 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<FeedEntry> 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<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
NumberExpression<Long> count = entry.id.count();
List<Tuple> 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<FeedEntry> 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<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch();
return delete(list);
}
public int deleteOldEntries(Long feedId, long max) {
List<FeedEntry> 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;
}
}

View File

@@ -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<FeedEntryStatus> {
@@ -60,13 +62,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
builder.append(o2.getEntryUpdated(), o1.getEntryUpdated());
builder.append(o2.getId(), o1.getId());
return builder.toComparison();
};
}
};
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse();
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
List<FeedEntryStatus> statuses = newQuery().from(status).where(status.entry.eq(entry), status.subscription.eq(sub)).list(status);
List<FeedEntryStatus> 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<FeedEntryStatus> {
}
public List<FeedEntryStatus> 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<FeedEntryStatus> 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<FeedEntryStatus> {
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<FeedEntryStatus> statuses = query.list(status);
List<FeedEntryStatus> 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<FeedEntryStatus> {
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<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> 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<FeedEntry> 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<FeedEntryStatus> {
return query;
}
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly, String keywords,
Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds, String tag) {
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
boolean onlyIds, String tag) {
int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(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<Tuple> tuples = query.list(entry.id, entry.updated, status.id);
HibernateQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag);
List<Tuple> 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<FeedEntryStatus> {
List<FeedEntryStatus> 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<FeedEntryStatus> {
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<FeedEntryStatus> {
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
UnreadCount uc = null;
HibernateQuery query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null);
List<Tuple> tuples = query.list(entry.count(), entry.updated.max());
HibernateQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null);
List<Tuple> 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<FeedEntryStatus> {
}
public List<FeedEntryStatus> 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();
}
}

View File

@@ -23,10 +23,10 @@ public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
}
public List<String> 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<FeedEntryTag> 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();
}
}

View File

@@ -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<FeedSubscription> {
@@ -29,57 +29,44 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
}
public FeedSubscription findById(User user, Long id) {
List<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user), sub.id.eq(id)).leftJoin(sub.feed).fetch()
.leftJoin(sub.category).fetch().list(sub);
List<FeedSubscription> 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<FeedSubscription> 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<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user), sub.feed.eq(feed)).list(sub);
List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user), sub.feed.eq(feed)).fetch();
return initRelations(Iterables.getFirst(subs, null));
}
public List<FeedSubscription> findAll(User user) {
List<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user)).leftJoin(sub.feed).fetch().leftJoin(sub.category)
.fetch().list(sub);
List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user)).leftJoin(sub.feed).fetchJoin()
.leftJoin(sub.category).fetchJoin().fetch();
return initRelations(subs);
}
public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
HibernateQuery query = newQuery().from(sub).where(sub.user.eq(user));
HibernateQuery<FeedSubscription> 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<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {
List<Long> categoryIds = Lists.transform(categories, new Function<FeedCategory, Long>() {
@Override
public Long apply(FeedCategory input) {
return input.getId();
}
});
List<FeedSubscription> subscriptions = Lists.newArrayList();
for (FeedSubscription sub : findAll(user)) {
if (sub.getCategory() != null && categoryIds.contains(sub.getCategory().getId())) {
subscriptions.add(sub);
}
}
return subscriptions;
Set<Long> 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<FeedSubscription> initRelations(List<FeedSubscription> list) {
for (FeedSubscription sub : list) {
initRelations(sub);
}
list.forEach(s -> initRelations(s));
return list;
}

View File

@@ -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<T extends AbstractModel> extends AbstractDAO<T> {
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<T extends AbstractModel> extends AbstractDAO<T>
}
public void saveOrUpdate(Collection<T> models) {
for (T model : models) {
persist(model);
}
}
public void merge(T model) {
currentSession().merge(model);
}
public void merge(Collection<T> 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<T extends AbstractModel> extends AbstractDAO<T>
}
public int delete(Collection<T> objects) {
for (T object : objects) {
delete(object);
}
objects.forEach(o -> delete(o));
return objects.size();
}

View File

@@ -5,17 +5,26 @@ import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.context.internal.ManagedSessionContext;
public abstract class UnitOfWork<T> {
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<T> {
public T runInSession();
}
public T run() {
public static void run(SessionFactory sessionFactory, SessionRunner sessionRunner) {
call(sessionFactory, () -> {
sessionRunner.runInSession();
return null;
});
}
public static <T> T call(SessionFactory sessionFactory, SessionRunnerReturningValue<T> 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<T> {
ManagedSessionContext.bind(session);
session.beginTransaction();
try {
t = runInSession();
t = sessionRunner.runInSession();
commitTransaction(session);
} catch (Exception e) {
rollbackTransaction(session);
this.<RuntimeException> rethrow(e);
UnitOfWork.<RuntimeException> rethrow(e);
}
} finally {
session.close();
@@ -38,14 +47,14 @@ public abstract class UnitOfWork<T> {
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<T> {
}
@SuppressWarnings("unchecked")
private <E extends Exception> void rethrow(Exception e) throws E {
private static <E extends Exception> void rethrow(Exception e) throws E {
throw (E) e;
}

View File

@@ -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<User> {
}
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();
}
}

View File

@@ -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<UserRole> {
@@ -25,18 +25,14 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
}
public List<UserRole> findAll() {
return newQuery().from(role).leftJoin(role.user).fetch().distinct().list(role);
return query().selectFrom(role).leftJoin(role.user).fetchJoin().distinct().fetch();
}
public List<UserRole> 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<Role> findRoles(User user) {
Set<Role> 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());
}
}

View File

@@ -20,6 +20,6 @@ public class UserSettingsDAO extends GenericDAO<UserSettings> {
}
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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<NameValuePair> params = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8.name());
for (NameValuePair param : params) {
if ("id".equals(param.getName())) {
return param.getValue();
}
}
return null;
}
}

View File

@@ -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<NameValuePair> params = URLEncodedUtils.parse(url.substring(url.indexOf("?") + 1), StandardCharsets.UTF_8);
Optional<NameValuePair> userId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("user")).findFirst();
Optional<NameValuePair> 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("<media:thumbnail url='");
int thumbnailEnd = thumbnailUrl.indexOf("'/>", thumbnailStart);
if (thumbnailStart != -1) {
thumbnailUrl = thumbnailUrl.substring(thumbnailStart + "<media:thumbnail url='".length(), thumbnailEnd);
YouTube.Channels.List list = youtube.channels().list("snippet");
list.setKey(googleAuthKey);
if (userId.isPresent()) {
list.setForUsername(userId.get().getValue());
} else {
list.setId(channelId.get().getValue());
}
// final get to actually retrieve the thumbnail
HttpResult iconResult = getter.getBinary(thumbnailUrl, TIMEOUT);
log.debug("contacting youtube api");
ChannelListResponse response = list.execute();
if (response.getItems().isEmpty()) {
log.debug("youtube api returned no items");
return null;
}
Channel channel = response.getItems().get(0);
Thumbnail thumbnail = channel.getSnippet().getThumbnails().getDefault();
log.debug("fetching favicon");
HttpResult iconResult = getter.getBinary(thumbnail.getUrl(), TIMEOUT);
bytes = iconResult.getContent();
contentType = iconResult.getContentType();
} catch (Exception e) {
@@ -68,23 +93,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
}
if (!isValidIconResponse(bytes, contentType)) {
bytes = null;
}
return bytes;
}
private String extractUserName(String url) {
int apiOrBase = url.indexOf("/users/");
if (apiOrBase == -1) {
return null;
}
int userEndSlash = url.indexOf('/', apiOrBase + "/users/".length());
if (userEndSlash == -1) {
return null;
}
return url.substring(apiOrBase + "/users/".length(), userEndSlash);
return new Favicon(bytes, contentType);
}
}

View File

@@ -0,0 +1,39 @@
package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
/**
* A keyword used in a search query
*/
@Getter
@RequiredArgsConstructor
public class FeedEntryKeyword {
public static enum Mode {
INCLUDE, EXCLUDE;
}
private final String keyword;
private final Mode mode;
public static List<FeedEntryKeyword> fromQueryString(String keywords) {
List<FeedEntryKeyword> 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;
}
}

View File

@@ -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;
}

View File

@@ -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<SyndContent, String> CONTENT_TO_STRING = new Function<SyndContent, String>() {
@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<FeedEntry> 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);
}

View File

@@ -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<FeedRefreshContext> addQueue = Queues.newConcurrentLinkedQueue();
private Queue<FeedRefreshContext> takeQueue = Queues.newConcurrentLinkedQueue();
private Queue<Feed> giveBackQueue = Queues.newConcurrentLinkedQueue();
private Queue<FeedRefreshContext> addQueue = new ConcurrentLinkedQueue<>();
private Queue<FeedRefreshContext> takeQueue = new ConcurrentLinkedQueue<>();
private Queue<Feed> 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<FeedRefreshContext> contexts = Lists.newArrayList();
List<FeedRefreshContext> 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<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold());
List<Feed> 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<Long, FeedRefreshContext> map = Maps.newLinkedHashMap();
Map<Long, FeedRefreshContext> 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<Feed> feeds = Lists.newArrayList();
for (FeedRefreshContext context : map.values()) {
feeds.add(context.getFeed());
}
feedDAO.merge(feeds);
List<Feed> 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;

View File

@@ -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<FeedEntry> 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<FeedEntry> getEntries() {
return entries;
}
public void setEntries(List<FeedEntry> entries) {
this.entries = entries;
}
}

View File

@@ -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) {

View File

@@ -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<FeedRefreshContext>(sessionFactory) {
@Override
protected FeedRefreshContext runInSession() throws Exception {
return queues.take();
}
}.run();
FeedRefreshContext context = queues.take();
if (context != null) {
feedRefreshed.mark();
worker.updateFeed(context);

View File

@@ -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<String> lastEntries = cache.getLastEntries(feed);
List<String> currentEntries = Lists.newArrayList();
List<String> currentEntries = new ArrayList<>();
List<FeedSubscription> 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<List<FeedSubscription>>(sessionFactory) {
@Override
protected List<FeedSubscription> 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<User> users = Lists.newArrayList();
for (FeedSubscription sub : subscriptions) {
users.add(sub.getUser());
}
List<User> 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<Boolean>(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();
}

View File

@@ -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<FeedEntry> 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);

View File

@@ -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<String> rules = Lists.newArrayList();
List<String> 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<String> rules = Lists.newArrayList();
List<String> 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<Long> getSortedTimestamps(List<FeedEntry> entries) {
List<Long> 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<Entry> entries, String keywords) {
if (StringUtils.isBlank(keywords)) {
return;
}
public static void removeUnwantedFromSearch(List<Entry> entries, List<FeedEntryKeyword> keywords) {
Iterator<Entry> 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;
}

View File

@@ -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<FeedEntry> entries = Lists.newArrayList();
private List<FeedEntry> 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<FeedEntry> getEntries() {
return entries;
}
public void setEntries(List<FeedEntry> 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;
}
}

View File

@@ -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<String, String> NUMERIC_MAPPING = Collections.unmodifiableMap(loadMap());
public static final String[] HTML_ENTITIES;
public static final String[] NUMERIC_ENTITIES;
private static synchronized Map<String, String> loadMap() {
Map<String, String> map = Maps.newLinkedHashMap();
static {
Map<String, String> map = new LinkedHashMap<>();
map.put("&Aacute;", "&#193;");
map.put("&aacute;", "&#225;");
map.put("&Acirc;", "&#194;");
@@ -261,6 +260,7 @@ public class HtmlEntities {
map.put("&zwj;", "&#8205;");
map.put("&zwnj;", "&#8204;");
return map;
HTML_ENTITIES = map.keySet().toArray(new String[map.size()]);
NUMERIC_ENTITIES = map.values().toArray(new String[map.size()]);
}
}

View File

@@ -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<FeedEntry> entries;
@OneToMany(mappedBy = "feed")
private Set<FeedSubscription> subscriptions;
/**
* detected hub for pubsubhubbub
*/

View File

@@ -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<FeedEntry> entries;

View File

@@ -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<FeedEntryTag> tags = Lists.newArrayList();
private List<FeedEntryTag> tags = new ArrayList<>();
/**
* Denormalization starts here

View File

@@ -40,4 +40,7 @@ public class FeedSubscription extends AbstractModel {
private Integer position;
@Column(length = 4096)
private String filter;
}

View File

@@ -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<UserRole> roles = Sets.newHashSet();
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private Set<FeedSubscription> 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));
}

View File

@@ -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")

View File

@@ -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<FeedCategory> categories = feedCategoryDAO.findAll(user);
Collections.sort(categories,
(e1, e2) -> MoreObjects.firstNonNull(e1.getPosition(), 0) - MoreObjects.firstNonNull(e2.getPosition(), 0));
List<FeedSubscription> 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<FeedSubscription> subscriptions) {
private Outline buildCategoryOutline(FeedCategory cat, List<FeedCategory> categories, List<FeedSubscription> 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;
}

View File

@@ -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<Outline> 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<Outline> 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) {

View File

@@ -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);
}
}

View File

@@ -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<Integer>(sessionFactory) {
@Override
protected Integer runInSession() throws Exception {
List<Feed> feeds = feedDAO.findWithoutSubscriptions(1);
return feedDAO.delete(feeds);
};
}.run();
List<Feed> 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<Integer>(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<Integer>(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<FeedCapacity> 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<Integer>(sessionFactory) {
@Override
protected Integer runInSession() throws Exception {
List<FeedEntryStatus> 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;

View File

@@ -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;

View File

@@ -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<Object> callable = script.callable(context);
Future<Object> 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);
}
}
}

View File

@@ -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<FeedSubscription> subscriptions, Date olderThan, String keywords) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null, false,
false, null);
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan, List<FeedEntryKeyword> keywords) {
List<FeedEntryStatus> 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<FeedEntryStatus> statuses, Date olderThan) {
List<FeedEntryStatus> list = Lists.newArrayList();
List<FeedEntryStatus> list = new ArrayList<>();
for (FeedEntryStatus status : statuses) {
if (!status.isRead()) {
Date inserted = status.getEntry().getInserted();

View File

@@ -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<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, entry);
Map<String, FeedEntryTag> tagMap = Maps.uniqueIndex(tags, new Function<FeedEntryTag, String>() {
@Override
public String apply(FeedEntryTag input) {
return input.getName();
}
});
List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
Set<String> existingTagNames = existingTags.stream().map(t -> t.getName()).collect(Collectors.toSet());
List<FeedEntryTag> addList = Lists.newArrayList();
List<FeedEntryTag> 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<FeedEntryTag> addList = tagNames.stream().filter(name -> !existingTagNames.contains(name))
.map(name -> new FeedEntryTag(user, entry, name)).collect(Collectors.toList());
List<FeedEntryTag> removeList = existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).collect(Collectors.toList());
feedEntryTagDAO.saveOrUpdate(addList);
feedEntryTagDAO.delete(removeList);

View File

@@ -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<AbstractFaviconFetcher> faviconFetchers;
private byte[] defaultFavicon;
private Favicon defaultFavicon;
@Inject
public FeedService(FeedDAO feedDAO, Set<AbstractFaviconFetcher> 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) {

View File

@@ -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<Long, UnreadCount> getUnreadCount(User user) {
Map<Long, UnreadCount> map = Maps.newHashMap();
List<FeedSubscription> 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) {

View File

@@ -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<FeedSubscription> 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;
}
}

View File

@@ -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");

View File

@@ -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")

View File

@@ -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<NameValuePair> nvp = Lists.newArrayList();
List<NameValuePair> 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"));

View File

@@ -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<Void>(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);
}

View File

@@ -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<User> 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<User> 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<Role> getRoles(User user) {
return userRoleDAO.findRoles(user);
}
}

Some files were not shown because too many files have changed in this diff Show More