forked from Archives/Athou_commafeed
Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a16d9877cc | ||
|
|
c24e9e083c | ||
|
|
101602c6f6 | ||
|
|
18a7bd1fd1 | ||
|
|
dfbd556bb8 | ||
|
|
040cdde8ba | ||
|
|
06373480ae | ||
|
|
5713a78f2e | ||
|
|
b9f2f17a24 | ||
|
|
9adc993472 | ||
|
|
dcd5f3d529 | ||
|
|
18e70a0e6b | ||
|
|
5ad57d1608 | ||
|
|
74eaf48ceb | ||
|
|
30bb0cb291 | ||
|
|
b50e6b93bd | ||
|
|
a0b5a1462d | ||
|
|
4910f93c94 | ||
|
|
4a52bd0cb7 | ||
|
|
b0bfb73952 | ||
|
|
69d049a69a | ||
|
|
7d75153362 | ||
|
|
748bfa31ae | ||
|
|
e7d995edbc | ||
|
|
a144fb2e48 | ||
|
|
7521013e11 | ||
|
|
c6321fc6b2 | ||
|
|
7d92d5d096 | ||
|
|
ab201d5016 | ||
|
|
efa38d5ee9 | ||
|
|
e8769d09a8 | ||
|
|
a216444825 | ||
|
|
fee3e10e6b | ||
|
|
4d71a8f3c2 | ||
|
|
fc104b0b01 | ||
|
|
3dcb351b36 | ||
|
|
600d05d08f | ||
|
|
6b6ff70ad3 | ||
|
|
891f660738 | ||
|
|
6901b9b728 | ||
|
|
c7f211a7f8 | ||
|
|
c48ea1152c | ||
|
|
f5d0eb94b4 | ||
|
|
cebeef04a0 | ||
|
|
3e77a83ca6 | ||
|
|
c872b335e7 | ||
|
|
cc1e173552 | ||
|
|
35e0567705 | ||
|
|
fb2add305e | ||
|
|
74d4c18c4c | ||
|
|
da3ce07485 | ||
|
|
c7ab179a9e | ||
|
|
6fd11fcd56 | ||
|
|
3966cf165b | ||
|
|
0b2ada5d1c | ||
|
|
4278101bbe | ||
|
|
8b43af49fc | ||
|
|
6e29e8426b | ||
|
|
af11d3c771 | ||
|
|
e5c5af4d57 | ||
|
|
3dbdf5adf2 | ||
|
|
4d7a030b70 | ||
|
|
3351262dd7 | ||
|
|
5ec4377502 | ||
|
|
9c8402c3a5 | ||
|
|
928a45e48e | ||
|
|
1d088c5eae | ||
|
|
cdcf81ab7c | ||
|
|
9f196bafe9 | ||
|
|
5c9e1406a1 | ||
|
|
0b42e00b29 | ||
|
|
88b98a138f | ||
|
|
136c37885d | ||
|
|
812988b31a | ||
|
|
191680a01b | ||
|
|
467d1a754d | ||
|
|
d1973922cd | ||
|
|
3b7689975d | ||
|
|
3386a71c5e | ||
|
|
7bb65a5e76 | ||
|
|
f3a9c8e0e2 | ||
|
|
22861ca8d0 | ||
|
|
19118ea241 | ||
|
|
4a9dc7249f | ||
|
|
5dad9c2eb8 | ||
|
|
d6b35b00b9 | ||
|
|
fda8ab500b | ||
|
|
66df421de2 | ||
|
|
33c62f08ca | ||
|
|
b660602809 | ||
|
|
6dfce2ca30 | ||
|
|
655e20e99e | ||
|
|
f2b80bdc08 | ||
|
|
10af873fa5 | ||
|
|
d87a5b14f8 | ||
|
|
b87a18b993 | ||
|
|
c4185034e4 | ||
|
|
9d64426b00 | ||
|
|
c81cc8bea4 | ||
|
|
90e680d6be | ||
|
|
04c0833111 | ||
|
|
06151eab3b | ||
|
|
3dcb8590f6 | ||
|
|
a9b313aa4a | ||
|
|
1f2e35060b | ||
|
|
a96862fffa | ||
|
|
68cb8e194d | ||
|
|
c164926c54 | ||
|
|
de7516116d | ||
|
|
fccfe5b088 | ||
|
|
23aa5fa0a3 | ||
|
|
d384c0a141 | ||
|
|
18058c2a36 | ||
|
|
71727202f3 | ||
|
|
eee0b949de | ||
|
|
3cbbb67b0c | ||
|
|
7879f66e78 | ||
|
|
c14ac37495 | ||
|
|
73a77183aa | ||
|
|
09cfa21091 | ||
|
|
c193571ece | ||
|
|
04bc92b071 | ||
|
|
94e58a449c | ||
|
|
9d044195aa | ||
|
|
caff34cc3b | ||
|
|
34c5c0b1f7 | ||
|
|
906801e13c | ||
|
|
dad4c6b866 | ||
|
|
090462022f | ||
|
|
cbf9f65fb4 | ||
|
|
5a493cd55d | ||
|
|
dfc204ef05 | ||
|
|
56c6e2d29c | ||
|
|
db03dd12a0 | ||
|
|
6c67e6363a | ||
|
|
e2888beb4c | ||
|
|
bba9166885 | ||
|
|
504e4eab3e | ||
|
|
2e475c35cc | ||
|
|
ccf18758fb | ||
|
|
68f9852790 | ||
|
|
d0150de003 | ||
|
|
e2b792335b | ||
|
|
ece38c9e59 | ||
|
|
a19b5090bf | ||
|
|
e4b3c35892 | ||
|
|
4b229a759a | ||
|
|
1e9e42ac48 | ||
|
|
245a48f66e | ||
|
|
e6d8397550 | ||
|
|
d59bd43846 | ||
|
|
c1579c83c7 | ||
|
|
4d782e60ad | ||
|
|
c702f47927 | ||
|
|
9110cfd923 | ||
|
|
e40dd14bbf | ||
|
|
90aaae9959 | ||
|
|
e81dda0fa8 | ||
|
|
f93796d036 | ||
|
|
d06359cb81 | ||
|
|
8b68fb578f | ||
|
|
cca300e419 | ||
|
|
77c3ec0bbe | ||
|
|
ed81fc576a | ||
|
|
435fcb9669 | ||
|
|
9020d95b62 | ||
|
|
84d7a501d4 | ||
|
|
e65dd49d69 | ||
|
|
a705cbe6c2 | ||
|
|
60b8af3860 | ||
|
|
9ac4187aa8 | ||
|
|
6419d29489 | ||
|
|
4684e43f42 | ||
|
|
a477c9fa6d | ||
|
|
d1be331f99 | ||
|
|
cbc792d406 | ||
|
|
0313c5c560 | ||
|
|
18aa2fcd92 | ||
|
|
10461941d7 | ||
|
|
e6050219bc | ||
|
|
81481c37fe | ||
|
|
5ea92a7d18 | ||
|
|
f40630aced | ||
|
|
81850acdfe | ||
|
|
6819d5aa8b | ||
|
|
2aef4e5d05 | ||
|
|
6d4d2c3e7e | ||
|
|
87bcaa4731 | ||
|
|
5d2378f291 | ||
|
|
253507d14b | ||
|
|
548fb7099b | ||
|
|
0dd7c777ee | ||
|
|
6812bf2388 | ||
|
|
12bcbfa9f7 | ||
|
|
b5dfd371d9 | ||
|
|
e09d7fb103 | ||
|
|
0fe3afe254 | ||
|
|
db50d50c19 | ||
|
|
691bdb1512 | ||
|
|
d50b712bca | ||
|
|
3b68e4f32b | ||
|
|
259b9a90dd | ||
|
|
f4c5fd7eb4 | ||
|
|
3cd42d03f0 | ||
|
|
3497b82e8c | ||
|
|
15a24e4e75 | ||
|
|
96837f908e | ||
|
|
4ea5ebbf9e | ||
|
|
281e015376 | ||
|
|
5825a16aff | ||
|
|
2586a8c433 | ||
|
|
9f7c9c3428 | ||
|
|
9790ba735b | ||
|
|
e3dbcac9fb | ||
|
|
1c99929429 | ||
|
|
9b2cdbbb18 | ||
|
|
928cf1220e | ||
|
|
c0557856a3 | ||
|
|
97c2cc3d15 | ||
|
|
a94ef980bb | ||
|
|
eea0c24d2b | ||
|
|
c8fded3c56 | ||
|
|
8f2ba5e186 | ||
|
|
5ce2823d0b | ||
|
|
a0c70d326f | ||
|
|
5f28fd4114 | ||
|
|
7151db0909 | ||
|
|
e82888f8f3 | ||
|
|
4fb60a6ec6 | ||
|
|
27f22f6094 | ||
|
|
7497a0151a | ||
|
|
41f133afb1 | ||
|
|
4b15ecbc1b | ||
|
|
6498130850 | ||
|
|
24bd1121af | ||
|
|
3cccf741d6 | ||
|
|
0a2d2c3f43 |
@@ -1,6 +1,23 @@
|
|||||||
#!/bin/bash
|
#!/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
|
||||||
rm -rf $OPENSHIFT_REPO_DIR/node_modules
|
rm -rf $OPENSHIFT_REPO_DIR/node_modules
|
||||||
rm -rf $OPENSHIFT_TMP_DIR/npm
|
rm -rf $OPENSHIFT_TMP_DIR/npm
|
||||||
@@ -16,7 +33,4 @@ export HOME="$OPENSHIFT_TMP_DIR/local"
|
|||||||
|
|
||||||
export NPM_CONFIG_ARCH="x64"
|
export NPM_CONFIG_ARCH="x64"
|
||||||
|
|
||||||
npm install npm
|
mvn clean package -DskipTests -Dos.arch=x64 -s .openshift/settings.xml
|
||||||
export PATH="$OPENSHIFT_REPO_DIR/node_modules/.bin:$PATH"
|
|
||||||
|
|
||||||
mvn clean package -DskipTests -Dos.arch=x64
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
cd $OPENSHIFT_REPO_DIR
|
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 &
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
# CommaFeed settings
|
# CommaFeed settings
|
||||||
# ------------------
|
# ------------------
|
||||||
app:
|
app:
|
||||||
# context path of the application
|
|
||||||
contextPath: /
|
|
||||||
|
|
||||||
# url used to access commafeed
|
# url used to access commafeed
|
||||||
publicUrl: https://@OPENSHIFT_APP_DNS@/
|
publicUrl: https://@OPENSHIFT_APP_DNS@/
|
||||||
|
|
||||||
# wether to allow user registrations
|
# wether to allow user registrations
|
||||||
allowRegistrations: false
|
allowRegistrations: false
|
||||||
|
|
||||||
|
# create a demo account the first time the app starts
|
||||||
|
createDemoAccount: false
|
||||||
|
|
||||||
# put your google analytics tracking code here
|
# put your google analytics tracking code here
|
||||||
googleAnalyticsTrackingCode:
|
googleAnalyticsTrackingCode:
|
||||||
|
|
||||||
|
# put your google server key (used for youtube favicon fetching)
|
||||||
|
googleAuthKey:
|
||||||
|
|
||||||
# number of http threads
|
# number of http threads
|
||||||
backgroundThreads: 3
|
backgroundThreads: 3
|
||||||
|
|
||||||
@@ -47,6 +50,9 @@ app:
|
|||||||
# time to keep unread statuses (in days), 0 to disable
|
# time to keep unread statuses (in days), 0 to disable
|
||||||
keepStatusDays: 0
|
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 service to use, possible values are 'noop' and 'redis'
|
||||||
cache: noop
|
cache: noop
|
||||||
|
|
||||||
|
|||||||
0
.openshift/markers/java8
Normal file
0
.openshift/markers/java8
Normal file
@@ -1,3 +1,41 @@
|
|||||||
<settings>
|
<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>
|
</settings>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
language: java
|
language: java
|
||||||
jdk:
|
jdk:
|
||||||
- openjdk7
|
|
||||||
- oraclejdk7
|
|
||||||
- oraclejdk8
|
- oraclejdk8
|
||||||
14
CHANGELOG
14
CHANGELOG
@@ -1,3 +1,17 @@
|
|||||||
|
v 2.2.0
|
||||||
|
- fix youtube and instagram favicon fetching
|
||||||
|
- mark as read filter was lost when a feed was rearranged with drag&drop
|
||||||
|
- feed entry categories are now displayed if available
|
||||||
|
- various performance and dependencies upgrades
|
||||||
|
- java8 is now required
|
||||||
|
v 2.1.0
|
||||||
|
- dropwizard upgrade to 0.8.0
|
||||||
|
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use server.applicationContextPath instead
|
||||||
|
- new setting app.maxFeedCapacity for deleting old entries
|
||||||
|
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title, content, author or url.
|
||||||
|
- ability to use !keyword or -keyword to exclude a keyword from a search query
|
||||||
|
- facebook feeds now show user favicon instead of facebook favicon
|
||||||
|
- new dark theme 'nightsky'
|
||||||
v 2.0.3
|
v 2.0.3
|
||||||
- internet explorer ajax cache workaround
|
- internet explorer ajax cache workaround
|
||||||
- categories are now deletable again
|
- categories are now deletable again
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -1,28 +1,38 @@
|
|||||||
CommaFeed [](https://travis-ci.org/Athou/commafeed)
|
# CommaFeed [](https://travis-ci.org/Athou/commafeed)
|
||||||
=========
|
|
||||||
Sources for [CommaFeed.com](http://www.commafeed.com/).
|
Sources for [CommaFeed.com](http://www.commafeed.com/).
|
||||||
|
|
||||||
Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS.
|
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)
|
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 short version
|
||||||
|
|
||||||
|
git clone https://github.com/Athou/commafeed.git
|
||||||
|
cd commafeed
|
||||||
|
mvn 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
|
||||||
|
|
||||||
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).
|
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.
|
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 Maven 3.x (and a Java 1.7+ JDK) installed in order to build the application.
|
You also need Maven 3.x (and a Java 1.8+ JDK) installed in order to build the application.
|
||||||
|
|
||||||
To install maven and openjdk on Ubuntu, issue the following commands
|
To install maven and openjdk on Ubuntu, issue the following commands
|
||||||
|
|
||||||
sudo apt-get install build-essential openjdk-7-jdk maven
|
sudo apt-get install g++ build-essential openjdk-8-jdk maven
|
||||||
# Make sure java7 is the selected java version
|
# Make sure java8 is the selected java version
|
||||||
sudo update-alternatives --config java
|
sudo update-alternatives --config java
|
||||||
sudo update-alternatives --config javac
|
sudo update-alternatives --config javac
|
||||||
|
|
||||||
@@ -41,12 +51,11 @@ Now build the application
|
|||||||
Copy `config.yml.example` to `config.yml` then edit the file to your liking.
|
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`.
|
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.
|
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:
|
[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 +64,12 @@ Deployment on OpenShift
|
|||||||
git remote add upstream -m master https://github.com/Athou/commafeed.git
|
git remote add upstream -m master https://github.com/Athou/commafeed.git
|
||||||
git pull -s recursive -X theirs upstream master
|
git pull -s recursive -X theirs upstream master
|
||||||
git push
|
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).
|
Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/src/main/app/i18n).
|
||||||
|
|
||||||
@@ -65,18 +77,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 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.
|
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:
|
Steps to configuring a development environment for CommaFeed may include, but may not be limited to:
|
||||||
|
|
||||||
@@ -100,8 +110,7 @@ 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.
|
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.
|
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-2014 CommaFeed.
|
||||||
|
|
||||||
|
|||||||
38
bower.json
38
bower.json
@@ -2,32 +2,36 @@
|
|||||||
"name": "commafeed",
|
"name": "commafeed",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jquery": "1.11.0",
|
"jquery": "2.1.3",
|
||||||
"jquery-ui": "1.10.3",
|
"jquery-ui": "1.10.3",
|
||||||
"jquery-mousewheel": "3.1.12",
|
"jquery-mousewheel": "3.1.12",
|
||||||
"lodash": "2.4.1",
|
"lodash": "3.4.0",
|
||||||
"bootstrap": "3.1.1",
|
"bootstrap": "3.3.2",
|
||||||
"font-awesome": "3.2.1",
|
"font-awesome": "3.2.1",
|
||||||
"angular": "1.2.16",
|
"angular": "1.3.14",
|
||||||
"angular-resource": "1.2.16",
|
"angular-resource": "1.3.14",
|
||||||
"angular-route": "1.2.16",
|
"angular-route": "1.3.14",
|
||||||
"angular-sanitize": "1.2.16",
|
"angular-sanitize": "1.3.14",
|
||||||
"angular-touch": "1.2.16",
|
"angular-touch": "1.3.14",
|
||||||
"angular-animate": "1.2.16",
|
"angular-animate": "1.3.14",
|
||||||
"angular-ui-router": "0.2.8",
|
"angular-ui-router": "0.2.13",
|
||||||
"angular-ui-utils": "0.1.0",
|
"angular-ui-utils": "0.1.0",
|
||||||
"angular-ui-select2": "0.0.5",
|
"angular-ui-select2": "0.0.5",
|
||||||
"angular-bootstrap": "0.2.0",
|
"angular-bootstrap": "0.2.0",
|
||||||
"angular-loading-bar": "0.5.0",
|
"angular-loading-bar": "0.6.0",
|
||||||
"angular-translate": "2.2.0",
|
"angular-translate": "2.6.1",
|
||||||
"angular-translate-loader-static-files": "2.2.0",
|
"angular-translate-loader-static-files": "2.6.1",
|
||||||
"ngInfiniteScroll": "1.0.0",
|
"ngInfiniteScroll": "1.0.0",
|
||||||
"ng-grid": "2.0.6",
|
"ng-grid": "2.0.6",
|
||||||
"mousetrap": "1.4.6",
|
"mousetrap": "1.4.6",
|
||||||
"momentjs": "2.6.0",
|
"momentjs": "2.9.0",
|
||||||
"device.js": "matthewhudson/device.js#2ae5c775e35ccc837589e5af34e292c54936778c",
|
"devicejs": "0.2.4",
|
||||||
"readabilicons": "arc90/readability-readabilicons#34c55561c5b8ec6e90714b50237c06b13cb9d59c",
|
"readabilicons": "arc90/readability-readabilicons#34c55561c5b8ec6e90714b50237c06b13cb9d59c",
|
||||||
"zocial": "samcollins/css-social-buttons#1f59ecacde475e563fb6771667597493ec4eecb6",
|
"zocial-less": "1.0.0",
|
||||||
"swagger-ui": "2.0.21"
|
"swagger-ui": "2.1.8-M1"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"angular": "1.3.14",
|
||||||
|
"angular-translate": "2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
# CommaFeed settings
|
# CommaFeed settings
|
||||||
# ------------------
|
# ------------------
|
||||||
app:
|
app:
|
||||||
# context path of the application
|
|
||||||
contextPath: /
|
|
||||||
|
|
||||||
# url used to access commafeed
|
# url used to access commafeed
|
||||||
publicUrl: http://localhost:8082/
|
publicUrl: http://localhost:8082/
|
||||||
|
|
||||||
# wether to allow user registrations
|
# wether to allow user registrations
|
||||||
allowRegistrations: true
|
allowRegistrations: true
|
||||||
|
|
||||||
|
# create a demo account the first time the app starts
|
||||||
|
createDemoAccount: false
|
||||||
|
|
||||||
# put your google analytics tracking code here
|
# put your google analytics tracking code here
|
||||||
googleAnalyticsTrackingCode:
|
googleAnalyticsTrackingCode:
|
||||||
|
|
||||||
|
# put your google server key (used for youtube favicon fetching)
|
||||||
|
googleAuthKey:
|
||||||
|
|
||||||
# number of http threads
|
# number of http threads
|
||||||
backgroundThreads: 3
|
backgroundThreads: 3
|
||||||
|
|
||||||
@@ -47,6 +50,9 @@ app:
|
|||||||
# time to keep unread statuses (in days), 0 to disable
|
# time to keep unread statuses (in days), 0 to disable
|
||||||
keepStatusDays: 0
|
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 service to use, possible values are 'noop' and 'redis'
|
||||||
cache: noop
|
cache: noop
|
||||||
|
|
||||||
@@ -69,17 +75,12 @@ app:
|
|||||||
|
|
||||||
database:
|
database:
|
||||||
driverClass: org.h2.Driver
|
driverClass: org.h2.Driver
|
||||||
url: jdbc:h2:./target/example
|
url: jdbc:h2:./target/example;mv_store=false
|
||||||
user: sa
|
user: sa
|
||||||
password: sa
|
password: sa
|
||||||
properties:
|
properties:
|
||||||
charSet: UTF-8
|
charSet: UTF-8
|
||||||
maxWaitForConnection: 1s
|
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
||||||
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
|
||||||
minSize: 1
|
|
||||||
maxSize: 50
|
|
||||||
checkConnectionWhileIdle: true
|
|
||||||
maxConnectionAge: 30m
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
applicationConnectors:
|
applicationConnectors:
|
||||||
@@ -94,7 +95,7 @@ logging:
|
|||||||
loggers:
|
loggers:
|
||||||
com.commafeed: DEBUG
|
com.commafeed: DEBUG
|
||||||
liquibase: INFO
|
liquibase: INFO
|
||||||
org.hibernate.SQL: ALL
|
org.hibernate.SQL: INFO # or ALL for sql debugging
|
||||||
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
|
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
|
||||||
appenders:
|
appenders:
|
||||||
- type: console
|
- type: console
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
# CommaFeed settings
|
# CommaFeed settings
|
||||||
# ------------------
|
# ------------------
|
||||||
app:
|
app:
|
||||||
# context path of the application
|
|
||||||
contextPath: /
|
|
||||||
|
|
||||||
# url used to access commafeed
|
# url used to access commafeed
|
||||||
publicUrl: http://localhost:8082/
|
publicUrl: http://localhost:8082/
|
||||||
|
|
||||||
# wether to allow user registrations
|
# wether to allow user registrations
|
||||||
allowRegistrations: false
|
allowRegistrations: false
|
||||||
|
|
||||||
|
# create a demo account the first time the app starts
|
||||||
|
createDemoAccount: false
|
||||||
|
|
||||||
# put your google analytics tracking code here
|
# put your google analytics tracking code here
|
||||||
googleAnalyticsTrackingCode:
|
googleAnalyticsTrackingCode:
|
||||||
|
|
||||||
|
# put your google server key (used for youtube favicon fetching)
|
||||||
|
googleAuthKey:
|
||||||
|
|
||||||
# number of http threads
|
# number of http threads
|
||||||
backgroundThreads: 3
|
backgroundThreads: 3
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ app:
|
|||||||
smtpTls: false
|
smtpTls: false
|
||||||
smtpUserName:
|
smtpUserName:
|
||||||
smtpPassword:
|
smtpPassword:
|
||||||
|
smtpFromAddress:
|
||||||
|
|
||||||
# wether this commafeed instance has a lot of feeds to refresh
|
# wether this commafeed instance has a lot of feeds to refresh
|
||||||
# leave this to false in almost all cases
|
# leave this to false in almost all cases
|
||||||
@@ -47,6 +51,9 @@ app:
|
|||||||
# time to keep unread statuses (in days), 0 to disable
|
# time to keep unread statuses (in days), 0 to disable
|
||||||
keepStatusDays: 0
|
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 service to use, possible values are 'noop' and 'redis'
|
||||||
cache: noop
|
cache: noop
|
||||||
|
|
||||||
@@ -69,17 +76,15 @@ app:
|
|||||||
|
|
||||||
database:
|
database:
|
||||||
driverClass: org.h2.Driver
|
driverClass: org.h2.Driver
|
||||||
url: jdbc:h2:/home/commafeed/db
|
url: jdbc:h2:/home/commafeed/db;mv_store=false
|
||||||
user: sa
|
user: sa
|
||||||
password: sa
|
password: sa
|
||||||
properties:
|
properties:
|
||||||
charSet: UTF-8
|
charSet: UTF-8
|
||||||
maxWaitForConnection: 1s
|
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
||||||
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
minSize: 1
|
||||||
minSize: 1
|
maxSize: 50
|
||||||
maxSize: 50
|
maxConnectionAge: 30m
|
||||||
checkConnectionWhileIdle: true
|
|
||||||
maxConnectionAge: 30m
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
applicationConnectors:
|
applicationConnectors:
|
||||||
|
|||||||
28
gulpfile.js
28
gulpfile.js
@@ -4,7 +4,6 @@ var revReplace = require('gulp-rev-replace');
|
|||||||
var minifyCSS = require('gulp-minify-css');
|
var minifyCSS = require('gulp-minify-css');
|
||||||
var uglify = require('gulp-uglify');
|
var uglify = require('gulp-uglify');
|
||||||
var filter = require('gulp-filter');
|
var filter = require('gulp-filter');
|
||||||
var bower = require('gulp-bower');
|
|
||||||
var connect = require('gulp-connect');
|
var connect = require('gulp-connect');
|
||||||
var modRewrite = require('connect-modrewrite');
|
var modRewrite = require('connect-modrewrite');
|
||||||
var sass = require('gulp-sass');
|
var sass = require('gulp-sass');
|
||||||
@@ -15,10 +14,6 @@ var SRC_DIR = 'src/main/app/';
|
|||||||
var TEMP_DIR = 'target/gulp/'
|
var TEMP_DIR = 'target/gulp/'
|
||||||
var BUILD_DIR = 'target/classes/assets/';
|
var BUILD_DIR = 'target/classes/assets/';
|
||||||
|
|
||||||
gulp.task('bower', function() {
|
|
||||||
return bower();
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task('images', function() {
|
gulp.task('images', function() {
|
||||||
return gulp.src(SRC_DIR + 'images/**/*').pipe(gulp.dest(BUILD_DIR + 'images'));
|
return gulp.src(SRC_DIR + 'images/**/*').pipe(gulp.dest(BUILD_DIR + 'images'));
|
||||||
});
|
});
|
||||||
@@ -27,31 +22,32 @@ gulp.task('i18n', function() {
|
|||||||
return gulp.src(SRC_DIR + 'i18n/**/*.js').pipe(gulp.dest(BUILD_DIR + 'i18n'));
|
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_png = SRC_DIR + '*.png';
|
||||||
var favicons_ico = SRC_DIR + '*.ico';
|
var favicons_ico = SRC_DIR + '*.ico';
|
||||||
var favicons_svg = SRC_DIR + '*.svg';
|
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() {
|
gulp.task('sass', function() {
|
||||||
return gulp.src(SRC_DIR + 'sass/app.scss').pipe(sass()).pipe(gulp.dest(TEMP_DIR + 'css'));
|
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 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-*';
|
var readabilicons = SRC_DIR + 'lib/readabilicons/webfont/fonts/readabilicons-*';
|
||||||
return gulp.src([font_awesome, zocial, readabilicons]).pipe(gulp.dest(BUILD_DIR + 'font'));
|
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 gif = SRC_DIR + 'lib/select2/*.gif';
|
||||||
var png = SRC_DIR + 'lib/select2/*.png';
|
var png = SRC_DIR + 'lib/select2/*.png';
|
||||||
return gulp.src([gif, png]).pipe(gulp.dest(BUILD_DIR + 'css'));
|
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 index_html = SRC_DIR + 'api/index.html';
|
||||||
var lib = SRC_DIR + 'lib/swagger-ui/dist/**/*';
|
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]).pipe(gulp.dest(BUILD_DIR + 'api'));
|
||||||
@@ -65,17 +61,17 @@ gulp.task('template-cache', function() {
|
|||||||
return gulp.src(SRC_DIR + 'templates/**/*.html').pipe(templateCache(options)).pipe(gulp.dest(TEMP_DIR + 'js'));
|
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({
|
var assets = useref.assets({
|
||||||
searchPath : [SRC_DIR, TEMP_DIR]
|
searchPath : [SRC_DIR, TEMP_DIR]
|
||||||
});
|
});
|
||||||
var jsFilter = filter("**/*.js");
|
var jsFilter = filter("**/*.js");
|
||||||
var cssFilter = filter("**/*.css");
|
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(
|
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({
|
var assets = useref.assets({
|
||||||
searchPath : [SRC_DIR, TEMP_DIR]
|
searchPath : [SRC_DIR, TEMP_DIR]
|
||||||
});
|
});
|
||||||
@@ -101,7 +97,9 @@ gulp.task('serve', function() {
|
|||||||
connect.server({
|
connect.server({
|
||||||
root : BUILD_DIR,
|
root : BUILD_DIR,
|
||||||
port : 8082,
|
port : 8082,
|
||||||
|
livereload : true,
|
||||||
middleware : function() {
|
middleware : function() {
|
||||||
|
var api = '^/api/(.*)$ http://localhost:8083/rest/$1 [P]';
|
||||||
var rest = '^/rest/(.*)$ 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 next = '^/next(.*)$ http://localhost:8083/next$1 [P]';
|
||||||
var logout = '^/logout$ http://localhost:8083/logout [P]';
|
var logout = '^/logout$ http://localhost:8083/logout [P]';
|
||||||
@@ -113,4 +111,4 @@ gulp.task('serve', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('dev', ['build-dev', 'watch', 'serve']);
|
gulp.task('dev', ['build-dev', 'watch', 'serve']);
|
||||||
gulp.task('default', ['build']);
|
gulp.task('default', ['build']);
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -4,17 +4,17 @@
|
|||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gulp": "3.8.7",
|
"bower": "1.4.1",
|
||||||
"gulp-rev": "1.0.0",
|
"gulp": "3.8.11",
|
||||||
"gulp-rev-replace": "0.3.0",
|
"gulp-rev": "4.0.0",
|
||||||
"gulp-minify-css": "0.3.7",
|
"gulp-rev-replace": "0.4.1",
|
||||||
"gulp-uglify": "0.3.1",
|
"gulp-minify-css": "1.1.5",
|
||||||
"gulp-filter": "1.0.0",
|
"gulp-uglify": "1.2.0",
|
||||||
"gulp-bower": "0.0.6",
|
"gulp-filter": "2.0.2",
|
||||||
"gulp-connect": "2.0.6",
|
"gulp-connect": "2.2.0",
|
||||||
"connect-modrewrite": "0.7.7",
|
"connect-modrewrite": "0.8.1",
|
||||||
"gulp-sass": "0.7.2",
|
"gulp-sass": "2.0.1",
|
||||||
"gulp-useref": "0.6.0",
|
"gulp-useref": "1.1.2",
|
||||||
"gulp-angular-templatecache": "1.3.0"
|
"gulp-angular-templatecache": "1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
197
pom.xml
197
pom.xml
@@ -4,19 +4,20 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>2.0.3</version>
|
<version>2.2.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>CommaFeed</name>
|
<name>CommaFeed</name>
|
||||||
|
|
||||||
<prerequisites>
|
<prerequisites>
|
||||||
<maven>3.0.0</maven>
|
<maven>3.0.5</maven>
|
||||||
</prerequisites>
|
</prerequisites>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<dropwizard.version>0.7.1</dropwizard.version>
|
<java.version>1.8</java.version>
|
||||||
<guice.version>3.0</guice.version>
|
<dropwizard.version>0.8.1</dropwizard.version>
|
||||||
<querydsl.version>3.5.0</querydsl.version>
|
<guice.version>4.0</guice.version>
|
||||||
|
<querydsl.version>3.6.4</querydsl.version>
|
||||||
<rome.version>1.5.0</rome.version>
|
<rome.version>1.5.0</rome.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -34,20 +35,53 @@
|
|||||||
<filtering>true</filtering>
|
<filtering>true</filtering>
|
||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</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>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.2</version>
|
<version>3.1</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>1.7</source>
|
<source>${java.version}</source>
|
||||||
<target>1.7</target>
|
<target>${java.version}</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>pl.project13.maven</groupId>
|
<groupId>pl.project13.maven</groupId>
|
||||||
<artifactId>git-commit-id-plugin</artifactId>
|
<artifactId>git-commit-id-plugin</artifactId>
|
||||||
<version>2.1.11</version>
|
<version>2.1.13</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<goals>
|
<goals>
|
||||||
@@ -98,7 +132,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.github.eirslett</groupId>
|
<groupId>com.github.eirslett</groupId>
|
||||||
<artifactId>frontend-maven-plugin</artifactId>
|
<artifactId>frontend-maven-plugin</artifactId>
|
||||||
<version>0.0.16</version>
|
<version>0.0.22</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>install node and npm</id>
|
<id>install node and npm</id>
|
||||||
@@ -107,8 +141,8 @@
|
|||||||
</goals>
|
</goals>
|
||||||
<phase>generate-resources</phase>
|
<phase>generate-resources</phase>
|
||||||
<configuration>
|
<configuration>
|
||||||
<nodeVersion>v0.10.30</nodeVersion>
|
<nodeVersion>v0.12.4</nodeVersion>
|
||||||
<npmVersion>1.3.8</npmVersion>
|
<npmVersion>2.10.1</npmVersion>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
<execution>
|
<execution>
|
||||||
@@ -118,6 +152,15 @@
|
|||||||
</goals>
|
</goals>
|
||||||
<phase>generate-resources</phase>
|
<phase>generate-resources</phase>
|
||||||
</execution>
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>bower install</id>
|
||||||
|
<goals>
|
||||||
|
<goal>bower</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<arguments>install</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
<execution>
|
<execution>
|
||||||
<id>gulp build</id>
|
<id>gulp build</id>
|
||||||
<goals>
|
<goals>
|
||||||
@@ -130,7 +173,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
<version>2.5</version>
|
<version>2.6</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<archive>
|
<archive>
|
||||||
<manifest>
|
<manifest>
|
||||||
@@ -139,29 +182,6 @@
|
|||||||
</archive>
|
</archive>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</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>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
@@ -169,13 +189,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.14.8</version>
|
<version>1.16.4</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
<version>1.7.7</version>
|
<version>1.7.12</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -199,11 +219,6 @@
|
|||||||
<artifactId>dropwizard-hibernate</artifactId>
|
<artifactId>dropwizard-hibernate</artifactId>
|
||||||
<version>${dropwizard.version}</version>
|
<version>${dropwizard.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>io.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-client</artifactId>
|
|
||||||
<version>${dropwizard.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.dropwizard</groupId>
|
<groupId>io.dropwizard</groupId>
|
||||||
<artifactId>dropwizard-migrations</artifactId>
|
<artifactId>dropwizard-migrations</artifactId>
|
||||||
@@ -218,13 +233,24 @@
|
|||||||
<groupId>io.dropwizard</groupId>
|
<groupId>io.dropwizard</groupId>
|
||||||
<artifactId>dropwizard-forms</artifactId>
|
<artifactId>dropwizard-forms</artifactId>
|
||||||
<version>${dropwizard.version}</version>
|
<version>${dropwizard.version}</version>
|
||||||
<type>pom</type>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<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>
|
<dependency>
|
||||||
<groupId>com.wordnik</groupId>
|
<groupId>com.wordnik</groupId>
|
||||||
<artifactId>swagger-jaxrs_2.10</artifactId>
|
<artifactId>swagger-jaxrs</artifactId>
|
||||||
<version>1.3.10</version>
|
<version>1.5.3-M1</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>javax.ws.rs</groupId>
|
<groupId>javax.ws.rs</groupId>
|
||||||
@@ -232,7 +258,7 @@
|
|||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.mysema.querydsl</groupId>
|
<groupId>com.mysema.querydsl</groupId>
|
||||||
<artifactId>querydsl-apt</artifactId>
|
<artifactId>querydsl-apt</artifactId>
|
||||||
@@ -245,7 +271,12 @@
|
|||||||
<artifactId>querydsl-jpa</artifactId>
|
<artifactId>querydsl-jpa</artifactId>
|
||||||
<version>${querydsl.version}</version>
|
<version>${querydsl.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>18.0</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId>
|
||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
@@ -259,65 +290,101 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-codec</groupId>
|
<groupId>commons-codec</groupId>
|
||||||
<artifactId>commons-codec</artifactId>
|
<artifactId>commons-codec</artifactId>
|
||||||
<version>1.9</version>
|
<version>1.10</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-math3</artifactId>
|
<artifactId>commons-math3</artifactId>
|
||||||
<version>3.3</version>
|
<version>3.5</version>
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>redis.clients</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
<version>2.6.0</version>
|
<version>2.7.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.sun.mail</groupId>
|
<groupId>com.sun.mail</groupId>
|
||||||
<artifactId>javax.mail</artifactId>
|
<artifactId>javax.mail</artifactId>
|
||||||
<version>1.5.2</version>
|
<version>1.5.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- upgrade jdom to 2.0.5 for performance reasons (https://github.com/hunterhacker/jdom/issues/112) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.rometools</groupId>
|
<groupId>com.rometools</groupId>
|
||||||
<artifactId>rome</artifactId>
|
<artifactId>rome</artifactId>
|
||||||
<version>${rome.version}</version>
|
<version>${rome.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>jdom</artifactId>
|
||||||
|
<groupId>org.jdom</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.rometools</groupId>
|
<groupId>com.rometools</groupId>
|
||||||
<artifactId>rome-opml</artifactId>
|
<artifactId>rome-opml</artifactId>
|
||||||
<version>${rome.version}</version>
|
<version>${rome.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jdom</groupId>
|
||||||
|
<artifactId>jdom2</artifactId>
|
||||||
|
<version>2.0.6</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
<artifactId>jsoup</artifactId>
|
<artifactId>jsoup</artifactId>
|
||||||
<version>1.8.1</version>
|
<version>1.8.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.googlecode.juniversalchardet</groupId>
|
<groupId>com.ibm.icu</groupId>
|
||||||
<artifactId>juniversalchardet</artifactId>
|
<artifactId>icu4j</artifactId>
|
||||||
<version>1.0.3</version>
|
<version>55.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.cssparser</groupId>
|
<groupId>net.sourceforge.cssparser</groupId>
|
||||||
<artifactId>cssparser</artifactId>
|
<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>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
<version>1.4.182</version>
|
<version>1.4.187</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>mysql</groupId>
|
<groupId>mysql</groupId>
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
<version>5.1.33</version>
|
<version>5.1.35</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<version>9.3-1102-jdbc41</version>
|
<version>9.4-1201-jdbc41</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.jtds</groupId>
|
<groupId>net.sourceforge.jtds</groupId>
|
||||||
@@ -328,13 +395,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
<version>4.11</version>
|
<version>4.12</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<version>1.10.8</version>
|
<version>2.0.11-beta</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Swagger UI</title>
|
<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/reset.css' media='screen' rel='stylesheet' type='text/css'/>
|
||||||
<link href='css/screen.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'/>
|
<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.slideto.min.js' type='text/javascript'></script>
|
||||||
<script src='lib/jquery.wiggle.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/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/underscore-min.js' type='text/javascript'></script>
|
||||||
<script src='lib/backbone-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='swagger-ui.js' type='text/javascript'></script>
|
||||||
<script src='lib/highlight.7.3.pack.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 -->
|
<!-- enabling this will enable oauth2 implicit scope support -->
|
||||||
<script src='lib/swagger-oauth.js' type='text/javascript'></script>
|
<script src='lib/swagger-oauth.js' type='text/javascript'></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function () {
|
$(function () {
|
||||||
window.swaggerUi = new SwaggerUi({
|
window.swaggerUi = new SwaggerUi({
|
||||||
url: "../rest/api-docs",
|
url: "../rest/swagger.json",
|
||||||
dom_id: "swagger-ui-container",
|
dom_id: "swagger-ui-container",
|
||||||
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
|
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
|
||||||
onComplete: function(swaggerApi, swaggerUi){
|
onComplete: function(swaggerApi, swaggerUi){
|
||||||
log("Loaded SwaggerUI");
|
|
||||||
|
|
||||||
if(typeof initOAuth == "function") {
|
if(typeof initOAuth == "function") {
|
||||||
/*
|
/*
|
||||||
initOAuth({
|
initOAuth({
|
||||||
|
|||||||
BIN
src/main/app/app-icon-192.png
Normal file
BIN
src/main/app/app-icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
@@ -98,6 +98,8 @@
|
|||||||
"next_refresh" : "Next refresh",
|
"next_refresh" : "Next refresh",
|
||||||
"queued_for_refresh" : "Queued for refresh",
|
"queued_for_refresh" : "Queued for refresh",
|
||||||
"feed_url" : "Feed URL",
|
"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.",
|
"generate_api_key_first" : "Generate an API key in your profile first.",
|
||||||
"unsubscribe" : "Unsubscribe",
|
"unsubscribe" : "Unsubscribe",
|
||||||
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed?",
|
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed?",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"new_category" : {
|
"new_category" : {
|
||||||
"name" : "Nome",
|
"name" : "Nome",
|
||||||
"parent" : "Pai"
|
"parent" : "Subcategoría de "
|
||||||
},
|
},
|
||||||
"toolbar" : {
|
"toolbar" : {
|
||||||
"unread" : "Sen Ler",
|
"unread" : "Sen Ler",
|
||||||
@@ -39,12 +39,12 @@
|
|||||||
"previous_entry" : "Entrada Anterior",
|
"previous_entry" : "Entrada Anterior",
|
||||||
"next_entry" : "Próxima Entrada",
|
"next_entry" : "Próxima Entrada",
|
||||||
"refresh" : "Actualizar",
|
"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",
|
"sort_by_asc_desc" : "Ordenar por data asc/desc",
|
||||||
"titles_only" : "Só títulos",
|
"titles_only" : "Só títulos",
|
||||||
"expanded_view" : "Vista expandida",
|
"expanded_view" : "Vista expandida",
|
||||||
"mark_all_as_read" : "Marcar todos como lidos",
|
"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_day" : "Artigos anteriores a un día",
|
||||||
"mark_all_older_week" : "Artigos de máis de unha semana",
|
"mark_all_older_week" : "Artigos de máis de unha semana",
|
||||||
"mark_all_older_two_weeks" : "Artigos de máis de dúas semanas",
|
"mark_all_older_two_weeks" : "Artigos de máis de dúas semanas",
|
||||||
@@ -56,14 +56,14 @@
|
|||||||
"donate" : "Doar"
|
"donate" : "Doar"
|
||||||
},
|
},
|
||||||
"view" : {
|
"view" : {
|
||||||
"entry_source" : "from ",
|
"entry_source" : "desde ",
|
||||||
"entry_author" : "by ",
|
"entry_author" : "por ",
|
||||||
"error_while_loading_feed" : "Erro mentras se cargaba esta fonte",
|
"error_while_loading_feed" : "Erro mentras se cargaba esta fonte",
|
||||||
"keep_unread" : "Gardar non lidos",
|
"keep_unread" : "Gardar non lidos",
|
||||||
"no_unread_items" : "non ten elementos sen ler.",
|
"no_unread_items" : "non ten elementos sen ler.",
|
||||||
"mark_up_to_here" : "Mark as read up to here ",
|
"mark_up_to_here" : "Marcar como lidos ate aquí ",
|
||||||
"search_for" : "searching for: ",
|
"search_for" : "buscando por: ",
|
||||||
"no_search_results" : "No match found for the requested keywords "
|
"no_search_results" : "Sen coincidencias para as palabras introducidas "
|
||||||
},
|
},
|
||||||
"feedsearch" : {
|
"feedsearch" : {
|
||||||
"hint" : "Escriba unha suscrición...",
|
"hint" : "Escriba unha suscrición...",
|
||||||
@@ -80,8 +80,8 @@
|
|||||||
"scroll_marks" : "En vista expandida, o desplazamento polas entradas márcaas como lidas."
|
"scroll_marks" : "En vista expandida, o desplazamento polas entradas márcaas como lidas."
|
||||||
},
|
},
|
||||||
"appearance" : "Aspecto",
|
"appearance" : "Aspecto",
|
||||||
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ",
|
"scroll_speed" : "Velocidade de desplazamento navegando entre entradas (en milisegundos) ",
|
||||||
"scroll_speed_help" : "set to 0 to disable ",
|
"scroll_speed_help" : "escriba 0 para deshabilitar ",
|
||||||
"theme" : "Decorado",
|
"theme" : "Decorado",
|
||||||
"submit_your_theme" : "Envíe o seu decorado",
|
"submit_your_theme" : "Envíe o seu decorado",
|
||||||
"custom_css" : "CSS Personalizado"
|
"custom_css" : "CSS Personalizado"
|
||||||
@@ -89,21 +89,21 @@
|
|||||||
"details" : {
|
"details" : {
|
||||||
"feed_details" : "Detalles de fontes",
|
"feed_details" : "Detalles de fontes",
|
||||||
"url" : "URL",
|
"url" : "URL",
|
||||||
"website" : "Website ",
|
"website" : "Sitio web ",
|
||||||
"name" : "Nome",
|
"name" : "Nome",
|
||||||
"category" : "Categoría",
|
"category" : "Categoría",
|
||||||
"position" : "Position ",
|
"position" : "Posición ",
|
||||||
"last_refresh" : "Última actualización",
|
"last_refresh" : "Última actualización",
|
||||||
"message" : "Last refresh message ",
|
"message" : "Última mensaxe da actualización ",
|
||||||
"next_refresh" : "Próxima actualización",
|
"next_refresh" : "Próxima actualización",
|
||||||
"queued_for_refresh" : "En cola para actualizar",
|
"queued_for_refresh" : "En cola para actualizar",
|
||||||
"feed_url" : "URL da fonte",
|
"feed_url" : "URL da fonte",
|
||||||
"generate_api_key_first" : "Antes debes xerar unha chave API no teu perfil.",
|
"generate_api_key_first" : "Antes debes xerar unha chave API no teu perfil.",
|
||||||
"unsubscribe" : "Rematar suscripción",
|
"unsubscribe" : "Rematar suscripción",
|
||||||
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ",
|
"unsubscribe_confirmation" : "Seguro que queres desuscribirte de esta fonte? ",
|
||||||
"delete_category_confirmation" : "Are you sure you want to delete this category? ",
|
"delete_category_confirmation" : "Seguro que queres eliminar esta categoría? ",
|
||||||
"category_details" : "Detalles da categoría",
|
"category_details" : "Detalles da categoría",
|
||||||
"tag_details" : "Tag details ",
|
"tag_details" : "Detalles da etiqueta ",
|
||||||
"parent_category" : "Categoría principal"
|
"parent_category" : "Categoría principal"
|
||||||
},
|
},
|
||||||
"profile" : {
|
"profile" : {
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"generate_new_api_key_info" : "Ao cambiar o contrasinal xerarase unha nova chave API",
|
"generate_new_api_key_info" : "Ao cambiar o contrasinal xerarase unha nova chave API",
|
||||||
"opml_export" : "Exportación de OPML",
|
"opml_export" : "Exportación de OPML",
|
||||||
"delete_account" : "Eliminar conta",
|
"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" : {
|
"about" : {
|
||||||
"rest_api" : {
|
"rest_api" : {
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
"open_next_entry" : "abrir próxima entrada",
|
"open_next_entry" : "abrir próxima entrada",
|
||||||
"open_previous_entry" : "abrir entrada anterior",
|
"open_previous_entry" : "abrir entrada anterior",
|
||||||
"spacebar" : "space/shift+space ",
|
"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_next_entry" : "Establecer o foco na próxima entrada sen abrila",
|
||||||
"focus_previous_entry" : "Establecer o foco na entrada anterior sen abrila",
|
"focus_previous_entry" : "Establecer o foco na entrada anterior sen abrila",
|
||||||
"open_next_feed" : "abrir a seguinte fonte ou categoría",
|
"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_current_entry" : "marcar como lida/non lida a entrada actual",
|
||||||
"mark_all_as_read" : "marcar todas as entradas como lidas",
|
"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",
|
"open_in_new_tab_mark_as_read" : "abrir entrada nunha nova lapela e marcar como lida",
|
||||||
"fullscreen" : "toggle full screen mode ",
|
"fullscreen" : "habilita a pantalla completa ",
|
||||||
"font_size" : "increase/decrease font size of the current entry ",
|
"font_size" : "aumenta/diminúe o tamaño da letra da entrada activa ",
|
||||||
"go_to_all" : "go to the All view ",
|
"go_to_all" : "ir a vista TODOS",
|
||||||
"go_to_starred" : "go to the Starred view ",
|
"go_to_starred" : "ir a vista Destacados ",
|
||||||
"feed_search" : "navegue ate unha suscrición introducindo o nome da suscrición"
|
"feed_search" : "navegue ate unha suscrición introducindo o nome da suscrición"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
"save" : "Salva",
|
"save" : "Salva",
|
||||||
"cancel" : "Cancella",
|
"cancel" : "Cancella",
|
||||||
"delete" : "Elimina",
|
"delete" : "Elimina",
|
||||||
"required" : "Required",
|
"required" : "Richiesto",
|
||||||
"download" : "Download",
|
"download" : "Download",
|
||||||
"link" : "Link",
|
"link" : "Link",
|
||||||
"bookmark" : "Segnalibro ",
|
"bookmark" : "Segnalibro",
|
||||||
"close" : "Chiudi",
|
"close" : "Chiudi",
|
||||||
"tags" : "Tags "
|
"tags" : "Etichette "
|
||||||
},
|
},
|
||||||
"tree" : {
|
"tree" : {
|
||||||
"subscribe" : "Iscriviti",
|
"subscribe" : "Abbonati",
|
||||||
"import" : "Importa",
|
"import" : "Importa",
|
||||||
"new_category" : "Nuova categoria",
|
"new_category" : "Nuova categoria",
|
||||||
"all" : "Tutto",
|
"all" : "Tutto",
|
||||||
@@ -19,19 +19,19 @@
|
|||||||
},
|
},
|
||||||
"subscribe" : {
|
"subscribe" : {
|
||||||
"feed_url" : "Feed URL",
|
"feed_url" : "Feed URL",
|
||||||
"feed_name" : "Nome Feed",
|
"feed_name" : "Nome feed",
|
||||||
"category" : "Categoria"
|
"category" : "Categoria"
|
||||||
},
|
},
|
||||||
"import" : {
|
"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_reader_suffix" : " account.",
|
||||||
"google_download" : "Oppure, carica il tuo subscriptions.xml",
|
"google_download" : "Oppure, carica il tuo file subscriptions.xml.",
|
||||||
"google_download_link" : "Scaricalo da qui",
|
"google_download_link" : "Scaricalo da qui.",
|
||||||
"xml_file" : "OPML File"
|
"xml_file" : "OPML File"
|
||||||
},
|
},
|
||||||
"new_category" : {
|
"new_category" : {
|
||||||
"name" : "Nome",
|
"name" : "Nome",
|
||||||
"parent" : "Parent"
|
"parent" : "Gruppo"
|
||||||
},
|
},
|
||||||
"toolbar" : {
|
"toolbar" : {
|
||||||
"unread" : "Non letti",
|
"unread" : "Non letti",
|
||||||
@@ -39,12 +39,12 @@
|
|||||||
"previous_entry" : "Precedente",
|
"previous_entry" : "Precedente",
|
||||||
"next_entry" : "Successivo",
|
"next_entry" : "Successivo",
|
||||||
"refresh" : "Ricarica",
|
"refresh" : "Ricarica",
|
||||||
"refresh_all" : "Force refresh all my feeds ",
|
"refresh_all" : "Forza l'aggiornamento di tutte i miei feed",
|
||||||
"sort_by_asc_desc" : "Sort by date asc/desc",
|
"sort_by_asc_desc" : "Ordina per data ascendente/decrescente",
|
||||||
"titles_only" : "Solo titoli",
|
"titles_only" : "Solo i titoli",
|
||||||
"expanded_view" : "Espandi",
|
"expanded_view" : "Espandi",
|
||||||
"mark_all_as_read" : "Contrassegna tutto come già letto",
|
"mark_all_as_read" : "Segna tutto come già letto",
|
||||||
"mark_all_older_12_hours" : "Items older than 12 hours ",
|
"mark_all_older_12_hours" : "Elementi più vecchi di 12 ore",
|
||||||
"mark_all_older_day" : "Elementi più vecchi di un giorno",
|
"mark_all_older_day" : "Elementi più vecchi di un giorno",
|
||||||
"mark_all_older_week" : "Elementi più vecchi di una settimana",
|
"mark_all_older_week" : "Elementi più vecchi di una settimana",
|
||||||
"mark_all_older_two_weeks" : "Elementi più vecchi di due settimane",
|
"mark_all_older_two_weeks" : "Elementi più vecchi di due settimane",
|
||||||
@@ -52,129 +52,131 @@
|
|||||||
"profile" : "Profilo",
|
"profile" : "Profilo",
|
||||||
"admin" : "Admin",
|
"admin" : "Admin",
|
||||||
"about" : "Informazioni",
|
"about" : "Informazioni",
|
||||||
"logout" : "Logout",
|
"logout" : "Esci",
|
||||||
"donate" : "Dona"
|
"donate" : "Dona"
|
||||||
},
|
},
|
||||||
"view" : {
|
"view" : {
|
||||||
"entry_source" : "from ",
|
"entry_source" : "da ",
|
||||||
"entry_author" : "by ",
|
"entry_author" : "di ",
|
||||||
"error_while_loading_feed" : "Si è verificato un errore, mentre caricavo il feed",
|
"error_while_loading_feed" : "Si è verificato un errore durante il caricamento di questo feed",
|
||||||
"keep_unread" : "Mantiene come da leggere",
|
"keep_unread" : "Mantiene come non leggere",
|
||||||
"no_unread_items" : "Non ci sono elementi da leggere.",
|
"no_unread_items" : "Non ci sono elementi da leggere.",
|
||||||
"mark_up_to_here" : "Mark as read up to here ",
|
"mark_up_to_here" : "Segna come letto fino qui",
|
||||||
"search_for" : "searching for: ",
|
"search_for" : "cercando: ",
|
||||||
"no_search_results" : "No match found for the requested keywords "
|
"no_search_results" : "Nessun risultato trovato per le parole chiave cercate"
|
||||||
},
|
},
|
||||||
"feedsearch" : {
|
"feedsearch" : {
|
||||||
"hint" : "Type in a subscription... ",
|
"hint" : "Digita in una sottoscrizione... ",
|
||||||
"help" : "Use the return key to select and arrow keys to navigate. ",
|
"help" : "Usa il tasto invio per selezionare e le frecce per navigare.",
|
||||||
"result_prefix" : "Le tue sottoscrizioni"
|
"result_prefix" : "Le tue sottoscrizioni:"
|
||||||
},
|
},
|
||||||
"settings" : {
|
"settings" : {
|
||||||
"general" : {
|
"general" : {
|
||||||
"value" : "Generali",
|
"value" : "Generali",
|
||||||
"language" : "Lingua",
|
"language" : "Lingua",
|
||||||
"language_contribute" : "Contribuisci con le traduzioni",
|
"language_contribute" : "Contribuisci nelle traduzioni",
|
||||||
"show_unread" : "Show feeds and categories with no unread entries",
|
"show_unread" : "Mostra i feed e le categorie con elementi non letti",
|
||||||
"social_buttons" : "Visualizza i social button",
|
"social_buttons" : "Mostra i pulsanti social network di condivisione",
|
||||||
"scroll_marks" : "Marca come letto quando scorri"
|
"scroll_marks" : "In modalità estesa, segna come letto le voci quando scorri"
|
||||||
},
|
},
|
||||||
"appearance" : "Appearance ",
|
"appearance" : "Aspetto",
|
||||||
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ",
|
"scroll_speed" : "Velocità dello scorrimento durante la navigazione tra i feed (in millisecondi) ",
|
||||||
"scroll_speed_help" : "set to 0 to disable ",
|
"scroll_speed_help" : "Imposta 0 per disabilitare",
|
||||||
"theme" : "Tema ",
|
"theme" : "Tema",
|
||||||
"submit_your_theme" : "Sottoponi il tuo tema ",
|
"submit_your_theme" : "Proponi il tuo tema",
|
||||||
"custom_css" : "Css modificato"
|
"custom_css" : "CSS personalizzato"
|
||||||
},
|
},
|
||||||
"details" : {
|
"details" : {
|
||||||
"feed_details" : "Dettagli feed",
|
"feed_details" : "Dettagli feed",
|
||||||
"url" : "URL",
|
"url" : "URL ",
|
||||||
"website" : "Website ",
|
"website" : "Sito Web",
|
||||||
"name" : "Nome",
|
"name" : "Nome",
|
||||||
"category" : "Categoria",
|
"category" : "Categoria",
|
||||||
"position" : "Posizione ",
|
"position" : "Posizione",
|
||||||
"last_refresh" : "Ultimo aggiornamento",
|
"last_refresh" : "Ultimo aggiornamento",
|
||||||
"message" : "Last refresh message ",
|
"message" : "Ultimo messaggio di aggiornamento",
|
||||||
"next_refresh" : "Next refresh ",
|
"next_refresh" : "Prossimo aggiornamento",
|
||||||
"queued_for_refresh" : "In attesa per l'aggiornamento ",
|
"queued_for_refresh" : "In attesa per l'aggiornamento",
|
||||||
"feed_url" : "Feed URL",
|
"feed_url" : "URL del feed ",
|
||||||
"generate_api_key_first" : "Generate an API key in your profile first.",
|
"filtering_expression" : "Espressione del filtro",
|
||||||
"unsubscribe" : "Annulla l\"'\"iscrizione",
|
"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>.",
|
||||||
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ",
|
"generate_api_key_first" : "Genera prima una chiave API nelle impostazioni del tuo profilo.",
|
||||||
"delete_category_confirmation" : "Are you sure you want to delete this category? ",
|
"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",
|
"category_details" : "Dettagli categoria",
|
||||||
"tag_details" : "Tag details ",
|
"tag_details" : "Dettagli etichette ",
|
||||||
"parent_category" : "Parent category"
|
"parent_category" : "Categoria principale"
|
||||||
},
|
},
|
||||||
"profile" : {
|
"profile" : {
|
||||||
"user_name" : "User name",
|
"user_name" : "Nome utente",
|
||||||
"email" : "E-mail",
|
"email" : "E-mail",
|
||||||
"change_password" : "Cambia password",
|
"change_password" : "Cambia password",
|
||||||
"confirm_password" : "Conferma password",
|
"confirm_password" : "Conferma password",
|
||||||
"minimum_6_chars" : "Minimo 6 caratteri",
|
"minimum_6_chars" : "Minimo 6 caratteri",
|
||||||
"passwords_do_not_match" : "Le password non corrispondono",
|
"passwords_do_not_match" : "Le password non corrispondono",
|
||||||
"api_key" : "API key",
|
"api_key" : "chiave API",
|
||||||
"api_key_not_generated" : "Non generata ancora",
|
"api_key_not_generated" : "Non ancora generata",
|
||||||
"generate_new_api_key" : "Genera una nuova chiave API",
|
"generate_new_api_key" : "Genera una nuova chiave API ",
|
||||||
"generate_new_api_key_info" : "Cambiando la password sarà generata una nuova chiave API",
|
"generate_new_api_key_info" : "Cambiando la password sarà generata una nuova chiave API ì",
|
||||||
"opml_export" : "Esporta OPML",
|
"opml_export" : "Esporta OPML",
|
||||||
"delete_account" : "Elimina account",
|
"delete_account" : "Elimina il profilo",
|
||||||
"delete_account_confirmation" : "Delete your acount? There's no turning back! "
|
"delete_account_confirmation" : "Eliminare il tuo profilo? Non si può tornare indietro!"
|
||||||
},
|
},
|
||||||
"about" : {
|
"about" : {
|
||||||
"rest_api" : {
|
"rest_api" : {
|
||||||
"value" : "REST API",
|
"value" : "REST API",
|
||||||
"line1" : "CommaFeed is built on top of JAX-RS and AngularJS. As such, a REST API is available.",
|
"line1" : "CommaFeed è costruito sopra JAX-RS e AngularJS. Ed ovviamente, una REST API è disponibile.",
|
||||||
"link_to_documentation" : "Link alla documentazione."
|
"link_to_documentation" : "Collegamento alla documentazione."
|
||||||
},
|
},
|
||||||
"keyboard_shortcuts" : "Scorciatoie da tastiera",
|
"keyboard_shortcuts" : "Scorciatoie da tastiera",
|
||||||
"version" : "CommaFeed version ",
|
"version" : "Versione di CommaFeed",
|
||||||
"line1_prefix" : "Commefeed è un progetto open-source. I codici sono hostati su ",
|
"line1_prefix" : "CommaFeed è un progetto open source. I codici sono ospitati su ",
|
||||||
"line1_suffix" : ".",
|
"line1_suffix" : ".",
|
||||||
"line2_prefix" : "Se hai qualche problema, segnalalo sulla pagina del ",
|
"line2_prefix" : "Se hai qualche problema, segnalalo sulla pagina del ",
|
||||||
"line2_suffix" : " progetto.",
|
"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.",
|
"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" : "Per chi preferisce Bitcoin, questo è l\"'\"indirizzo ",
|
"line4" : "Se preferisci i Bitcoin, questo è l'indirizzo",
|
||||||
"goodies" : {
|
"goodies" : {
|
||||||
"value" : "Goodies",
|
"value" : "Goodies",
|
||||||
"android_app" : "Android app ",
|
"android_app" : "Applicazione Android",
|
||||||
"subscribe_url" : "Subscribe URL ",
|
"subscribe_url" : "Sottoscrivi URL",
|
||||||
"chrome_extension" : "Estenzione per Chrome ",
|
"chrome_extension" : "Estensione per Chrome",
|
||||||
"firefox_extension" : "Estensione per Firefox",
|
"firefox_extension" : "Estensione per Firefox",
|
||||||
"opera_extension" : "Estensione per Opera",
|
"opera_extension" : "Estensione per Opera",
|
||||||
"subscribe_bookmarklet" : "Add subscription bookmarklet (click) ",
|
"subscribe_bookmarklet" : "Aggiungi la sottoscrizione ai segnalibri (clicca)",
|
||||||
"subscribe_bookmarklet_asc" : "Oldest first ",
|
"subscribe_bookmarklet_asc" : "I più vecchi prima",
|
||||||
"subscribe_bookmarklet_desc" : "Newest first ",
|
"subscribe_bookmarklet_desc" : "I più nuovi prima",
|
||||||
"next_unread_bookmarklet" : "Next unread item bookmarklet (drag to bookmark bar) "
|
"next_unread_bookmarklet" : "Prossimo elemento non letto nei segnalibri (trascinali nella barra dei segnalibri)"
|
||||||
},
|
},
|
||||||
"translation" : {
|
"translation" : {
|
||||||
"value" : "Traduzioni",
|
"value" : "Traduzioni",
|
||||||
"message" : "Abbiamo bisogno del tuo aiuto per tradurre CommaFeed.",
|
"message" : "Abbiamo bisogno del tuo aiuto per tradurre CommaFeed.",
|
||||||
"link" : "Vedi come aiutare con le traduzioni."
|
"link" : "Vedi come aiutarci nella traduzioni."
|
||||||
},
|
},
|
||||||
"announcements" : "Annunci",
|
"announcements" : "Annunci",
|
||||||
"shortcuts" : {
|
"shortcuts" : {
|
||||||
"mouse_middleclick" : "mouse middleclick",
|
"mouse_middleclick" : "click centrale del mouse",
|
||||||
"open_next_entry" : "open next entry",
|
"open_next_entry" : "apri l'elemento successivo",
|
||||||
"open_previous_entry" : "open previous entry",
|
"open_previous_entry" : "apri l'elemento precedente",
|
||||||
"spacebar" : "space/shift+space ",
|
"spacebar" : "spazio/shift+spazio",
|
||||||
"move_page_down_up" : "moves the page down/up ",
|
"move_page_down_up" : "muovi la pagina sopra/sotto",
|
||||||
"focus_next_entry" : "set focus on next entry without opening it ",
|
"focus_next_entry" : "imposta il fuoco sull'elemento successivo senza aprirlo",
|
||||||
"focus_previous_entry" : "set focus on previous entry without opening it ",
|
"focus_previous_entry" : "imposta il fuoco sull'elemento precedente senza aprirlo",
|
||||||
"open_next_feed" : "open next feed or category ",
|
"open_next_feed" : "apri il feed successivo od una categoria",
|
||||||
"open_previous_feed" : "open previous feed or category ",
|
"open_previous_feed" : "apri il feed precedente od una categoria",
|
||||||
"open_close_current_entry" : "open/close current entry",
|
"open_close_current_entry" : "apri/chiusi la categoria corrente",
|
||||||
"open_current_entry_in_new_window" : "open current entry in a new window",
|
"open_current_entry_in_new_window" : "apri il corrente elemento in una nuova finestra",
|
||||||
"open_current_entry_in_new_window_background" : "open current entry in a new window in the background ",
|
"open_current_entry_in_new_window_background" : "apri il corrente elemento in una nuova finestra in secondo piano",
|
||||||
"star_unstar" : "star/unstar current entry",
|
"star_unstar" : "segna/togli il segno all'elemento corrente",
|
||||||
"mark_current_entry" : "mark as read/unread current entry",
|
"mark_current_entry" : "segna come letto/non letto l'elemento corrente",
|
||||||
"mark_all_as_read" : "mark all entries as read",
|
"mark_all_as_read" : "segna come letti tutti gli elementi",
|
||||||
"open_in_new_tab_mark_as_read" : "open entry in new tab and mark as read",
|
"open_in_new_tab_mark_as_read" : "apri l'elemento in una nuova finestra e segnala come letta",
|
||||||
"fullscreen" : "toggle full screen mode ",
|
"fullscreen" : "alterna la modalità a schermo intero",
|
||||||
"font_size" : "increase/decrease font size of the current entry ",
|
"font_size" : "aumenta/decrementa la grandezza del font dell'elemento corrente",
|
||||||
"go_to_all" : "go to the All view ",
|
"go_to_all" : "vai nella visione totale",
|
||||||
"go_to_starred" : "go to the Starred view ",
|
"go_to_starred" : "vai nella visione dei preferiti",
|
||||||
"feed_search" : "navigate to a subscription by entering the subscription name "
|
"feed_search" : "naviga in una sottoscrizione scrivendo il suo nome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"link" : "Bağlantı",
|
"link" : "Bağlantı",
|
||||||
"bookmark" : "Yer imi",
|
"bookmark" : "Yer imi",
|
||||||
"close" : "Kapat",
|
"close" : "Kapat",
|
||||||
"tags" : "Tags "
|
"tags" : "Etiketler "
|
||||||
},
|
},
|
||||||
"tree" : {
|
"tree" : {
|
||||||
"subscribe" : "Abone ol",
|
"subscribe" : "Abone ol",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"import" : {
|
"import" : {
|
||||||
"google_reader_prefix" : "Aboneliklerinizi ",
|
"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" : "Veya, subscriptions.xml dosyanızı yükleyin.",
|
||||||
"google_download_link" : "Buradan indirebilirsiniz.",
|
"google_download_link" : "Buradan indirebilirsiniz.",
|
||||||
"xml_file" : "OPML dosyası"
|
"xml_file" : "OPML dosyası"
|
||||||
@@ -39,15 +39,15 @@
|
|||||||
"previous_entry" : "Önceki ileti",
|
"previous_entry" : "Önceki ileti",
|
||||||
"next_entry" : "Sonraki ileti",
|
"next_entry" : "Sonraki ileti",
|
||||||
"refresh" : "Yenile",
|
"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",
|
"sort_by_asc_desc" : "Tarihe göre sırala artan/azalan",
|
||||||
"titles_only" : "Sadece başlıklar",
|
"titles_only" : "Sadece başlıklar",
|
||||||
"expanded_view" : "Genişletilmiş görünüm",
|
"expanded_view" : "Genişletilmiş görünüm",
|
||||||
"mark_all_as_read" : "Tümünü okundu işaretle",
|
"mark_all_as_read" : "Tümünü okundu işaretle",
|
||||||
"mark_all_older_12_hours" : "Items older than 12 hours ",
|
"mark_all_older_12_hours" : "12 saatten daha eski yayınlar ",
|
||||||
"mark_all_older_day" : "Bir günden eski yazılar",
|
"mark_all_older_day" : "Bir günden eski yayınlar",
|
||||||
"mark_all_older_week" : "Bir haftadan eski yazılar",
|
"mark_all_older_week" : "Bir haftadan eski yayınlar",
|
||||||
"mark_all_older_two_weeks" : "İki haftadan eski yazılar",
|
"mark_all_older_two_weeks" : "İki haftadan eski yayınlar",
|
||||||
"settings" : "Ayarlar",
|
"settings" : "Ayarlar",
|
||||||
"profile" : "Profil",
|
"profile" : "Profil",
|
||||||
"admin" : "Yönetim",
|
"admin" : "Yönetim",
|
||||||
@@ -56,14 +56,14 @@
|
|||||||
"donate" : "Bağış"
|
"donate" : "Bağış"
|
||||||
},
|
},
|
||||||
"view" : {
|
"view" : {
|
||||||
"entry_source" : "from ",
|
"entry_source" : "kaynak: ",
|
||||||
"entry_author" : "by ",
|
"entry_author" : "yazar: ",
|
||||||
"error_while_loading_feed" : "Bu aboneliği çekerken hata oluştu.",
|
"error_while_loading_feed" : "Bu aboneliği çekerken hata oluştu.",
|
||||||
"keep_unread" : "Okunmadı olarak sakla",
|
"keep_unread" : "Okunmadı olarak sakla",
|
||||||
"no_unread_items" : "okunmamış ileti yok.",
|
"no_unread_items" : "Okunmamış ileti yok.",
|
||||||
"mark_up_to_here" : "Mark as read up to here ",
|
"mark_up_to_here" : "Buraya kadar olan bütün yayınları okundu olarak işaretle!",
|
||||||
"search_for" : "searching for: ",
|
"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" : {
|
"feedsearch" : {
|
||||||
"hint" : "Bir abonelik yazın...",
|
"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"
|
"scroll_marks" : "Genişletilmiş görünümde götüntülenen iletileri okunmuş işaretle"
|
||||||
},
|
},
|
||||||
"appearance" : "Görünüm",
|
"appearance" : "Görünüm",
|
||||||
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ",
|
"scroll_speed" : "İçerikler arasında gezinirken kaydırma hızı (milisaniye cinsinden)",
|
||||||
"scroll_speed_help" : "set to 0 to disable ",
|
"scroll_speed_help" : "ayarı kapatmak için 0 yazınız",
|
||||||
"theme" : "Tema",
|
"theme" : "Tema",
|
||||||
"submit_your_theme" : "Tema gönder",
|
"submit_your_theme" : "Tema gönder",
|
||||||
"custom_css" : "Kişiselleştirilmiş CSS"
|
"custom_css" : "Kişiselleştirilmiş CSS"
|
||||||
@@ -100,10 +100,10 @@
|
|||||||
"feed_url" : "Yayın URL'si",
|
"feed_url" : "Yayın URL'si",
|
||||||
"generate_api_key_first" : "Öncelikle profilinizden bir API anahtarı oluşturun.",
|
"generate_api_key_first" : "Öncelikle profilinizden bir API anahtarı oluşturun.",
|
||||||
"unsubscribe" : "Aboneliği iptal et",
|
"unsubscribe" : "Aboneliği iptal et",
|
||||||
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ",
|
"unsubscribe_confirmation" : "Bu yayından çıkmak istediğinizden emin misiniz? ",
|
||||||
"delete_category_confirmation" : "Are you sure you want to delete this category? ",
|
"delete_category_confirmation" : "Bu kategoriyi silmek istediğinizden emin misiniz? ",
|
||||||
"category_details" : "Kategori detayları",
|
"category_details" : "Kategori detayları",
|
||||||
"tag_details" : "Tag details ",
|
"tag_details" : "Etiket detayları ",
|
||||||
"parent_category" : "Üst kategori"
|
"parent_category" : "Üst kategori"
|
||||||
},
|
},
|
||||||
"profile" : {
|
"profile" : {
|
||||||
@@ -116,10 +116,10 @@
|
|||||||
"api_key" : "API anahtarı",
|
"api_key" : "API anahtarı",
|
||||||
"api_key_not_generated" : "Henüz oluşturulmadı",
|
"api_key_not_generated" : "Henüz oluşturulmadı",
|
||||||
"generate_new_api_key" : "Yeni bir API anahtarı oluştur",
|
"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",
|
"opml_export" : "OPML dışa aktar",
|
||||||
"delete_account" : "Hesabı sil",
|
"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" : {
|
"about" : {
|
||||||
"rest_api" : {
|
"rest_api" : {
|
||||||
@@ -128,24 +128,24 @@
|
|||||||
"link_to_documentation" : "Dökümantasyon için tıklayın."
|
"link_to_documentation" : "Dökümantasyon için tıklayın."
|
||||||
},
|
},
|
||||||
"keyboard_shortcuts" : "Klavye kısayolları",
|
"keyboard_shortcuts" : "Klavye kısayolları",
|
||||||
"version" : "CommaFeed version ",
|
"version" : "CommaFeed versiyon ",
|
||||||
"line1_prefix" : "CommaFeed bir açık kaynak projedir. Kaynak dosyaları ",
|
"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_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.",
|
"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 ",
|
"line4" : "Bitcoin'i tercih edenler için adres ",
|
||||||
"goodies" : {
|
"goodies" : {
|
||||||
"value" : "Extralar",
|
"value" : "Ekstralar",
|
||||||
"android_app" : "Android app ",
|
"android_app" : "Android eklentisi",
|
||||||
"subscribe_url" : "Abonelik URL'si",
|
"subscribe_url" : "Abonelik URL'si",
|
||||||
"chrome_extension" : "Chrome eklentisi",
|
"chrome_extension" : "Chrome eklentisi",
|
||||||
"firefox_extension" : "Firefox eklentisi",
|
"firefox_extension" : "Firefox eklentisi",
|
||||||
"opera_extension" : "Opera eklentisi",
|
"opera_extension" : "Opera eklentisi",
|
||||||
"subscribe_bookmarklet" : "Bookmarklet'a abonelik ekle (tıklayın)",
|
"subscribe_bookmarklet" : "Yer imilerine abonelik ekle (tıklayın)",
|
||||||
"subscribe_bookmarklet_asc" : "Oldest first ",
|
"subscribe_bookmarklet_asc" : "Eskiler önce",
|
||||||
"subscribe_bookmarklet_desc" : "Newest first ",
|
"subscribe_bookmarklet_desc" : "Yeniler önce ",
|
||||||
"next_unread_bookmarklet" : "Bookmarklet'daki en son okunmamış ileti (Sık kullanılan çubuğuna sürükleyin)"
|
"next_unread_bookmarklet" : "Yer imilerindeki en son okunmamış ileti (Sık kullanılan çubuğuna sürükleyin)"
|
||||||
},
|
},
|
||||||
"translation" : {
|
"translation" : {
|
||||||
"value" : "Çeviri",
|
"value" : "Çeviri",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
"open_next_entry" : "sonraki öğeyi görüntüle",
|
"open_next_entry" : "sonraki öğeyi görüntüle",
|
||||||
"open_previous_entry" : "önceki öğeyi görüntüle",
|
"open_previous_entry" : "önceki öğeyi görüntüle",
|
||||||
"spacebar" : "space/shift+space ",
|
"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_next_entry" : "sonraki öğeyi görüntülemeden işaretle",
|
||||||
"focus_previous_entry" : "önceki öğ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",
|
"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_current_entry" : "görüntülenen öğeyi okundu/okunmadı işaretle",
|
||||||
"mark_all_as_read" : "tümünü okundu 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",
|
"open_in_new_tab_mark_as_read" : "öğeyi yeni bir sekmede aç ve okundu işaretle",
|
||||||
"fullscreen" : "toggle full screen mode ",
|
"fullscreen" : "tam ekran moduna geç ",
|
||||||
"font_size" : "increase/decrease font size of the current entry ",
|
"font_size" : "mevcut içerik için yazı boyunutunu arttır/azalt",
|
||||||
"go_to_all" : "go to the All view ",
|
"go_to_all" : "Tüm öğeleri görüntüle",
|
||||||
"go_to_starred" : "go to the Starred view ",
|
"go_to_starred" : "yıldızlı öğerleri görüntüle",
|
||||||
"feed_search" : "abonelik ismini yazarak aboneliğe git"
|
"feed_search" : "abonelik ismini yazarak aboneliğe git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
<title>CommaFeed</title>
|
<title>CommaFeed</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<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="shortcut icon" type="image/x-icon" href="favicon.ico" />
|
||||||
<link rel="apple-touch-icon" href="app-icon-57.png" />
|
<link rel="apple-touch-icon" href="app-icon-57.png" />
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="app-icon-72.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="32x32" href="app-icon-32.png" />
|
||||||
<link rel="icon" sizes="64x64" href="app-icon-64.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="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" />
|
<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="application-name" content="CommaFeed" />
|
||||||
<meta name="msapplication-navbutton-color" content="#F88A14" />
|
<meta name="msapplication-navbutton-color" content="#F88A14" />
|
||||||
<meta name="msapplication-starturl" content="/" />
|
<meta name="msapplication-starturl" content="/" />
|
||||||
@@ -39,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- build:js js/app.js -->
|
<!-- 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/dist/jquery.js"></script>
|
||||||
<script type="text/javascript" src="lib/jquery-ui/ui/jquery-ui.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>
|
<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/angular-ui-select2/src/select2.js"></script>
|
||||||
<script type="text/javascript" src="lib/select2/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/mousetrap/mousetrap.js"></script>
|
||||||
<script type="text/javascript" src="lib/momentjs/min/moment-with-langs.js"></script>
|
<script type="text/javascript" src="lib/momentjs/min/moment-with-locales.js"></script>
|
||||||
<script type="text/javascript" src="lib/device.js/lib/device.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/controllers.js"></script>
|
||||||
<script type="text/javascript" src="js/directives.js"></script>
|
<script type="text/javascript" src="js/directives.js"></script>
|
||||||
|
|||||||
@@ -322,17 +322,21 @@ module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedS
|
|||||||
|
|
||||||
$scope.save = function() {
|
$scope.save = function() {
|
||||||
var sub = $scope.sub;
|
var sub = $scope.sub;
|
||||||
|
$scope.error = null;
|
||||||
FeedService.modify({
|
FeedService.modify({
|
||||||
id : sub.id,
|
id : sub.id,
|
||||||
name : sub.name,
|
name : sub.name,
|
||||||
position : sub.position,
|
position : sub.position,
|
||||||
categoryId : sub.categoryId
|
categoryId : sub.categoryId,
|
||||||
|
filter : sub.filter
|
||||||
}, function() {
|
}, function() {
|
||||||
CategoryService.init();
|
CategoryService.init();
|
||||||
$state.transitionTo('feeds.view', {
|
$state.transitionTo('feeds.view', {
|
||||||
_id : 'all',
|
_id : 'all',
|
||||||
_type : 'category'
|
_type : 'category'
|
||||||
});
|
});
|
||||||
|
}, function(e) {
|
||||||
|
$scope.error = e.data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
@@ -489,6 +493,7 @@ module.controller('ToolbarCtrl', [
|
|||||||
type : $stateParams._type,
|
type : $stateParams._type,
|
||||||
id : $stateParams._id,
|
id : $stateParams._id,
|
||||||
olderThan : olderThan,
|
olderThan : olderThan,
|
||||||
|
keywords : $location.search().q,
|
||||||
read : true
|
read : true
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -881,6 +886,7 @@ module.controller('FeedListCtrl', [
|
|||||||
service.mark({
|
service.mark({
|
||||||
id : $scope.selectedId,
|
id : $scope.selectedId,
|
||||||
olderThan : olderThan || $scope.timestamp,
|
olderThan : olderThan || $scope.timestamp,
|
||||||
|
keywords : $location.search().q,
|
||||||
read : true
|
read : true
|
||||||
}, function() {
|
}, function() {
|
||||||
CategoryService.refresh(function() {
|
CategoryService.refresh(function() {
|
||||||
@@ -1365,7 +1371,7 @@ module.controller('SettingsCtrl', ['$scope', '$location', 'SettingsService', 'An
|
|||||||
|
|
||||||
$scope.langs = LangService.langs;
|
$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.settingsService = SettingsService;
|
||||||
$scope.$watch('settingsService.settings', function(value) {
|
$scope.$watch('settingsService.settings', function(value) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ module.directive('tags', function() {
|
|||||||
tags : []
|
tags : []
|
||||||
};
|
};
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
data.tags = newValue.split(',');
|
data.tags = newValue;
|
||||||
}
|
}
|
||||||
EntryService.tag(data);
|
EntryService.tag(data);
|
||||||
}
|
}
|
||||||
@@ -308,7 +308,8 @@ module.directive('droppable', ['CategoryService', 'FeedService', function(Catego
|
|||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
id : source.id,
|
id : source.id,
|
||||||
name : source.name
|
name : source.name,
|
||||||
|
filter : source.filter
|
||||||
};
|
};
|
||||||
|
|
||||||
if (source.children) {
|
if (source.children) {
|
||||||
|
|||||||
@@ -23,26 +23,23 @@ app.config([
|
|||||||
|
|
||||||
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/);
|
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/);
|
||||||
var interceptor = ['$rootScope', '$q', '$injector', function(scope, $q, $injector) {
|
var interceptor = ['$rootScope', '$q', '$injector', function(scope, $q, $injector) {
|
||||||
|
var f = {};
|
||||||
var success = function(response) {
|
|
||||||
|
f.response = function(response) {
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
var error = function(response) {
|
|
||||||
|
f.responseError = function(response) {
|
||||||
var status = response.status;
|
var status = response.status;
|
||||||
if (status == 401) {
|
if (status == 401) {
|
||||||
$injector.get('$state').transitionTo('welcome');
|
$injector.get('$state').transitionTo('welcome');
|
||||||
}
|
}
|
||||||
return $q.reject(response);
|
return $q.reject(response);
|
||||||
};
|
};
|
||||||
|
return f;
|
||||||
var promise = function(promise) {
|
|
||||||
return promise.then(success, error);
|
|
||||||
};
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
$httpProvider.responseInterceptors.push(interceptor);
|
$httpProvider.interceptors.push(interceptor);
|
||||||
|
|
||||||
$stateProvider.state('feeds', {
|
$stateProvider.state('feeds', {
|
||||||
'abstract' : true,
|
'abstract' : true,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ module.factory('SettingsService', ['$resource', '$translate', function($resource
|
|||||||
} else if (lang === 'ms') {
|
} else if (lang === 'ms') {
|
||||||
lang = 'ms-my';
|
lang = 'ms-my';
|
||||||
}
|
}
|
||||||
moment.lang(lang, {});
|
moment.locale(lang, {});
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(data);
|
callback(data);
|
||||||
}
|
}
|
||||||
@@ -298,6 +298,7 @@ module.factory('EntryService', ['$resource', '$http', function($resource, $http)
|
|||||||
$http.get('rest/entry/tags').success(function(data) {
|
$http.get('rest/entry/tags').success(function(data) {
|
||||||
res.tags = [];
|
res.tags = [];
|
||||||
res.tags.push.apply(res.tags, data);
|
res.tags.push.apply(res.tags, data);
|
||||||
|
res.tags.sort();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
var oldTag = res.tag;
|
var oldTag = res.tag;
|
||||||
|
|||||||
31
src/main/app/manifest.json
Normal file
31
src/main/app/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
@import "themes/bootstrap";
|
@import "themes/bootstrap";
|
||||||
@import "themes/ebraminio";
|
@import "themes/ebraminio";
|
||||||
@import "themes/MRACHINI";
|
@import "themes/MRACHINI";
|
||||||
|
@import "themes/nightsky";
|
||||||
@import "themes/svetla";
|
@import "themes/svetla";
|
||||||
@import "themes/dark";
|
@import "themes/dark";
|
||||||
@import "themes/third";
|
@import "themes/third";
|
||||||
|
|||||||
@@ -13,8 +13,4 @@
|
|||||||
content: "\e018";
|
content: "\e018";
|
||||||
font-family: "readabilicons";
|
font-family: "readabilicons";
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
font-size: 21px;
|
|
||||||
top: 5px;
|
|
||||||
position: relative;
|
|
||||||
line-height: 0px;
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
a:focus {
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.container-full {
|
.container-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -43,6 +48,10 @@ label {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pre-wrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.form-horizontal .control-group {
|
.form-horizontal .control-group {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@@ -116,19 +125,23 @@ blockquote p {
|
|||||||
font-size: 14px;
|
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-left-radius: 2px;
|
||||||
border-top-right-radius: 2px;
|
border-top-right-radius: 2px;
|
||||||
border-bottom-left-radius: 2px;
|
border-bottom-left-radius: 2px;
|
||||||
border-bottom-right-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-top-left-radius: 2px;
|
||||||
border-bottom-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-top-right-radius: 2px;
|
||||||
border-bottom-right-radius: 2px;
|
border-bottom-right-radius: 2px;
|
||||||
|
|||||||
126
src/main/app/sass/themes/_nightsky.scss
Normal file
126
src/main/app/sass/themes/_nightsky.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<button type="button" class="close" ng-click="close()">×</button>
|
<button type="button" class="close" ng-click="close()">×</button>
|
||||||
<h4>
|
<h4>
|
||||||
<input ng-model="filter" class="filter-input"
|
<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">
|
focus="feedSearchModal">
|
||||||
</h4>
|
</h4>
|
||||||
<small>{{ 'feedsearch.help' | translate }}</small>
|
<small>{{ 'feedsearch.help' | translate }}</small>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<span>
|
<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>
|
<i class="icon-tags"></i>
|
||||||
{{ 'global.tags' | translate }}
|
{{ 'global.tags' | translate }}
|
||||||
</a>
|
</span>
|
||||||
<span ng-if="!edit_mode">
|
<span ng-if="!edit_mode">
|
||||||
<span class="label label-info" ng-repeat="tag in entry.tags">{{tag}}</span>
|
<span class="label label-info" ng-repeat="tag in entry.tags">{{tag}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="edit_mode">
|
<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>
|
||||||
</span>
|
</span>
|
||||||
@@ -124,9 +124,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group donate">
|
<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>
|
<i class="icon-info-sign"></i>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label">{{ 'details.feed_url' | translate }}</label>
|
<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>
|
<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>
|
<span ng-show="!user.apiKey">{{ 'details.generate_api_key_first' | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,15 +3,16 @@
|
|||||||
<h3>{{ 'details.feed_details' | translate }}</h3>
|
<h3>{{ 'details.feed_details' | translate }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<form name="form" class="form-horizontal" ng-submit="save()">
|
<form name="form" class="form-horizontal" ng-submit="save()">
|
||||||
|
<div class="alert alert-danger" ng-if="error">{{ error }}</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label">{{ 'details.url' | translate }}</label>
|
<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>
|
<a href="{{sub.feedUrl}}" target="_blank">{{sub.feedUrl}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label">{{ 'details.website' | translate }}</label>
|
<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>
|
<a href="{{sub.feedLink}}" target="_blank">{{sub.feedLink}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,26 +50,34 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label">{{ 'details.next_refresh' | translate }}</label>
|
<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>
|
<span>{{sub.nextRefresh|entryDate:('details.queued_for_refresh' | translate) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label">{{ 'details.message' | translate }}</label>
|
<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>
|
<span>{{sub.message}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label">{{ 'details.feed_url' | translate }}</label>
|
<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>
|
<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>
|
<span ng-show="!user.apiKey">{{ 'details.generate_api_key_first' | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<button type="submit" class="btn btn-primary">{{ 'global.save' | translate }}</button>
|
<button type="submit" class="btn btn-primary">{{ 'global.save' | translate }}</button>
|
||||||
|
|||||||
@@ -51,6 +51,9 @@
|
|||||||
<span class="entry-author-prefix">{{ 'view.entry_author' | translate }}</span>
|
<span class="entry-author-prefix">{{ 'view.entry_author' | translate }}</span>
|
||||||
<span class="entry-author-name">{{entry.author}}</span>
|
<span class="entry-author-name">{{entry.author}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="entry-categories" ng-if="entry.categories">
|
||||||
|
<span class="entry-categories-name">({{entry.categories}})</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ package com.commafeed;
|
|||||||
import io.dropwizard.Application;
|
import io.dropwizard.Application;
|
||||||
import io.dropwizard.assets.AssetsBundle;
|
import io.dropwizard.assets.AssetsBundle;
|
||||||
import io.dropwizard.db.DataSourceFactory;
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
|
import io.dropwizard.forms.MultiPartBundle;
|
||||||
import io.dropwizard.hibernate.HibernateBundle;
|
import io.dropwizard.hibernate.HibernateBundle;
|
||||||
import io.dropwizard.migrations.MigrationsBundle;
|
import io.dropwizard.migrations.MigrationsBundle;
|
||||||
|
import io.dropwizard.server.DefaultServerFactory;
|
||||||
import io.dropwizard.servlets.CacheBustingFilter;
|
import io.dropwizard.servlets.CacheBustingFilter;
|
||||||
import io.dropwizard.setup.Bootstrap;
|
import io.dropwizard.setup.Bootstrap;
|
||||||
import io.dropwizard.setup.Environment;
|
import io.dropwizard.setup.Environment;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
|
||||||
import javax.servlet.DispatcherType;
|
import javax.servlet.DispatcherType;
|
||||||
@@ -21,7 +26,9 @@ import javax.servlet.ServletRequest;
|
|||||||
import javax.servlet.ServletResponse;
|
import javax.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.eclipse.jetty.server.session.SessionHandler;
|
import org.eclipse.jetty.server.session.SessionHandler;
|
||||||
|
import org.hibernate.cfg.AvailableSettings;
|
||||||
|
|
||||||
import com.commafeed.backend.feed.FeedRefreshTaskGiver;
|
import com.commafeed.backend.feed.FeedRefreshTaskGiver;
|
||||||
import com.commafeed.backend.feed.FeedRefreshUpdater;
|
import com.commafeed.backend.feed.FeedRefreshUpdater;
|
||||||
@@ -39,10 +46,8 @@ import com.commafeed.backend.model.UserRole;
|
|||||||
import com.commafeed.backend.model.UserSettings;
|
import com.commafeed.backend.model.UserSettings;
|
||||||
import com.commafeed.backend.service.StartupService;
|
import com.commafeed.backend.service.StartupService;
|
||||||
import com.commafeed.backend.service.UserService;
|
import com.commafeed.backend.service.UserService;
|
||||||
import com.commafeed.backend.task.OldStatusesCleanupTask;
|
import com.commafeed.backend.task.ScheduledTask;
|
||||||
import com.commafeed.backend.task.OrphansCleanupTask;
|
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
|
||||||
import com.commafeed.frontend.auth.SecurityCheckProvider;
|
|
||||||
import com.commafeed.frontend.auth.SecurityCheckProvider.SecurityCheckUserServiceProvider;
|
|
||||||
import com.commafeed.frontend.resource.AdminREST;
|
import com.commafeed.frontend.resource.AdminREST;
|
||||||
import com.commafeed.frontend.resource.CategoryREST;
|
import com.commafeed.frontend.resource.CategoryREST;
|
||||||
import com.commafeed.frontend.resource.EntryREST;
|
import com.commafeed.frontend.resource.EntryREST;
|
||||||
@@ -54,19 +59,14 @@ import com.commafeed.frontend.servlet.AnalyticsServlet;
|
|||||||
import com.commafeed.frontend.servlet.CustomCssServlet;
|
import com.commafeed.frontend.servlet.CustomCssServlet;
|
||||||
import com.commafeed.frontend.servlet.LogoutServlet;
|
import com.commafeed.frontend.servlet.LogoutServlet;
|
||||||
import com.commafeed.frontend.servlet.NextUnreadServlet;
|
import com.commafeed.frontend.servlet.NextUnreadServlet;
|
||||||
import com.commafeed.frontend.session.SessionHelperProvider;
|
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.google.inject.Guice;
|
import com.google.inject.Guice;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import com.sun.jersey.api.core.ResourceConfig;
|
import com.google.inject.Key;
|
||||||
import com.wordnik.swagger.config.ConfigFactory;
|
import com.google.inject.TypeLiteral;
|
||||||
import com.wordnik.swagger.config.ScannerFactory;
|
import com.wordnik.swagger.jaxrs.config.BeanConfig;
|
||||||
import com.wordnik.swagger.config.SwaggerConfig;
|
import com.wordnik.swagger.jaxrs.listing.ApiListingResource;
|
||||||
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;
|
|
||||||
|
|
||||||
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||||
|
|
||||||
@@ -89,36 +89,44 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
|||||||
FeedSubscription.class, User.class, UserRole.class, UserSettings.class) {
|
FeedSubscription.class, User.class, UserRole.class, UserSettings.class) {
|
||||||
@Override
|
@Override
|
||||||
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
|
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>() {
|
bootstrap.addBundle(new MigrationsBundle<CommaFeedConfiguration>() {
|
||||||
@Override
|
@Override
|
||||||
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
|
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
|
||||||
return configuration.getDatabase();
|
return configuration.getDataSourceFactory();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
|
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
|
||||||
|
bootstrap.addBundle(new MultiPartBundle());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
|
public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
|
||||||
// configure context path
|
|
||||||
environment.getApplicationContext().setContextPath(config.getApplicationSettings().getContextPath());
|
|
||||||
|
|
||||||
// guice init
|
// guice init
|
||||||
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
|
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.servlets().setSessionHandler(new SessionHandler(config.getSessionManagerFactory().build()));
|
||||||
environment.jersey().register(new SecurityCheckUserServiceProvider(injector.getInstance(UserService.class)));
|
|
||||||
environment.jersey().register(SecurityCheckProvider.class);
|
// support for "@SecurityCheck User user" injection
|
||||||
environment.jersey().register(SessionHelperProvider.class);
|
environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class)));
|
||||||
|
// support for "@Context SessionHelper sessionHelper" injection
|
||||||
|
environment.jersey().register(new SessionHelperFactoryProvider.Binder());
|
||||||
|
|
||||||
// REST resources
|
// REST resources
|
||||||
environment.jersey().setUrlPattern("/rest/*");
|
environment.jersey().setUrlPattern("/rest/*");
|
||||||
|
((DefaultServerFactory) config.getServerFactory()).setJerseyRootPath("/rest/*");
|
||||||
environment.jersey().register(injector.getInstance(AdminREST.class));
|
environment.jersey().register(injector.getInstance(AdminREST.class));
|
||||||
environment.jersey().register(injector.getInstance(CategoryREST.class));
|
environment.jersey().register(injector.getInstance(CategoryREST.class));
|
||||||
environment.jersey().register(injector.getInstance(EntryREST.class));
|
environment.jersey().register(injector.getInstance(EntryREST.class));
|
||||||
@@ -134,9 +142,13 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
|||||||
environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js");
|
environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js");
|
||||||
|
|
||||||
// Scheduled tasks
|
// Scheduled tasks
|
||||||
ScheduledExecutorService executor = environment.lifecycle().scheduledExecutorService("task-scheduler").build();
|
Set<ScheduledTask> tasks = injector.getInstance(Key.get(new TypeLiteral<Set<ScheduledTask>>() {
|
||||||
injector.getInstance(OldStatusesCleanupTask.class).register(executor);
|
}));
|
||||||
injector.getInstance(OrphansCleanupTask.class).register(executor);
|
ScheduledExecutorService executor = environment.lifecycle().scheduledExecutorService("task-scheduler", true).threads(tasks.size())
|
||||||
|
.build();
|
||||||
|
for (ScheduledTask task : tasks) {
|
||||||
|
task.register(executor);
|
||||||
|
}
|
||||||
|
|
||||||
// database init/changelogs
|
// database init/changelogs
|
||||||
environment.lifecycle().manage(injector.getInstance(StartupService.class));
|
environment.lifecycle().manage(injector.getInstance(StartupService.class));
|
||||||
@@ -147,14 +159,19 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
|||||||
environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class));
|
environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class));
|
||||||
|
|
||||||
// Swagger
|
// Swagger
|
||||||
environment.jersey().register(new ApiListingResourceJSON());
|
environment.jersey().register(new ApiListingResource());
|
||||||
environment.jersey().register(new ApiDeclarationProvider());
|
environment.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||||
environment.jersey().register(new ResourceListingProvider());
|
|
||||||
ScannerFactory.setScanner(new DefaultJaxrsScanner());
|
String modelsPackage = "com.commafeed.frontend.model";
|
||||||
ClassReaders.setReader(new DefaultJaxrsApiReader());
|
String requestsPackage = "com.commafeed.frontend.model.request";
|
||||||
SwaggerConfig swaggerConfig = ConfigFactory.config();
|
String endpointsPackage = "com.commafeed.frontend.resource";
|
||||||
swaggerConfig.setApiVersion("1");
|
List<String> packages = Arrays.asList(modelsPackage, requestsPackage, endpointsPackage);
|
||||||
|
BeanConfig swaggerConfig = new BeanConfig();
|
||||||
|
swaggerConfig.setTitle("CommaFeed");
|
||||||
|
swaggerConfig.setVersion("1");
|
||||||
swaggerConfig.setBasePath("/rest");
|
swaggerConfig.setBasePath("/rest");
|
||||||
|
swaggerConfig.setResourcePackage(StringUtils.join(packages, ","));
|
||||||
|
swaggerConfig.setScan(true);
|
||||||
|
|
||||||
// cache configuration
|
// cache configuration
|
||||||
// prevent caching on REST resources, except for favicons
|
// prevent caching on REST resources, except for favicons
|
||||||
@@ -170,8 +187,6 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
|||||||
}
|
}
|
||||||
}).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*");
|
}).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*");
|
||||||
|
|
||||||
// enable wadl
|
|
||||||
environment.jersey().disable(ResourceConfig.FEATURE_DISABLE_WADL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import javax.validation.constraints.NotNull;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
import org.apache.commons.lang.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
import org.hibernate.validator.constraints.NotBlank;
|
import org.hibernate.validator.constraints.NotBlank;
|
||||||
|
|
||||||
import com.commafeed.backend.cache.RedisPoolFactory;
|
import com.commafeed.backend.cache.RedisPoolFactory;
|
||||||
@@ -35,7 +35,7 @@ public class CommaFeedConfiguration extends Configuration {
|
|||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty("database")
|
@JsonProperty("database")
|
||||||
private DataSourceFactory database = new DataSourceFactory();
|
private DataSourceFactory dataSourceFactory = new DataSourceFactory();
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -64,60 +64,76 @@ public class CommaFeedConfiguration extends Configuration {
|
|||||||
public static class ApplicationSettings {
|
public static class ApplicationSettings {
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String contextPath;
|
@Valid
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@NotBlank
|
|
||||||
private String publicUrl;
|
private String publicUrl;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private boolean allowRegistrations;
|
@Valid
|
||||||
|
private Boolean allowRegistrations;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
private Boolean createDemoAccount;
|
||||||
|
|
||||||
private String googleAnalyticsTrackingCode;
|
private String googleAnalyticsTrackingCode;
|
||||||
|
|
||||||
@NotNull
|
private String googleAuthKey;
|
||||||
@Min(1)
|
|
||||||
private int backgroundThreads;
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Min(1)
|
@Min(1)
|
||||||
private int databaseUpdateThreads;
|
@Valid
|
||||||
|
private Integer backgroundThreads;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Min(1)
|
||||||
|
@Valid
|
||||||
|
private Integer databaseUpdateThreads;
|
||||||
|
|
||||||
private String smtpHost;
|
private String smtpHost;
|
||||||
|
|
||||||
private int smtpPort;
|
private int smtpPort;
|
||||||
|
|
||||||
private boolean smtpTls;
|
private boolean smtpTls;
|
||||||
|
|
||||||
private String smtpUserName;
|
private String smtpUserName;
|
||||||
|
|
||||||
private String smtpPassword;
|
private String smtpPassword;
|
||||||
|
private String smtpFromAddress;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private boolean heavyLoad;
|
@Valid
|
||||||
|
private Boolean heavyLoad;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private boolean pubsubhubbub;
|
@Valid
|
||||||
|
private Boolean pubsubhubbub;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private boolean imageProxyEnabled;
|
@Valid
|
||||||
|
private Boolean imageProxyEnabled;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Min(0)
|
@Min(0)
|
||||||
private int queryTimeout;
|
@Valid
|
||||||
|
private Integer queryTimeout;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Min(0)
|
@Min(0)
|
||||||
private int keepStatusDays;
|
@Valid
|
||||||
|
private Integer keepStatusDays;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Min(0)
|
@Min(0)
|
||||||
private int refreshIntervalMinutes;
|
@Valid
|
||||||
|
private Integer maxFeedCapacity;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@Min(0)
|
||||||
|
@Valid
|
||||||
|
private Integer refreshIntervalMinutes;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
private CacheType cache;
|
private CacheType cache;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@Valid
|
||||||
private String announcement;
|
private String announcement;
|
||||||
|
|
||||||
public Date getUnreadThreshold() {
|
public Date getUnreadThreshold() {
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ import com.commafeed.CommaFeedConfiguration.CacheType;
|
|||||||
import com.commafeed.backend.cache.CacheService;
|
import com.commafeed.backend.cache.CacheService;
|
||||||
import com.commafeed.backend.cache.NoopCacheService;
|
import com.commafeed.backend.cache.NoopCacheService;
|
||||||
import com.commafeed.backend.cache.RedisCacheService;
|
import com.commafeed.backend.cache.RedisCacheService;
|
||||||
import com.commafeed.backend.favicon.DefaultFaviconFetcher;
|
|
||||||
import com.commafeed.backend.favicon.AbstractFaviconFetcher;
|
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.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.AbstractModule;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.multibindings.Multibinder;
|
import com.google.inject.multibindings.Multibinder;
|
||||||
@@ -38,8 +44,15 @@ public class CommaFeedModule extends AbstractModule {
|
|||||||
log.info("using cache {}", cacheService.getClass());
|
log.info("using cache {}", cacheService.getClass());
|
||||||
bind(CacheService.class).toInstance(cacheService);
|
bind(CacheService.class).toInstance(cacheService);
|
||||||
|
|
||||||
Multibinder<AbstractFaviconFetcher> multibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
|
Multibinder<AbstractFaviconFetcher> faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
|
||||||
multibinder.addBinding().to(YoutubeFaviconFetcher.class);
|
faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class);
|
||||||
multibinder.addBinding().to(DefaultFaviconFetcher.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
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.Consts;
|
||||||
import org.apache.http.Header;
|
import org.apache.http.Header;
|
||||||
import org.apache.http.HttpEntity;
|
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.methods.HttpUriRequest;
|
||||||
import org.apache.http.client.protocol.HttpClientContext;
|
import org.apache.http.client.protocol.HttpClientContext;
|
||||||
import org.apache.http.config.ConnectionConfig;
|
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.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
import org.apache.http.impl.client.HttpClients;
|
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 ACCEPT_LANGUAGE = "en";
|
||||||
private static final String PRAGMA_NO_CACHE = "No-cache";
|
private static final String PRAGMA_NO_CACHE = "No-cache";
|
||||||
private static final String CACHE_CONTROL_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 final HttpResponseInterceptor REMOVE_INCORRECT_CONTENT_ENCODING = new ContentEncodingInterceptor();
|
||||||
|
|
||||||
private static SSLContext SSL_CONTEXT = null;
|
private static SSLContext SSL_CONTEXT = null;
|
||||||
@@ -181,8 +181,8 @@ public class HttpGetter {
|
|||||||
builder.addInterceptorFirst(REMOVE_INCORRECT_CONTENT_ENCODING);
|
builder.addInterceptorFirst(REMOVE_INCORRECT_CONTENT_ENCODING);
|
||||||
builder.disableAutomaticRetries();
|
builder.disableAutomaticRetries();
|
||||||
|
|
||||||
builder.setSslcontext(SSL_CONTEXT);
|
builder.setSSLContext(SSL_CONTEXT);
|
||||||
builder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
|
builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
|
||||||
|
|
||||||
RequestConfig.Builder configBuilder = RequestConfig.custom();
|
RequestConfig.Builder configBuilder = RequestConfig.custom();
|
||||||
configBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES);
|
configBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.cache;
|
package com.commafeed.backend.cache;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -18,7 +19,6 @@ import com.commafeed.frontend.model.Category;
|
|||||||
import com.commafeed.frontend.model.UnreadCount;
|
import com.commafeed.frontend.model.UnreadCount;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -30,7 +30,7 @@ public class RedisCacheService extends CacheService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getLastEntries(Feed feed) {
|
public List<String> getLastEntries(Feed feed) {
|
||||||
List<String> list = Lists.newArrayList();
|
List<String> list = new ArrayList<>();
|
||||||
try (Jedis jedis = pool.getResource()) {
|
try (Jedis jedis = pool.getResource()) {
|
||||||
String key = buildRedisEntryKey(feed);
|
String key = buildRedisEntryKey(feed);
|
||||||
Set<String> members = jedis.smembers(key);
|
Set<String> members = jedis.smembers(key);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.commafeed.backend.cache;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import redis.clients.jedis.JedisPool;
|
import redis.clients.jedis.JedisPool;
|
||||||
import redis.clients.jedis.JedisPoolConfig;
|
import redis.clients.jedis.JedisPoolConfig;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import org.apache.commons.lang.ObjectUtils;
|
|
||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
import com.commafeed.backend.model.FeedCategory;
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
import com.commafeed.backend.model.QFeedCategory;
|
import com.commafeed.backend.model.QFeedCategory;
|
||||||
import com.commafeed.backend.model.QUser;
|
import com.commafeed.backend.model.QUser;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.mysema.query.types.Predicate;
|
import com.mysema.query.types.Predicate;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -54,14 +54,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {
|
public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {
|
||||||
List<FeedCategory> list = Lists.newArrayList();
|
return findAll(user).stream().filter(c -> isChild(c, parent)).collect(Collectors.toList());
|
||||||
List<FeedCategory> all = findAll(user);
|
|
||||||
for (FeedCategory cat : all) {
|
|
||||||
if (isChild(cat, parent)) {
|
|
||||||
list.add(cat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isChild(FeedCategory child, FeedCategory parent) {
|
private boolean isChild(FeedCategory child, FeedCategory parent) {
|
||||||
@@ -70,7 +63,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
|
|||||||
}
|
}
|
||||||
boolean isChild = false;
|
boolean isChild = false;
|
||||||
while (child != null) {
|
while (child != null) {
|
||||||
if (ObjectUtils.equals(child.getId(), parent.getId())) {
|
if (Objects.equals(child.getId(), parent.getId())) {
|
||||||
isChild = true;
|
isChild = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import javax.inject.Inject;
|
|||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
@@ -17,6 +17,7 @@ import com.commafeed.backend.model.QUser;
|
|||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.mysema.query.BooleanBuilder;
|
import com.mysema.query.BooleanBuilder;
|
||||||
import com.mysema.query.jpa.hibernate.HibernateQuery;
|
import com.mysema.query.jpa.hibernate.HibernateQuery;
|
||||||
|
import com.mysema.query.jpa.hibernate.HibernateSubQuery;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedDAO extends GenericDAO<Feed> {
|
public class FeedDAO extends GenericDAO<Feed> {
|
||||||
@@ -33,12 +34,14 @@ public class FeedDAO extends GenericDAO<Feed> {
|
|||||||
disabledDatePredicate.or(feed.disabledUntil.isNull());
|
disabledDatePredicate.or(feed.disabledUntil.isNull());
|
||||||
disabledDatePredicate.or(feed.disabledUntil.lt(new Date()));
|
disabledDatePredicate.or(feed.disabledUntil.lt(new Date()));
|
||||||
|
|
||||||
HibernateQuery query = newQuery().from(feed);
|
HibernateQuery query = null;
|
||||||
if (lastLoginThreshold != null) {
|
if (lastLoginThreshold != null) {
|
||||||
QFeedSubscription subs = QFeedSubscription.feedSubscription;
|
QFeedSubscription subs = QFeedSubscription.feedSubscription;
|
||||||
QUser user = QUser.user;
|
QUser user = QUser.user;
|
||||||
query.join(feed.subscriptions, subs).join(subs.user, user).where(disabledDatePredicate, user.lastLogin.gt(lastLoginThreshold));
|
query = newQuery().from(subs);
|
||||||
|
query.join(subs.feed, feed).join(subs.user, user).where(disabledDatePredicate, user.lastLogin.gt(lastLoginThreshold));
|
||||||
} else {
|
} else {
|
||||||
|
query = newQuery().from(feed);
|
||||||
query.where(disabledDatePredicate);
|
query.where(disabledDatePredicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +63,7 @@ public class FeedDAO extends GenericDAO<Feed> {
|
|||||||
|
|
||||||
public List<Feed> findWithoutSubscriptions(int max) {
|
public List<Feed> findWithoutSubscriptions(int max) {
|
||||||
QFeedSubscription sub = QFeedSubscription.feedSubscription;
|
QFeedSubscription sub = QFeedSubscription.feedSubscription;
|
||||||
return newQuery().from(feed).leftJoin(feed.subscriptions, sub).where(sub.id.isNull()).limit(max).list(feed);
|
return newQuery().from(feed).where(new HibernateSubQuery().from(sub).where(sub.feed.eq(feed)).notExists()).limit(max).list(feed);
|
||||||
|
// return newQuery().from(feed).leftJoin(feed.subscriptions, sub).where(sub.id.isNull()).limit(max).list(feed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.commafeed.backend.model.FeedEntryContent;
|
|||||||
import com.commafeed.backend.model.QFeedEntry;
|
import com.commafeed.backend.model.QFeedEntry;
|
||||||
import com.commafeed.backend.model.QFeedEntryContent;
|
import com.commafeed.backend.model.QFeedEntryContent;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.mysema.query.jpa.hibernate.HibernateSubQuery;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
||||||
@@ -30,8 +31,10 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
|||||||
|
|
||||||
public int deleteWithoutEntries(int max) {
|
public int deleteWithoutEntries(int max) {
|
||||||
QFeedEntry entry = QFeedEntry.feedEntry;
|
QFeedEntry entry = QFeedEntry.feedEntry;
|
||||||
List<FeedEntryContent> list = newQuery().from(content).leftJoin(content.entries, entry).where(entry.id.isNull()).limit(max)
|
|
||||||
.list(content);
|
HibernateSubQuery subQuery = new HibernateSubQuery().from(entry).where(entry.content.id.eq(content.id));
|
||||||
|
List<FeedEntryContent> list = newQuery().from(content).where(subQuery.notExists()).limit(max).list(content);
|
||||||
|
|
||||||
int deleted = list.size();
|
int deleted = list.size();
|
||||||
delete(list);
|
delete(list);
|
||||||
return deleted;
|
return deleted;
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.QFeed;
|
|
||||||
import com.commafeed.backend.model.QFeedEntry;
|
import com.commafeed.backend.model.QFeedEntry;
|
||||||
import com.commafeed.backend.model.QFeedSubscription;
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.mysema.query.Tuple;
|
||||||
|
import com.mysema.query.types.expr.NumberExpression;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||||
@@ -32,17 +35,26 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
|||||||
return Iterables.getFirst(list, null);
|
return Iterables.getFirst(list, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedEntry> findWithoutSubscriptions(int max) {
|
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
|
||||||
QFeed feed = QFeed.feed;
|
NumberExpression<Long> count = entry.id.count();
|
||||||
QFeedSubscription sub = QFeedSubscription.feedSubscription;
|
List<Tuple> tuples = newQuery().from(entry).groupBy(entry.feed).having(count.gt(maxCapacity)).limit(max).list(entry.feed.id, count);
|
||||||
return newQuery().from(entry).join(entry.feed, feed).leftJoin(feed.subscriptions, sub).where(sub.id.isNull()).limit(max)
|
return tuples.stream().map(t -> new FeedCapacity(t.get(entry.feed.id), t.get(count))).collect(Collectors.toList());
|
||||||
.list(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int delete(Date olderThan, int max) {
|
public int delete(Long feedId, long max) {
|
||||||
List<FeedEntry> list = newQuery().from(entry).where(entry.inserted.lt(olderThan)).limit(max).list(entry);
|
List<FeedEntry> list = newQuery().from(entry).where(entry.feed.id.eq(feedId)).limit(max).list(entry);
|
||||||
int deleted = list.size();
|
return delete(list);
|
||||||
delete(list);
|
}
|
||||||
return deleted;
|
|
||||||
|
public int deleteOldEntries(Long feedId, long max) {
|
||||||
|
List<FeedEntry> list = newQuery().from(entry).where(entry.feed.id.eq(feedId)).orderBy(entry.updated.asc()).limit(max).list(entry);
|
||||||
|
return delete(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public static class FeedCapacity {
|
||||||
|
private Long id;
|
||||||
|
private Long capacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -7,12 +8,14 @@ import java.util.List;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.apache.commons.lang.builder.CompareToBuilder;
|
import org.apache.commons.lang3.builder.CompareToBuilder;
|
||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.FixedSizeSortedSet;
|
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.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryStatus;
|
import com.commafeed.backend.model.FeedEntryStatus;
|
||||||
import com.commafeed.backend.model.FeedEntryTag;
|
import com.commafeed.backend.model.FeedEntryTag;
|
||||||
@@ -26,7 +29,6 @@ import com.commafeed.backend.model.User;
|
|||||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||||
import com.commafeed.frontend.model.UnreadCount;
|
import com.commafeed.frontend.model.UnreadCount;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.common.collect.Ordering;
|
import com.google.common.collect.Ordering;
|
||||||
import com.mysema.query.BooleanBuilder;
|
import com.mysema.query.BooleanBuilder;
|
||||||
import com.mysema.query.Tuple;
|
import com.mysema.query.Tuple;
|
||||||
@@ -102,7 +104,11 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
query.orderBy(status.entryUpdated.desc(), status.id.desc());
|
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.list(status);
|
||||||
for (FeedEntryStatus status : statuses) {
|
for (FeedEntryStatus status : statuses) {
|
||||||
@@ -112,18 +118,21 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
return lazyLoadContent(includeContent, statuses);
|
return lazyLoadContent(includeContent, statuses);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HibernateQuery buildQuery(User user, FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset,
|
private HibernateQuery buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords, Date newerThan,
|
||||||
int limit, ReadingOrder order, Date last, String tag) {
|
int offset, int limit, ReadingOrder order, Date last, String tag) {
|
||||||
|
|
||||||
HibernateQuery query = newQuery().from(entry).where(entry.feed.eq(sub.getFeed()));
|
HibernateQuery query = newQuery().from(entry).where(entry.feed.eq(sub.getFeed()));
|
||||||
|
|
||||||
if (keywords != null) {
|
if (CollectionUtils.isNotEmpty(keywords)) {
|
||||||
query.join(entry.content, content);
|
query.join(entry.content, content);
|
||||||
|
|
||||||
for (String keyword : StringUtils.split(keywords)) {
|
for (FeedEntryKeyword keyword : keywords) {
|
||||||
BooleanBuilder or = new BooleanBuilder();
|
BooleanBuilder or = new BooleanBuilder();
|
||||||
or.or(content.content.containsIgnoreCase(keyword));
|
or.or(content.content.containsIgnoreCase(keyword.getKeyword()));
|
||||||
or.or(content.title.containsIgnoreCase(keyword));
|
or.or(content.title.containsIgnoreCase(keyword.getKeyword()));
|
||||||
|
if (keyword.getMode() == Mode.EXCLUDE) {
|
||||||
|
or.not();
|
||||||
|
}
|
||||||
query.where(or);
|
query.where(or);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,8 +189,9 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly, String keywords,
|
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
|
||||||
Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds, String tag) {
|
List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
|
||||||
|
boolean onlyIds, String tag) {
|
||||||
int capacity = offset + limit;
|
int capacity = offset + limit;
|
||||||
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
|
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
|
||||||
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator);
|
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator);
|
||||||
@@ -211,7 +221,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
List<FeedEntryStatus> placeholders = set.asList();
|
List<FeedEntryStatus> placeholders = set.asList();
|
||||||
int size = placeholders.size();
|
int size = placeholders.size();
|
||||||
if (size < offset) {
|
if (size < offset) {
|
||||||
return Lists.newArrayList();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
placeholders = placeholders.subList(Math.max(offset, 0), size);
|
placeholders = placeholders.subList(Math.max(offset, 0), size);
|
||||||
|
|
||||||
@@ -219,7 +229,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
if (onlyIds) {
|
if (onlyIds) {
|
||||||
statuses = placeholders;
|
statuses = placeholders;
|
||||||
} else {
|
} else {
|
||||||
statuses = Lists.newArrayList();
|
statuses = new ArrayList<>();
|
||||||
for (FeedEntryStatus placeholder : placeholders) {
|
for (FeedEntryStatus placeholder : placeholders) {
|
||||||
Long statusId = placeholder.getId();
|
Long statusId = placeholder.getId();
|
||||||
FeedEntry entry = feedEntryDAO.findById(placeholder.getEntry().getId());
|
FeedEntry entry = feedEntryDAO.findById(placeholder.getEntry().getId());
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -13,9 +15,7 @@ import com.commafeed.backend.model.FeedSubscription;
|
|||||||
import com.commafeed.backend.model.Models;
|
import com.commafeed.backend.model.Models;
|
||||||
import com.commafeed.backend.model.QFeedSubscription;
|
import com.commafeed.backend.model.QFeedSubscription;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.google.common.base.Function;
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.mysema.query.jpa.hibernate.HibernateQuery;
|
import com.mysema.query.jpa.hibernate.HibernateQuery;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -60,26 +60,13 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {
|
public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {
|
||||||
List<Long> categoryIds = Lists.transform(categories, new Function<FeedCategory, Long>() {
|
Set<Long> categoryIds = categories.stream().map(c -> c.getId()).collect(Collectors.toSet());
|
||||||
@Override
|
return findAll(user).stream().filter(s -> s.getCategory() != null && categoryIds.contains(s.getCategory().getId()))
|
||||||
public Long apply(FeedCategory input) {
|
.collect(Collectors.toList());
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<FeedSubscription> initRelations(List<FeedSubscription> list) {
|
private List<FeedSubscription> initRelations(List<FeedSubscription> list) {
|
||||||
for (FeedSubscription sub : list) {
|
list.forEach(s -> initRelations(s));
|
||||||
initRelations(sub);
|
|
||||||
}
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,19 +24,7 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void saveOrUpdate(Collection<T> models) {
|
public void saveOrUpdate(Collection<T> models) {
|
||||||
for (T model : models) {
|
models.forEach(m -> persist(m));
|
||||||
persist(model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void merge(T model) {
|
|
||||||
currentSession().merge(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void merge(Collection<T> models) {
|
|
||||||
for (T model : models) {
|
|
||||||
merge(model);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public T findById(Long id) {
|
public T findById(Long id) {
|
||||||
@@ -50,9 +38,7 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int delete(Collection<T> objects) {
|
public int delete(Collection<T> objects) {
|
||||||
for (T object : objects) {
|
objects.forEach(o -> delete(o));
|
||||||
delete(object);
|
|
||||||
}
|
|
||||||
return objects.size();
|
return objects.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,26 @@ import org.hibernate.SessionFactory;
|
|||||||
import org.hibernate.Transaction;
|
import org.hibernate.Transaction;
|
||||||
import org.hibernate.context.internal.ManagedSessionContext;
|
import org.hibernate.context.internal.ManagedSessionContext;
|
||||||
|
|
||||||
public abstract class UnitOfWork<T> {
|
public class UnitOfWork {
|
||||||
|
|
||||||
private SessionFactory sessionFactory;
|
@FunctionalInterface
|
||||||
|
public static interface SessionRunner {
|
||||||
public UnitOfWork(SessionFactory sessionFactory) {
|
public void runInSession();
|
||||||
this.sessionFactory = sessionFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
run(sessionFactory, () -> {
|
||||||
|
sessionRunner.runInSession();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T run(SessionFactory sessionFactory, SessionRunnerReturningValue<T> sessionRunner) {
|
||||||
final Session session = sessionFactory.openSession();
|
final Session session = sessionFactory.openSession();
|
||||||
if (ManagedSessionContext.hasBind(sessionFactory)) {
|
if (ManagedSessionContext.hasBind(sessionFactory)) {
|
||||||
throw new IllegalStateException("Already in a unit of work!");
|
throw new IllegalStateException("Already in a unit of work!");
|
||||||
@@ -25,11 +34,11 @@ public abstract class UnitOfWork<T> {
|
|||||||
ManagedSessionContext.bind(session);
|
ManagedSessionContext.bind(session);
|
||||||
session.beginTransaction();
|
session.beginTransaction();
|
||||||
try {
|
try {
|
||||||
t = runInSession();
|
t = sessionRunner.runInSession();
|
||||||
commitTransaction(session);
|
commitTransaction(session);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
rollbackTransaction(session);
|
rollbackTransaction(session);
|
||||||
this.<RuntimeException> rethrow(e);
|
UnitOfWork.<RuntimeException> rethrow(e);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
session.close();
|
session.close();
|
||||||
@@ -38,14 +47,14 @@ public abstract class UnitOfWork<T> {
|
|||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void rollbackTransaction(Session session) {
|
private static void rollbackTransaction(Session session) {
|
||||||
final Transaction txn = session.getTransaction();
|
final Transaction txn = session.getTransaction();
|
||||||
if (txn != null && txn.isActive()) {
|
if (txn != null && txn.isActive()) {
|
||||||
txn.rollback();
|
txn.rollback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void commitTransaction(Session session) {
|
private static void commitTransaction(Session session) {
|
||||||
final Transaction txn = session.getTransaction();
|
final Transaction txn = session.getTransaction();
|
||||||
if (txn != null && txn.isActive()) {
|
if (txn != null && txn.isActive()) {
|
||||||
txn.commit();
|
txn.commit();
|
||||||
@@ -53,7 +62,7 @@ public abstract class UnitOfWork<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@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;
|
throw (E) e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import javax.inject.Singleton;
|
|||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
import com.commafeed.backend.model.QUser;
|
import com.commafeed.backend.model.QUser;
|
||||||
import com.commafeed.backend.model.QUserRole;
|
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -20,18 +19,15 @@ public class UserDAO extends GenericDAO<User> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User findByName(String name) {
|
public User findByName(String name) {
|
||||||
return newQuery().from(user).where(user.name.equalsIgnoreCase(name)).leftJoin(user.roles, QUserRole.userRole).fetch()
|
return newQuery().from(user).where(user.name.equalsIgnoreCase(name)).uniqueResult(user);
|
||||||
.uniqueResult(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public User findByApiKey(String key) {
|
public User findByApiKey(String key) {
|
||||||
return newQuery().from(user).where(user.apiKey.equalsIgnoreCase(key)).leftJoin(user.roles, QUserRole.userRole).fetch()
|
return newQuery().from(user).where(user.apiKey.equalsIgnoreCase(key)).uniqueResult(user);
|
||||||
.uniqueResult(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public User findByEmail(String email) {
|
public User findByEmail(String email) {
|
||||||
return newQuery().from(user).where(user.email.equalsIgnoreCase(email)).leftJoin(user.roles, QUserRole.userRole).fetch()
|
return newQuery().from(user).where(user.email.equalsIgnoreCase(email)).uniqueResult(user);
|
||||||
.uniqueResult(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long count() {
|
public long count() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.commafeed.backend.dao;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
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.User;
|
||||||
import com.commafeed.backend.model.UserRole;
|
import com.commafeed.backend.model.UserRole;
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
import com.commafeed.backend.model.UserRole.Role;
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class UserRoleDAO extends GenericDAO<UserRole> {
|
public class UserRoleDAO extends GenericDAO<UserRole> {
|
||||||
@@ -33,10 +33,6 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Set<Role> findRoles(User user) {
|
public Set<Role> findRoles(User user) {
|
||||||
Set<Role> list = Sets.newHashSet();
|
return findAll(user).stream().map(r -> r.getRole()).collect(Collectors.toSet());
|
||||||
for (UserRole role : findAll(user)) {
|
|
||||||
list.add(role.getRole());
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package com.commafeed.backend.favicon;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ public abstract class AbstractFaviconFetcher {
|
|||||||
|
|
||||||
protected static int TIMEOUT = 4000;
|
protected static int TIMEOUT = 4000;
|
||||||
|
|
||||||
public abstract byte[] fetch(Feed feed);
|
public abstract Favicon fetch(Feed feed);
|
||||||
|
|
||||||
protected boolean isValidIconResponse(byte[] content, String contentType) {
|
protected boolean isValidIconResponse(byte[] content, String contentType) {
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
@@ -48,4 +50,11 @@ public abstract class AbstractFaviconFetcher {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public static class Favicon {
|
||||||
|
private final byte[] icon;
|
||||||
|
private final String mediaType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import javax.inject.Singleton;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
@@ -28,9 +28,15 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
private final HttpGetter getter;
|
private final HttpGetter getter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] fetch(Feed feed) {
|
public Favicon fetch(Feed feed) {
|
||||||
String url = feed.getLink() != null ? feed.getLink() : feed.getUrl();
|
Favicon icon = fetch(feed.getLink());
|
||||||
|
if (icon == null) {
|
||||||
|
icon = fetch(feed.getUrl());
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Favicon fetch(String url) {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
log.debug("url is null");
|
log.debug("url is null");
|
||||||
return null;
|
return null;
|
||||||
@@ -47,7 +53,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
url = url.substring(0, firstSlash);
|
url = url.substring(0, firstSlash);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] icon = getIconAtRoot(url);
|
Favicon icon = getIconAtRoot(url);
|
||||||
|
|
||||||
if (icon == null) {
|
if (icon == null) {
|
||||||
icon = getIconInPage(url);
|
icon = getIconInPage(url);
|
||||||
@@ -56,7 +62,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getIconAtRoot(String url) {
|
private Favicon getIconAtRoot(String url) {
|
||||||
byte[] bytes = null;
|
byte[] bytes = null;
|
||||||
String contentType = null;
|
String contentType = null;
|
||||||
|
|
||||||
@@ -67,23 +73,25 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
bytes = result.getContent();
|
bytes = result.getContent();
|
||||||
contentType = result.getContentType();
|
contentType = result.getContentType();
|
||||||
} catch (Exception e) {
|
} 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)) {
|
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;
|
Document doc = null;
|
||||||
try {
|
try {
|
||||||
HttpResult result = getter.getBinary(url, TIMEOUT);
|
HttpResult result = getter.getBinary(url, TIMEOUT);
|
||||||
doc = Jsoup.parse(new String(result.getContent()), url);
|
doc = Jsoup.parse(new String(result.getContent()), url);
|
||||||
} catch (Exception e) {
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +117,8 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
bytes = result.getContent();
|
bytes = result.getContent();
|
||||||
contentType = result.getContentType();
|
contentType = result.getContentType();
|
||||||
} catch (Exception e) {
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +127,6 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes;
|
return new Favicon(bytes, contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,18 +1,31 @@
|
|||||||
package com.commafeed.backend.favicon;
|
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.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
import org.apache.http.NameValuePair;
|
||||||
import org.jsoup.nodes.Document;
|
import org.apache.http.client.utils.URLEncodedUtils;
|
||||||
import org.jsoup.select.Elements;
|
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.HttpGetter;
|
import com.commafeed.backend.HttpGetter;
|
||||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||||
import com.commafeed.backend.model.Feed;
|
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;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@@ -20,47 +33,60 @@ import com.commafeed.backend.model.Feed;
|
|||||||
public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
||||||
|
|
||||||
private final HttpGetter getter;
|
private final HttpGetter getter;
|
||||||
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] fetch(Feed feed) {
|
public Favicon fetch(Feed feed) {
|
||||||
String url = feed.getUrl();
|
String url = feed.getUrl();
|
||||||
|
|
||||||
if (!url.toLowerCase().contains("://gdata.youtube.com/")) {
|
if (!url.toLowerCase().contains("youtube.com/feeds/videos.xml")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String userName = extractUserName(url);
|
String googleAuthKey = config.getApplicationSettings().getGoogleAuthKey();
|
||||||
if (userName == null) {
|
if (googleAuthKey == null) {
|
||||||
|
log.debug("no google auth key configured");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String profileUrl = "https://gdata.youtube.com/feeds/users/" + userName;
|
|
||||||
|
|
||||||
byte[] bytes = null;
|
byte[] bytes = null;
|
||||||
String contentType = null;
|
String contentType = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.debug("Getting YouTube user's icon, {}", url);
|
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();
|
||||||
// initial get to translate username to obscure user thumbnail URL
|
Optional<NameValuePair> channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel")).findFirst();
|
||||||
HttpResult profileResult = getter.getBinary(profileUrl, TIMEOUT);
|
System.out.println(userId.isPresent());
|
||||||
Document doc = Jsoup.parse(new String(profileResult.getContent()), profileUrl);
|
if (!userId.isPresent() && !channelId.isPresent()) {
|
||||||
|
|
||||||
Elements thumbnails = doc.select("media|thumbnail");
|
|
||||||
if (thumbnails.isEmpty()) {
|
|
||||||
return null;
|
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='");
|
YouTube.Channels.List list = youtube.channels().list("snippet");
|
||||||
int thumbnailEnd = thumbnailUrl.indexOf("'/>", thumbnailStart);
|
list.setKey(googleAuthKey);
|
||||||
if (thumbnailStart != -1) {
|
if (userId.isPresent()) {
|
||||||
thumbnailUrl = thumbnailUrl.substring(thumbnailStart + "<media:thumbnail url='".length(), thumbnailEnd);
|
list.setForUsername(userId.get().getValue());
|
||||||
|
} else {
|
||||||
|
list.setId(channelId.get().getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
// final get to actually retrieve the thumbnail
|
log.debug("contacting youtube api");
|
||||||
HttpResult iconResult = getter.getBinary(thumbnailUrl, TIMEOUT);
|
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();
|
bytes = iconResult.getContent();
|
||||||
contentType = iconResult.getContentType();
|
contentType = iconResult.getContentType();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -68,23 +94,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidIconResponse(bytes, contentType)) {
|
if (!isValidIconResponse(bytes, contentType)) {
|
||||||
bytes = null;
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractUserName(String url) {
|
|
||||||
int apiOrBase = url.indexOf("/users/");
|
|
||||||
if (apiOrBase == -1) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return new Favicon(bytes, contentType);
|
||||||
int userEndSlash = url.indexOf('/', apiOrBase + "/users/".length());
|
|
||||||
if (userEndSlash == -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.substring(apiOrBase + "/users/".length(), userEndSlash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,16 +41,16 @@ public class FeedFetcher {
|
|||||||
byte[] content = result.getContent();
|
byte[] content = result.getContent();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fetchedFeed = parser.parse(feedUrl, content);
|
fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
|
||||||
} catch (FeedException e) {
|
} catch (FeedException e) {
|
||||||
if (extractFeedUrlFromHtml) {
|
if (extractFeedUrlFromHtml) {
|
||||||
String extractedUrl = extractFeedUrl(StringUtils.newStringUtf8(result.getContent()), feedUrl);
|
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;
|
feedUrl = extractedUrl;
|
||||||
|
|
||||||
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
|
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
|
||||||
content = result.getContent();
|
content = result.getContent();
|
||||||
fetchedFeed = parser.parse(feedUrl, content);
|
fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.commafeed.backend.feed;
|
package com.commafeed.backend.feed;
|
||||||
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -11,8 +13,7 @@ import javax.inject.Singleton;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang.SystemUtils;
|
|
||||||
import org.jdom2.Element;
|
import org.jdom2.Element;
|
||||||
import org.jdom2.Namespace;
|
import org.jdom2.Namespace;
|
||||||
import org.xml.sax.InputSource;
|
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.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryContent;
|
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.google.common.collect.Iterables;
|
||||||
import com.rometools.rome.feed.synd.SyndContent;
|
|
||||||
import com.rometools.rome.feed.synd.SyndEnclosure;
|
import com.rometools.rome.feed.synd.SyndEnclosure;
|
||||||
import com.rometools.rome.feed.synd.SyndEntry;
|
import com.rometools.rome.feed.synd.SyndEntry;
|
||||||
import com.rometools.rome.feed.synd.SyndFeed;
|
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 START = new Date(86400000);
|
||||||
private static final Date END = new Date(1000l * Integer.MAX_VALUE - 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 {
|
public FetchedFeed parse(String feedUrl, byte[] xml) throws FeedException {
|
||||||
FetchedFeed fetchedFeed = new FetchedFeed();
|
FetchedFeed fetchedFeed = new FetchedFeed();
|
||||||
Feed feed = fetchedFeed.getFeed();
|
Feed feed = fetchedFeed.getFeed();
|
||||||
List<FeedEntry> entries = fetchedFeed.getEntries();
|
List<FeedEntry> entries = fetchedFeed.getEntries();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String encoding = FeedUtils.guessEncoding(xml);
|
Charset encoding = FeedUtils.guessEncoding(xml);
|
||||||
String xmlString = FeedUtils.trimInvalidXmlCharacters(new String(xml, encoding));
|
String xmlString = FeedUtils.trimInvalidXmlCharacters(new String(xml, encoding));
|
||||||
if (xmlString == null) {
|
if (xmlString == null) {
|
||||||
throw new FeedException("Input string is null for url " + feedUrl);
|
throw new FeedException("Input string is null for url " + feedUrl);
|
||||||
@@ -86,7 +77,7 @@ public class FeedParser {
|
|||||||
}
|
}
|
||||||
entry.setGuid(FeedUtils.truncate(guid, 2048));
|
entry.setGuid(FeedUtils.truncate(guid, 2048));
|
||||||
entry.setUpdated(validateDate(getEntryUpdateDate(item), true));
|
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 link is empty but guid is used as url
|
||||||
if (StringUtils.isBlank(entry.getUrl()) && StringUtils.startsWith(entry.getGuid(), "http")) {
|
if (StringUtils.isBlank(entry.getUrl()) && StringUtils.startsWith(entry.getGuid(), "http")) {
|
||||||
@@ -95,6 +86,8 @@ public class FeedParser {
|
|||||||
|
|
||||||
FeedEntryContent content = new FeedEntryContent();
|
FeedEntryContent content = new FeedEntryContent();
|
||||||
content.setContent(getContent(item));
|
content.setContent(getContent(item));
|
||||||
|
content.setCategories(FeedUtils.truncate(
|
||||||
|
item.getCategories().stream().map(c -> c.getName()).collect(Collectors.joining(", ")), 4096));
|
||||||
content.setTitle(getTitle(item));
|
content.setTitle(getTitle(item));
|
||||||
content.setAuthor(StringUtils.trimToNull(item.getAuthor()));
|
content.setAuthor(StringUtils.trimToNull(item.getAuthor()));
|
||||||
SyndEnclosure enclosure = Iterables.getFirst(item.getEnclosures(), null);
|
SyndEnclosure enclosure = Iterables.getFirst(item.getEnclosures(), null);
|
||||||
@@ -173,7 +166,7 @@ public class FeedParser {
|
|||||||
if (item.getContents().isEmpty()) {
|
if (item.getContents().isEmpty()) {
|
||||||
content = item.getDescription() == null ? null : item.getDescription().getValue();
|
content = item.getDescription() == null ? null : item.getDescription().getValue();
|
||||||
} else {
|
} 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);
|
return StringUtils.trimToNull(content);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
package com.commafeed.backend.feed;
|
package com.commafeed.backend.feed;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
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.Gauge;
|
||||||
import com.codahale.metrics.Meter;
|
import com.codahale.metrics.Meter;
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.dao.FeedDAO;
|
import com.commafeed.backend.dao.FeedDAO;
|
||||||
|
import com.commafeed.backend.dao.UnitOfWork;
|
||||||
import com.commafeed.backend.model.Feed;
|
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
|
@Singleton
|
||||||
public class FeedQueues {
|
public class FeedQueues {
|
||||||
|
|
||||||
|
private SessionFactory sessionFactory;
|
||||||
private final FeedDAO feedDAO;
|
private final FeedDAO feedDAO;
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
private Queue<FeedRefreshContext> addQueue = Queues.newConcurrentLinkedQueue();
|
private Queue<FeedRefreshContext> addQueue = new ConcurrentLinkedQueue<>();
|
||||||
private Queue<FeedRefreshContext> takeQueue = Queues.newConcurrentLinkedQueue();
|
private Queue<FeedRefreshContext> takeQueue = new ConcurrentLinkedQueue<>();
|
||||||
private Queue<Feed> giveBackQueue = Queues.newConcurrentLinkedQueue();
|
private Queue<Feed> giveBackQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
private Meter refill;
|
private Meter refill;
|
||||||
|
|
||||||
@Inject
|
@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.config = config;
|
||||||
this.feedDAO = feedDAO;
|
this.feedDAO = feedDAO;
|
||||||
|
|
||||||
@@ -78,13 +83,7 @@ public class FeedQueues {
|
|||||||
public void add(Feed feed, boolean urgent) {
|
public void add(Feed feed, boolean urgent) {
|
||||||
int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes();
|
int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes();
|
||||||
if (feed.getLastUpdated() == null || feed.getLastUpdated().before(DateUtils.addMinutes(new Date(), -1 * refreshInterval))) {
|
if (feed.getLastUpdated() == null || feed.getLastUpdated().before(DateUtils.addMinutes(new Date(), -1 * refreshInterval))) {
|
||||||
boolean alreadyQueued = false;
|
boolean alreadyQueued = addQueue.stream().anyMatch(c -> c.getFeed().getId().equals(feed.getId()));
|
||||||
for (FeedRefreshContext context : addQueue) {
|
|
||||||
if (context.getFeed().getId().equals(feed.getId())) {
|
|
||||||
alreadyQueued = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!alreadyQueued) {
|
if (!alreadyQueued) {
|
||||||
addQueue.add(new FeedRefreshContext(feed, urgent));
|
addQueue.add(new FeedRefreshContext(feed, urgent));
|
||||||
}
|
}
|
||||||
@@ -97,7 +96,7 @@ public class FeedQueues {
|
|||||||
private void refill() {
|
private void refill() {
|
||||||
refill.mark();
|
refill.mark();
|
||||||
|
|
||||||
List<FeedRefreshContext> contexts = Lists.newArrayList();
|
List<FeedRefreshContext> contexts = new ArrayList<>();
|
||||||
int batchSize = Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
|
int batchSize = Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
|
||||||
|
|
||||||
// add feeds we got from the add() method
|
// 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
|
// add feeds that are up to refresh from the database
|
||||||
int count = batchSize - contexts.size();
|
int count = batchSize - contexts.size();
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
List<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold());
|
List<Feed> feeds = UnitOfWork.run(sessionFactory, () -> feedDAO.findNextUpdatable(count, getLastLoginThreshold()));
|
||||||
for (Feed feed : feeds) {
|
for (Feed feed : feeds) {
|
||||||
contexts.add(new FeedRefreshContext(feed, false));
|
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
|
// set the disabledDate as we use it in feedDAO to decide what to refresh next. We also use a map to remove
|
||||||
// duplicates.
|
// duplicates.
|
||||||
Map<Long, FeedRefreshContext> map = Maps.newLinkedHashMap();
|
Map<Long, FeedRefreshContext> map = new LinkedHashMap<>();
|
||||||
for (FeedRefreshContext context : contexts) {
|
for (FeedRefreshContext context : contexts) {
|
||||||
Feed feed = context.getFeed();
|
Feed feed = context.getFeed();
|
||||||
feed.setDisabledUntil(DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes()));
|
feed.setDisabledUntil(DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes()));
|
||||||
@@ -135,11 +134,8 @@ public class FeedQueues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update all feeds in the database
|
// update all feeds in the database
|
||||||
List<Feed> feeds = Lists.newArrayList();
|
List<Feed> feeds = map.values().stream().map(c -> c.getFeed()).collect(Collectors.toList());
|
||||||
for (FeedRefreshContext context : map.values()) {
|
UnitOfWork.run(sessionFactory, () -> feedDAO.saveOrUpdate(feeds));
|
||||||
feeds.add(context.getFeed());
|
|
||||||
}
|
|
||||||
feedDAO.merge(feeds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,7 +150,7 @@ public class FeedQueues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Date getLastLoginThreshold() {
|
private Date getLastLoginThreshold() {
|
||||||
if (config.getApplicationSettings().isHeavyLoad()) {
|
if (config.getApplicationSettings().getHeavyLoad()) {
|
||||||
return DateUtils.addDays(new Date(), -30);
|
return DateUtils.addDays(new Date(), -30);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ package com.commafeed.backend.feed;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
public class FeedRefreshContext {
|
public class FeedRefreshContext {
|
||||||
private Feed feed;
|
private Feed feed;
|
||||||
private List<FeedEntry> entries;
|
private List<FeedEntry> entries;
|
||||||
@@ -14,29 +19,4 @@ public class FeedRefreshContext {
|
|||||||
this.feed = feed;
|
this.feed = feed;
|
||||||
this.urgent = isUrgent;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,14 @@ public class FeedRefreshExecutor {
|
|||||||
return offerLast(r);
|
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() {
|
pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
|
||||||
@Override
|
@Override
|
||||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
|
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
|
||||||
|
|||||||
@@ -10,13 +10,10 @@ import javax.inject.Singleton;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.hibernate.SessionFactory;
|
|
||||||
|
|
||||||
import com.codahale.metrics.Meter;
|
import com.codahale.metrics.Meter;
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.dao.FeedDAO;
|
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.
|
* 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
|
@Singleton
|
||||||
public class FeedRefreshTaskGiver implements Managed {
|
public class FeedRefreshTaskGiver implements Managed {
|
||||||
|
|
||||||
private final SessionFactory sessionFactory;
|
|
||||||
private final FeedQueues queues;
|
private final FeedQueues queues;
|
||||||
private final FeedRefreshWorker worker;
|
private final FeedRefreshWorker worker;
|
||||||
|
|
||||||
@@ -36,9 +32,8 @@ public class FeedRefreshTaskGiver implements Managed {
|
|||||||
private Meter threadWaited;
|
private Meter threadWaited;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedRefreshTaskGiver(SessionFactory sessionFactory, FeedQueues queues, FeedDAO feedDAO, FeedRefreshWorker worker,
|
public FeedRefreshTaskGiver(FeedQueues queues, FeedDAO feedDAO, FeedRefreshWorker worker, CommaFeedConfiguration config,
|
||||||
CommaFeedConfiguration config, MetricRegistry metrics) {
|
MetricRegistry metrics) {
|
||||||
this.sessionFactory = sessionFactory;
|
|
||||||
this.queues = queues;
|
this.queues = queues;
|
||||||
this.worker = worker;
|
this.worker = worker;
|
||||||
|
|
||||||
@@ -68,12 +63,7 @@ public class FeedRefreshTaskGiver implements Managed {
|
|||||||
public void run() {
|
public void run() {
|
||||||
while (!executor.isShutdown()) {
|
while (!executor.isShutdown()) {
|
||||||
try {
|
try {
|
||||||
FeedRefreshContext context = new UnitOfWork<FeedRefreshContext>(sessionFactory) {
|
FeedRefreshContext context = queues.take();
|
||||||
@Override
|
|
||||||
protected FeedRefreshContext runInSession() throws Exception {
|
|
||||||
return queues.take();
|
|
||||||
}
|
|
||||||
}.run();
|
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
feedRefreshed.mark();
|
feedRefreshed.mark();
|
||||||
worker.updateFeed(context);
|
worker.updateFeed(context);
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.commafeed.backend.feed;
|
|||||||
|
|
||||||
import io.dropwizard.lifecycle.Managed;
|
import io.dropwizard.lifecycle.Managed;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -16,8 +18,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
import com.codahale.metrics.Meter;
|
import com.codahale.metrics.Meter;
|
||||||
@@ -35,7 +37,6 @@ import com.commafeed.backend.model.FeedSubscription;
|
|||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.service.FeedUpdateService;
|
import com.commafeed.backend.service.FeedUpdateService;
|
||||||
import com.commafeed.backend.service.PubSubService;
|
import com.commafeed.backend.service.PubSubService;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.common.util.concurrent.Striped;
|
import com.google.common.util.concurrent.Striped;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -112,7 +113,7 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
feed.setMessage("Feed has no entries");
|
feed.setMessage("Feed has no entries");
|
||||||
} else {
|
} else {
|
||||||
List<String> lastEntries = cache.getLastEntries(feed);
|
List<String> lastEntries = cache.getLastEntries(feed);
|
||||||
List<String> currentEntries = Lists.newArrayList();
|
List<String> currentEntries = new ArrayList<>();
|
||||||
|
|
||||||
List<FeedSubscription> subscriptions = null;
|
List<FeedSubscription> subscriptions = null;
|
||||||
for (FeedEntry entry : entries) {
|
for (FeedEntry entry : entries) {
|
||||||
@@ -120,12 +121,7 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
if (!lastEntries.contains(cacheKey)) {
|
if (!lastEntries.contains(cacheKey)) {
|
||||||
log.debug("cache miss for {}", entry.getUrl());
|
log.debug("cache miss for {}", entry.getUrl());
|
||||||
if (subscriptions == null) {
|
if (subscriptions == null) {
|
||||||
subscriptions = new UnitOfWork<List<FeedSubscription>>(sessionFactory) {
|
subscriptions = UnitOfWork.run(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed));
|
||||||
@Override
|
|
||||||
protected List<FeedSubscription> runInSession() throws Exception {
|
|
||||||
return feedSubscriptionDAO.findByFeed(feed);
|
|
||||||
}
|
|
||||||
}.run();
|
|
||||||
}
|
}
|
||||||
ok &= addEntry(feed, entry, subscriptions);
|
ok &= addEntry(feed, entry, subscriptions);
|
||||||
entryCacheMiss.mark();
|
entryCacheMiss.mark();
|
||||||
@@ -143,16 +139,13 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (CollectionUtils.isNotEmpty(subscriptions)) {
|
if (CollectionUtils.isNotEmpty(subscriptions)) {
|
||||||
List<User> users = Lists.newArrayList();
|
List<User> users = subscriptions.stream().map(s -> s.getUser()).collect(Collectors.toList());
|
||||||
for (FeedSubscription sub : subscriptions) {
|
|
||||||
users.add(sub.getUser());
|
|
||||||
}
|
|
||||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||||
cache.invalidateUserRootCategory(users.toArray(new User[0]));
|
cache.invalidateUserRootCategory(users.toArray(new User[0]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.getApplicationSettings().isPubsubhubbub()) {
|
if (config.getApplicationSettings().getPubsubhubbub()) {
|
||||||
handlePubSub(feed);
|
handlePubSub(feed);
|
||||||
}
|
}
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -190,12 +183,7 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
|
locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
|
||||||
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
|
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
|
||||||
if (locked1 && locked2) {
|
if (locked1 && locked2) {
|
||||||
boolean inserted = new UnitOfWork<Boolean>(sessionFactory) {
|
boolean inserted = UnitOfWork.run(sessionFactory, () -> feedUpdateService.addEntry(feed, entry, subscriptions));
|
||||||
@Override
|
|
||||||
protected Boolean runInSession() throws Exception {
|
|
||||||
return feedUpdateService.addEntry(feed, entry);
|
|
||||||
}
|
|
||||||
}.run();
|
|
||||||
if (inserted) {
|
if (inserted) {
|
||||||
entryInserted.mark();
|
entryInserted.mark();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import io.dropwizard.lifecycle.Managed;
|
|||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -11,8 +13,8 @@ import javax.inject.Singleton;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
|
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
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.feed.FeedRefreshExecutor.Task;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.google.common.base.Optional;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls {@link FeedFetcher} and handles its outcome
|
* Calls {@link FeedFetcher} and handles its outcome
|
||||||
@@ -84,13 +85,18 @@ public class FeedRefreshWorker implements Managed {
|
|||||||
int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes();
|
int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes();
|
||||||
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
|
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
|
||||||
try {
|
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(),
|
FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
||||||
feed.getLastPublishedDate(), feed.getLastContentHash());
|
feed.getLastPublishedDate(), feed.getLastContentHash());
|
||||||
// stops here if NotModifiedException or any other exception is thrown
|
// stops here if NotModifiedException or any other exception is thrown
|
||||||
List<FeedEntry> entries = fetchedFeed.getEntries();
|
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()
|
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
|
||||||
.getAverageEntryInterval(), disabledUntil);
|
.getAverageEntryInterval(), disabledUntil);
|
||||||
}
|
}
|
||||||
@@ -118,7 +124,7 @@ public class FeedRefreshWorker implements Managed {
|
|||||||
} catch (NotModifiedException e) {
|
} catch (NotModifiedException e) {
|
||||||
log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
|
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);
|
disabledUntil = FeedUtils.buildDisabledUntil(feed.getLastEntryDate(), feed.getAverageEntryInterval(), disabledUntil);
|
||||||
}
|
}
|
||||||
feed.setErrorCount(0);
|
feed.setErrorCount(0);
|
||||||
|
|||||||
@@ -3,19 +3,22 @@ package com.commafeed.backend.feed;
|
|||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.apache.commons.lang.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
|
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
@@ -25,14 +28,15 @@ import org.jsoup.nodes.Entities.EscapeMode;
|
|||||||
import org.jsoup.safety.Cleaner;
|
import org.jsoup.safety.Cleaner;
|
||||||
import org.jsoup.safety.Whitelist;
|
import org.jsoup.safety.Whitelist;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
import org.mozilla.universalchardet.UniversalDetector;
|
|
||||||
import org.w3c.css.sac.InputSource;
|
import org.w3c.css.sac.InputSource;
|
||||||
import org.w3c.dom.css.CSSStyleDeclaration;
|
import org.w3c.dom.css.CSSStyleDeclaration;
|
||||||
|
|
||||||
|
import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.frontend.model.Entry;
|
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 com.steadystate.css.parser.CSSOMParser;
|
||||||
|
|
||||||
import edu.uci.ics.crawler4j.url.URLCanonicalizer;
|
import edu.uci.ics.crawler4j.url.URLCanonicalizer;
|
||||||
@@ -97,14 +101,14 @@ public class FeedUtils {
|
|||||||
* feed
|
* feed
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public static String guessEncoding(byte[] bytes) {
|
public static Charset guessEncoding(byte[] bytes) {
|
||||||
String extracted = extractDeclaredEncoding(bytes);
|
String extracted = extractDeclaredEncoding(bytes);
|
||||||
if (StringUtils.startsWithIgnoreCase(extracted, "iso-8859-")) {
|
if (StringUtils.startsWithIgnoreCase(extracted, "iso-8859-")) {
|
||||||
if (StringUtils.endsWith(extracted, "1") == false) {
|
if (StringUtils.endsWith(extracted, "1") == false) {
|
||||||
return extracted;
|
return Charset.forName(extracted);
|
||||||
}
|
}
|
||||||
} else if (StringUtils.startsWithIgnoreCase(extracted, "windows-")) {
|
} else if (StringUtils.startsWithIgnoreCase(extracted, "windows-")) {
|
||||||
return extracted;
|
return Charset.forName(extracted);
|
||||||
}
|
}
|
||||||
return detectEncoding(bytes);
|
return detectEncoding(bytes);
|
||||||
}
|
}
|
||||||
@@ -112,27 +116,23 @@ public class FeedUtils {
|
|||||||
/**
|
/**
|
||||||
* Detect encoding by analyzing characters in the array
|
* Detect encoding by analyzing characters in the array
|
||||||
*/
|
*/
|
||||||
public static String detectEncoding(byte[] bytes) {
|
public static Charset detectEncoding(byte[] bytes) {
|
||||||
String DEFAULT_ENCODING = "UTF-8";
|
String encoding = "UTF-8";
|
||||||
UniversalDetector detector = new UniversalDetector(null);
|
|
||||||
detector.handleData(bytes, 0, bytes.length);
|
CharsetDetector detector = new CharsetDetector();
|
||||||
detector.dataEnd();
|
detector.setText(bytes);
|
||||||
String encoding = detector.getDetectedCharset();
|
CharsetMatch match = detector.detect();
|
||||||
detector.reset();
|
if (match != null) {
|
||||||
if (encoding == null) {
|
encoding = match.getName();
|
||||||
encoding = DEFAULT_ENCODING;
|
}
|
||||||
} else if (encoding.equalsIgnoreCase("ISO-8859-1")) {
|
if (encoding.equalsIgnoreCase("ISO-8859-1")) {
|
||||||
encoding = "windows-1252";
|
encoding = "windows-1252";
|
||||||
}
|
}
|
||||||
return encoding;
|
return Charset.forName(encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String replaceHtmlEntitiesWithNumericEntities(String source) {
|
public static String replaceHtmlEntitiesWithNumericEntities(String source) {
|
||||||
String result = source;
|
return StringUtils.replaceEach(source, HtmlEntities.HTML_ENTITIES, HtmlEntities.NUMERIC_ENTITIES);
|
||||||
for (String entity : HtmlEntities.NUMERIC_MAPPING.keySet()) {
|
|
||||||
result = StringUtils.replace(result, entity, HtmlEntities.NUMERIC_MAPPING.get(entity));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,7 +182,7 @@ public class FeedUtils {
|
|||||||
return null;
|
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=\"");
|
index = StringUtils.indexOf(pi, "encoding=\"");
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
return null;
|
return null;
|
||||||
@@ -227,7 +227,7 @@ public class FeedUtils {
|
|||||||
String rule = "";
|
String rule = "";
|
||||||
CSSOMParser parser = new CSSOMParser();
|
CSSOMParser parser = new CSSOMParser();
|
||||||
try {
|
try {
|
||||||
List<String> rules = Lists.newArrayList();
|
List<String> rules = new ArrayList<>();
|
||||||
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||||
|
|
||||||
for (int i = 0; i < decl.getLength(); i++) {
|
for (int i = 0; i < decl.getLength(); i++) {
|
||||||
@@ -252,7 +252,7 @@ public class FeedUtils {
|
|||||||
String rule = "";
|
String rule = "";
|
||||||
CSSOMParser parser = new CSSOMParser();
|
CSSOMParser parser = new CSSOMParser();
|
||||||
try {
|
try {
|
||||||
List<String> rules = Lists.newArrayList();
|
List<String> rules = new ArrayList<>();
|
||||||
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||||
|
|
||||||
for (int i = 0; i < decl.getLength(); i++) {
|
for (int i = 0; i < decl.getLength(); i++) {
|
||||||
@@ -386,13 +386,7 @@ public class FeedUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<Long> getSortedTimestamps(List<FeedEntry> entries) {
|
public static List<Long> getSortedTimestamps(List<FeedEntry> entries) {
|
||||||
List<Long> timestamps = Lists.newArrayList();
|
return entries.stream().map(t -> t.getUpdated().getTime()).sorted(Collections.reverseOrder()).collect(Collectors.toList());
|
||||||
for (FeedEntry entry : entries) {
|
|
||||||
timestamps.add(entry.getUpdated().getTime());
|
|
||||||
}
|
|
||||||
Collections.sort(timestamps);
|
|
||||||
Collections.reverse(timestamps);
|
|
||||||
return timestamps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String removeTrailingSlash(String url) {
|
public static String removeTrailingSlash(String url) {
|
||||||
@@ -436,12 +430,8 @@ public class FeedUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isRelative(final String url) {
|
public static boolean isRelative(final String url) {
|
||||||
// the regex means "doesn't start with 'scheme://'"
|
// the regex means "start with 'scheme://'"
|
||||||
if ((url != null) && (url.startsWith("/") == false) && (!url.matches("^\\w+\\:\\/\\/.*")) && !(url.startsWith("#"))) {
|
return url.startsWith("/") || url.startsWith("#") || !url.matches("^\\w+\\:\\/\\/.*");
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) {
|
public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) {
|
||||||
@@ -495,19 +485,20 @@ public class FeedUtils {
|
|||||||
return rot13(new String(Base64.decodeBase64(code)));
|
return rot13(new String(Base64.decodeBase64(code)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void removeUnwantedFromSearch(List<Entry> entries, String keywords) {
|
public static void removeUnwantedFromSearch(List<Entry> entries, List<FeedEntryKeyword> keywords) {
|
||||||
if (StringUtils.isBlank(keywords)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterator<Entry> it = entries.iterator();
|
Iterator<Entry> it = entries.iterator();
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Entry entry = it.next();
|
Entry entry = it.next();
|
||||||
boolean keep = true;
|
boolean keep = true;
|
||||||
for (String keyword : keywords.split(" ")) {
|
for (FeedEntryKeyword keyword : keywords) {
|
||||||
String title = Jsoup.parse(entry.getTitle()).text();
|
String title = Jsoup.parse(entry.getTitle()).text();
|
||||||
String content = Jsoup.parse(entry.getContent()).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;
|
keep = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,23 @@
|
|||||||
package com.commafeed.backend.feed;
|
package com.commafeed.backend.feed;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
public class FetchedFeed {
|
public class FetchedFeed {
|
||||||
|
|
||||||
private Feed feed = new Feed();
|
private Feed feed = new Feed();
|
||||||
private List<FeedEntry> entries = Lists.newArrayList();
|
private List<FeedEntry> entries = new ArrayList<>();
|
||||||
|
|
||||||
private String title;
|
private String title;
|
||||||
private String urlAfterRedirect;
|
private String urlAfterRedirect;
|
||||||
private long fetchDuration;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package com.commafeed.backend.feed;
|
package com.commafeed.backend.feed;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
|
|
||||||
public class HtmlEntities {
|
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() {
|
static {
|
||||||
Map<String, String> map = Maps.newLinkedHashMap();
|
Map<String, String> map = new LinkedHashMap<>();
|
||||||
map.put("Á", "Á");
|
map.put("Á", "Á");
|
||||||
map.put("á", "á");
|
map.put("á", "á");
|
||||||
map.put("Â", "Â");
|
map.put("Â", "Â");
|
||||||
@@ -261,6 +260,7 @@ public class HtmlEntities {
|
|||||||
map.put("‍", "‍");
|
map.put("‍", "‍");
|
||||||
map.put("‌", "‌");
|
map.put("‌", "‌");
|
||||||
|
|
||||||
return map;
|
HTML_ENTITIES = map.keySet().toArray(new String[map.size()]);
|
||||||
|
NUMERIC_ENTITIES = map.values().toArray(new String[map.size()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package com.commafeed.backend.model;
|
package com.commafeed.backend.model;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.persistence.CascadeType;
|
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
import javax.persistence.OneToMany;
|
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
import javax.persistence.Temporal;
|
import javax.persistence.Temporal;
|
||||||
import javax.persistence.TemporalType;
|
import javax.persistence.TemporalType;
|
||||||
@@ -103,12 +100,6 @@ public class Feed extends AbstractModel {
|
|||||||
@Column(length = 40)
|
@Column(length = 40)
|
||||||
private String lastContentHash;
|
private String lastContentHash;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "feed", cascade = CascadeType.REMOVE)
|
|
||||||
private Set<FeedEntry> entries;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "feed")
|
|
||||||
private Set<FeedSubscription> subscriptions;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* detected hub for pubsubhubbub
|
* detected hub for pubsubhubbub
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import javax.persistence.Table;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "FEEDENTRYCONTENTS")
|
@Table(name = "FEEDENTRYCONTENTS")
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
@@ -26,6 +28,7 @@ public class FeedEntryContent extends AbstractModel {
|
|||||||
|
|
||||||
@Lob
|
@Lob
|
||||||
@Column(length = Integer.MAX_VALUE)
|
@Column(length = Integer.MAX_VALUE)
|
||||||
|
@Type(type = "org.hibernate.type.StringClobType")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@Column(length = 40)
|
@Column(length = 40)
|
||||||
@@ -40,6 +43,9 @@ public class FeedEntryContent extends AbstractModel {
|
|||||||
@Column(length = 255)
|
@Column(length = 255)
|
||||||
private String enclosureType;
|
private String enclosureType;
|
||||||
|
|
||||||
|
@Column(length = 4096)
|
||||||
|
private String categories;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "content")
|
@OneToMany(mappedBy = "content")
|
||||||
private Set<FeedEntry> entries;
|
private Set<FeedEntry> entries;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.model;
|
package com.commafeed.backend.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -16,8 +17,6 @@ import javax.persistence.Transient;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "FEEDENTRYSTATUSES")
|
@Table(name = "FEEDENTRYSTATUSES")
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
@@ -41,7 +40,7 @@ public class FeedEntryStatus extends AbstractModel {
|
|||||||
private boolean markable;
|
private boolean markable;
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private List<FeedEntryTag> tags = Lists.newArrayList();
|
private List<FeedEntryTag> tags = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Denormalization starts here
|
* Denormalization starts here
|
||||||
|
|||||||
@@ -40,4 +40,7 @@ public class FeedSubscription extends AbstractModel {
|
|||||||
|
|
||||||
private Integer position;
|
private Integer position;
|
||||||
|
|
||||||
|
@Column(length = 4096)
|
||||||
|
private String filter;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package com.commafeed.backend.model;
|
package com.commafeed.backend.model;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.persistence.CascadeType;
|
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
import javax.persistence.FetchType;
|
|
||||||
import javax.persistence.OneToMany;
|
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
import javax.persistence.Temporal;
|
import javax.persistence.Temporal;
|
||||||
import javax.persistence.TemporalType;
|
import javax.persistence.TemporalType;
|
||||||
@@ -15,11 +11,7 @@ import javax.persistence.TemporalType;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import org.apache.commons.lang.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
import org.hibernate.annotations.Cascade;
|
|
||||||
|
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "USERS")
|
@Table(name = "USERS")
|
||||||
@@ -58,27 +50,10 @@ public class User extends AbstractModel {
|
|||||||
@Temporal(TemporalType.TIMESTAMP)
|
@Temporal(TemporalType.TIMESTAMP)
|
||||||
private Date recoverPasswordTokenDate;
|
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")
|
@Column(name = "last_full_refresh")
|
||||||
@Temporal(TemporalType.TIMESTAMP)
|
@Temporal(TemporalType.TIMESTAMP)
|
||||||
private Date lastFullRefresh;
|
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) {
|
public boolean shouldRefreshFeedsAt(Date when) {
|
||||||
return (lastFullRefresh == null || lastFullRefreshMoreThan30MinutesBefore(when));
|
return (lastFullRefresh == null || lastFullRefreshMoreThan30MinutesBefore(when));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import javax.persistence.Table;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "USERSETTINGS")
|
@Table(name = "USERSETTINGS")
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
@@ -59,6 +61,7 @@ public class UserSettings extends AbstractModel {
|
|||||||
|
|
||||||
@Lob
|
@Lob
|
||||||
@Column(length = Integer.MAX_VALUE)
|
@Column(length = Integer.MAX_VALUE)
|
||||||
|
@Type(type = "org.hibernate.type.StringClobType")
|
||||||
private String customCss;
|
private String customCss;
|
||||||
|
|
||||||
@Column(name = "scroll_speed")
|
@Column(name = "scroll_speed")
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.commafeed.backend.opml;
|
package com.commafeed.backend.opml;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
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.FeedCategory;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.User;
|
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.Attribute;
|
||||||
import com.rometools.opml.feed.opml.Opml;
|
import com.rometools.opml.feed.opml.Opml;
|
||||||
import com.rometools.opml.feed.opml.Outline;
|
import com.rometools.opml.feed.opml.Outline;
|
||||||
@@ -31,39 +34,40 @@ public class OPMLExporter {
|
|||||||
opml.setCreated(new Date());
|
opml.setCreated(new Date());
|
||||||
|
|
||||||
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
|
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);
|
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
|
||||||
|
Collections.sort(subscriptions,
|
||||||
|
(e1, e2) -> MoreObjects.firstNonNull(e1.getPosition(), 0) - MoreObjects.firstNonNull(e2.getPosition(), 0));
|
||||||
|
|
||||||
// export root categories
|
// export root categories
|
||||||
for (FeedCategory cat : categories) {
|
for (FeedCategory cat : categories.stream().filter(c -> c.getParent() == null).collect(Collectors.toList())) {
|
||||||
if (cat.getParent() == null) {
|
opml.getOutlines().add(buildCategoryOutline(cat, categories, subscriptions));
|
||||||
opml.getOutlines().add(buildCategoryOutline(cat, subscriptions));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// export root subscriptions
|
// export root subscriptions
|
||||||
for (FeedSubscription sub : subscriptions) {
|
for (FeedSubscription sub : subscriptions.stream().filter(s -> s.getCategory() == null).collect(Collectors.toList())) {
|
||||||
if (sub.getCategory() == null) {
|
opml.getOutlines().add(buildSubscriptionOutline(sub));
|
||||||
opml.getOutlines().add(buildSubscriptionOutline(sub));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return opml;
|
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 outline = new Outline();
|
||||||
outline.setText(cat.getName());
|
outline.setText(cat.getName());
|
||||||
outline.setTitle(cat.getName());
|
outline.setTitle(cat.getName());
|
||||||
|
|
||||||
for (FeedCategory child : cat.getChildren()) {
|
for (FeedCategory child : categories.stream().filter(c -> c.getParent() != null && c.getParent().getId().equals(cat.getId()))
|
||||||
outline.getChildren().add(buildCategoryOutline(child, subscriptions));
|
.collect(Collectors.toList())) {
|
||||||
|
outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (FeedSubscription sub : subscriptions) {
|
for (FeedSubscription sub : subscriptions.stream()
|
||||||
if (sub.getCategory() != null && sub.getCategory().getId().equals(cat.getId())) {
|
.filter(s -> s.getCategory() != null && s.getCategory().getId().equals(cat.getId())).collect(Collectors.toList())) {
|
||||||
outline.getChildren().add(buildSubscriptionOutline(sub));
|
outline.getChildren().add(buildSubscriptionOutline(sub));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return outline;
|
return outline;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
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.cache.CacheService;
|
||||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||||
@@ -38,8 +38,8 @@ public class OPMLImporter {
|
|||||||
try {
|
try {
|
||||||
Opml feed = (Opml) input.build(new StringReader(xml));
|
Opml feed = (Opml) input.build(new StringReader(xml));
|
||||||
List<Outline> outlines = feed.getOutlines();
|
List<Outline> outlines = feed.getOutlines();
|
||||||
for (Outline outline : outlines) {
|
for (int i = 0; i < outlines.size(); i++) {
|
||||||
handleOutline(user, outline, null);
|
handleOutline(user, outlines.get(i), null, i);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), 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();
|
List<Outline> children = outline.getChildren();
|
||||||
if (CollectionUtils.isNotEmpty(children)) {
|
if (CollectionUtils.isNotEmpty(children)) {
|
||||||
String name = FeedUtils.truncate(outline.getText(), 128);
|
String name = FeedUtils.truncate(outline.getText(), 128);
|
||||||
@@ -64,11 +64,12 @@ public class OPMLImporter {
|
|||||||
category.setName(name);
|
category.setName(name);
|
||||||
category.setParent(parent);
|
category.setParent(parent);
|
||||||
category.setUser(user);
|
category.setUser(user);
|
||||||
|
category.setPosition(position);
|
||||||
feedCategoryDAO.saveOrUpdate(category);
|
feedCategoryDAO.saveOrUpdate(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Outline child : children) {
|
for (int i = 0; i < children.size(); i++) {
|
||||||
handleOutline(user, child, category);
|
handleOutline(user, children.get(i), category, i);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String name = FeedUtils.truncate(outline.getText(), 128);
|
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
|
// make sure we continue with the import process even if a feed failed
|
||||||
try {
|
try {
|
||||||
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent);
|
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position);
|
||||||
} catch (FeedSubscriptionException e) {
|
} catch (FeedSubscriptionException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package com.commafeed.backend.rome;
|
package com.commafeed.backend.rome;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.jdom2.Document;
|
import org.jdom2.Document;
|
||||||
import org.jdom2.Element;
|
import org.jdom2.Element;
|
||||||
|
|
||||||
import com.rometools.opml.io.impl.OPML10Parser;
|
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
|
* Support for OPML 1.1 parsing
|
||||||
@@ -19,12 +23,17 @@ public class OPML11Parser extends OPML10Parser {
|
|||||||
public boolean isMyType(Document document) {
|
public boolean isMyType(Document document) {
|
||||||
Element e = document.getRootElement();
|
Element e = document.getRootElement();
|
||||||
|
|
||||||
if (e.getName().equals("opml") && (e.getChild("head") == null || e.getChild("head").getChild("docs") == null)
|
if (e.getName().equals("opml")) {
|
||||||
&& (e.getAttributeValue("version") == null || e.getAttributeValue("version").equals("1.1"))) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -16,10 +14,10 @@ import org.hibernate.SessionFactory;
|
|||||||
import com.commafeed.backend.dao.FeedDAO;
|
import com.commafeed.backend.dao.FeedDAO;
|
||||||
import com.commafeed.backend.dao.FeedEntryContentDAO;
|
import com.commafeed.backend.dao.FeedEntryContentDAO;
|
||||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||||
|
import com.commafeed.backend.dao.FeedEntryDAO.FeedCapacity;
|
||||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||||
import com.commafeed.backend.dao.UnitOfWork;
|
import com.commafeed.backend.dao.UnitOfWork;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntryStatus;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains utility methods for cleaning the database
|
* Contains utility methods for cleaning the database
|
||||||
@@ -42,14 +40,18 @@ public class DatabaseCleaningService {
|
|||||||
log.info("cleaning feeds without subscriptions");
|
log.info("cleaning feeds without subscriptions");
|
||||||
long total = 0;
|
long total = 0;
|
||||||
int deleted = 0;
|
int deleted = 0;
|
||||||
|
long entriesTotal = 0;
|
||||||
do {
|
do {
|
||||||
deleted = new UnitOfWork<Integer>(sessionFactory) {
|
List<Feed> feeds = UnitOfWork.run(sessionFactory, () -> feedDAO.findWithoutSubscriptions(1));
|
||||||
@Override
|
for (Feed feed : feeds) {
|
||||||
protected Integer runInSession() throws Exception {
|
int entriesDeleted = 0;
|
||||||
List<Feed> feeds = feedDAO.findWithoutSubscriptions(1);
|
do {
|
||||||
return feedDAO.delete(feeds);
|
entriesDeleted = UnitOfWork.run(sessionFactory, () -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE));
|
||||||
};
|
entriesTotal += entriesDeleted;
|
||||||
}.run();
|
log.info("removed {} entries for feeds without subscriptions", entriesTotal);
|
||||||
|
} while (entriesDeleted > 0);
|
||||||
|
}
|
||||||
|
deleted = UnitOfWork.run(sessionFactory, () -> feedDAO.delete(feeds));
|
||||||
total += deleted;
|
total += deleted;
|
||||||
log.info("removed {} feeds without subscriptions", total);
|
log.info("removed {} feeds without subscriptions", total);
|
||||||
} while (deleted != 0);
|
} while (deleted != 0);
|
||||||
@@ -62,12 +64,7 @@ public class DatabaseCleaningService {
|
|||||||
long total = 0;
|
long total = 0;
|
||||||
int deleted = 0;
|
int deleted = 0;
|
||||||
do {
|
do {
|
||||||
deleted = new UnitOfWork<Integer>(sessionFactory) {
|
deleted = UnitOfWork.run(sessionFactory, () -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE));
|
||||||
@Override
|
|
||||||
protected Integer runInSession() throws Exception {
|
|
||||||
return feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE);
|
|
||||||
}
|
|
||||||
}.run();
|
|
||||||
total += deleted;
|
total += deleted;
|
||||||
log.info("removed {} contents without entries", total);
|
log.info("removed {} contents without entries", total);
|
||||||
} while (deleted != 0);
|
} while (deleted != 0);
|
||||||
@@ -75,23 +72,28 @@ public class DatabaseCleaningService {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long cleanEntriesOlderThan(long value, TimeUnit unit) {
|
public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) {
|
||||||
final Calendar cal = Calendar.getInstance();
|
|
||||||
cal.add(Calendar.MINUTE, -1 * (int) unit.toMinutes(value));
|
|
||||||
|
|
||||||
long total = 0;
|
long total = 0;
|
||||||
int deleted = 0;
|
while (true) {
|
||||||
do {
|
List<FeedCapacity> feeds = UnitOfWork.run(sessionFactory,
|
||||||
deleted = new UnitOfWork<Integer>(sessionFactory) {
|
() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE));
|
||||||
@Override
|
if (feeds.isEmpty()) {
|
||||||
protected Integer runInSession() throws Exception {
|
break;
|
||||||
return feedEntryDAO.delete(cal.getTime(), BATCH_SIZE);
|
}
|
||||||
}
|
|
||||||
}.run();
|
for (final FeedCapacity feed : feeds) {
|
||||||
total += deleted;
|
long remaining = feed.getCapacity() - maxFeedCapacity;
|
||||||
log.info("removed {} entries", total);
|
do {
|
||||||
} while (deleted != 0);
|
final long rem = remaining;
|
||||||
log.info("cleanup done: {} entries deleted", total);
|
int deleted = UnitOfWork.run(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;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,15 +102,10 @@ public class DatabaseCleaningService {
|
|||||||
long total = 0;
|
long total = 0;
|
||||||
int deleted = 0;
|
int deleted = 0;
|
||||||
do {
|
do {
|
||||||
deleted = new UnitOfWork<Integer>(sessionFactory) {
|
deleted = UnitOfWork.run(sessionFactory,
|
||||||
@Override
|
() -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE)));
|
||||||
protected Integer runInSession() throws Exception {
|
|
||||||
List<FeedEntryStatus> list = feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE);
|
|
||||||
return feedEntryStatusDAO.delete(list);
|
|
||||||
}
|
|
||||||
}.run();
|
|
||||||
total += deleted;
|
total += deleted;
|
||||||
log.info("cleaned {} old read statuses", total);
|
log.info("removed {} old read statuses", total);
|
||||||
} while (deleted != 0);
|
} while (deleted != 0);
|
||||||
log.info("cleanup done: {} old read statuses deleted", total);
|
log.info("cleanup done: {} old read statuses deleted", total);
|
||||||
return total;
|
return total;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import javax.inject.Singleton;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
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.dao.FeedEntryContentDAO;
|
||||||
import com.commafeed.backend.feed.FeedUtils;
|
import com.commafeed.backend.feed.FeedUtils;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
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.FeedEntryDAO;
|
||||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||||
|
import com.commafeed.backend.feed.FeedEntryKeyword;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryStatus;
|
import com.commafeed.backend.model.FeedEntryStatus;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -65,9 +66,9 @@ public class FeedEntryService {
|
|||||||
feedEntryStatusDAO.saveOrUpdate(status);
|
feedEntryStatusDAO.saveOrUpdate(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) {
|
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan, List<FeedEntryKeyword> keywords) {
|
||||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, -1, -1, null, false,
|
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null,
|
||||||
false, null);
|
false, false, null);
|
||||||
markList(statuses, olderThan);
|
markList(statuses, olderThan);
|
||||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||||
cache.invalidateUserRootCategory(user);
|
cache.invalidateUserRootCategory(user);
|
||||||
@@ -79,7 +80,7 @@ public class FeedEntryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void markList(List<FeedEntryStatus> statuses, Date olderThan) {
|
private void markList(List<FeedEntryStatus> statuses, Date olderThan) {
|
||||||
List<FeedEntryStatus> list = Lists.newArrayList();
|
List<FeedEntryStatus> list = new ArrayList<>();
|
||||||
for (FeedEntryStatus status : statuses) {
|
for (FeedEntryStatus status : statuses) {
|
||||||
if (!status.isRead()) {
|
if (!status.isRead()) {
|
||||||
Date inserted = status.getEntry().getInserted();
|
Date inserted = status.getEntry().getInserted();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
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.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryTag;
|
import com.commafeed.backend.model.FeedEntryTag;
|
||||||
import com.commafeed.backend.model.User;
|
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 }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -30,29 +28,12 @@ public class FeedEntryTagService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, entry);
|
List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
|
||||||
Map<String, FeedEntryTag> tagMap = Maps.uniqueIndex(tags, new Function<FeedEntryTag, String>() {
|
Set<String> existingTagNames = existingTags.stream().map(t -> t.getName()).collect(Collectors.toSet());
|
||||||
@Override
|
|
||||||
public String apply(FeedEntryTag input) {
|
|
||||||
return input.getName();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
List<FeedEntryTag> addList = Lists.newArrayList();
|
List<FeedEntryTag> addList = tagNames.stream().filter(name -> !existingTagNames.contains(name))
|
||||||
List<FeedEntryTag> removeList = Lists.newArrayList();
|
.map(name -> new FeedEntryTag(user, entry, name)).collect(Collectors.toList());
|
||||||
|
List<FeedEntryTag> removeList = existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).collect(Collectors.toList());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
feedEntryTagDAO.saveOrUpdate(addList);
|
feedEntryTagDAO.saveOrUpdate(addList);
|
||||||
feedEntryTagDAO.delete(removeList);
|
feedEntryTagDAO.delete(removeList);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.apache.commons.io.IOUtils;
|
|||||||
|
|
||||||
import com.commafeed.backend.dao.FeedDAO;
|
import com.commafeed.backend.dao.FeedDAO;
|
||||||
import com.commafeed.backend.favicon.AbstractFaviconFetcher;
|
import com.commafeed.backend.favicon.AbstractFaviconFetcher;
|
||||||
|
import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon;
|
||||||
import com.commafeed.backend.feed.FeedUtils;
|
import com.commafeed.backend.feed.FeedUtils;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ public class FeedService {
|
|||||||
private final FeedDAO feedDAO;
|
private final FeedDAO feedDAO;
|
||||||
private final Set<AbstractFaviconFetcher> faviconFetchers;
|
private final Set<AbstractFaviconFetcher> faviconFetchers;
|
||||||
|
|
||||||
private byte[] defaultFavicon;
|
private Favicon defaultFavicon;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedService(FeedDAO feedDAO, Set<AbstractFaviconFetcher> faviconFetchers) {
|
public FeedService(FeedDAO feedDAO, Set<AbstractFaviconFetcher> faviconFetchers) {
|
||||||
@@ -29,7 +30,7 @@ public class FeedService {
|
|||||||
this.faviconFetchers = faviconFetchers;
|
this.faviconFetchers = faviconFetchers;
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("could not load default favicon", e);
|
throw new RuntimeException("could not load default favicon", e);
|
||||||
}
|
}
|
||||||
@@ -49,9 +50,9 @@ public class FeedService {
|
|||||||
return feed;
|
return feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] fetchFavicon(Feed feed) {
|
public Favicon fetchFavicon(Feed feed) {
|
||||||
|
|
||||||
byte[] icon = null;
|
Favicon icon = null;
|
||||||
for (AbstractFaviconFetcher faviconFetcher : faviconFetchers) {
|
for (AbstractFaviconFetcher faviconFetcher : faviconFetchers) {
|
||||||
icon = faviconFetcher.fetch(feed);
|
icon = faviconFetcher.fetch(feed);
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.commafeed.backend.service;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -9,7 +10,7 @@ import javax.inject.Singleton;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.cache.CacheService;
|
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.Models;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.frontend.model.UnreadCount;
|
import com.commafeed.frontend.model.UnreadCount;
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@@ -44,7 +44,15 @@ public class FeedSubscriptionService {
|
|||||||
private final CacheService cache;
|
private final CacheService cache;
|
||||||
private final CommaFeedConfiguration config;
|
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();
|
final String pubUrl = config.getApplicationSettings().getPublicUrl();
|
||||||
if (StringUtils.isBlank(pubUrl)) {
|
if (StringUtils.isBlank(pubUrl)) {
|
||||||
@@ -63,7 +71,7 @@ public class FeedSubscriptionService {
|
|||||||
sub.setUser(user);
|
sub.setUser(user);
|
||||||
}
|
}
|
||||||
sub.setCategory(category);
|
sub.setCategory(category);
|
||||||
sub.setPosition(0);
|
sub.setPosition(position);
|
||||||
sub.setTitle(FeedUtils.truncate(title, 128));
|
sub.setTitle(FeedUtils.truncate(title, 128));
|
||||||
feedSubscriptionDAO.saveOrUpdate(sub);
|
feedSubscriptionDAO.saveOrUpdate(sub);
|
||||||
|
|
||||||
@@ -92,12 +100,7 @@ public class FeedSubscriptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Map<Long, UnreadCount> getUnreadCount(User user) {
|
public Map<Long, UnreadCount> getUnreadCount(User user) {
|
||||||
Map<Long, UnreadCount> map = Maps.newHashMap();
|
return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(s -> s.getId(), s -> getUnreadCount(user, s)));
|
||||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
|
||||||
for (FeedSubscription sub : subs) {
|
|
||||||
map.put(sub.getId(), getUnreadCount(user, sub));
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private UnreadCount getUnreadCount(User user, FeedSubscription sub) {
|
private UnreadCount getUnreadCount(User user, FeedSubscription sub) {
|
||||||
|
|||||||
@@ -1,30 +1,39 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
|
||||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||||
|
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryContent;
|
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 }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedUpdateService {
|
public class FeedUpdateService {
|
||||||
|
|
||||||
private final FeedEntryDAO feedEntryDAO;
|
private final FeedEntryDAO feedEntryDAO;
|
||||||
|
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||||
private final FeedEntryContentService feedEntryContentService;
|
private final FeedEntryContentService feedEntryContentService;
|
||||||
|
private final FeedEntryFilteringService feedEntryFilteringService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this is NOT thread-safe
|
* 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);
|
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
@@ -36,8 +45,23 @@ public class FeedUpdateService {
|
|||||||
entry.setContent(content);
|
entry.setContent(content);
|
||||||
entry.setInserted(new Date());
|
entry.setInserted(new Date());
|
||||||
entry.setFeed(feed);
|
entry.setFeed(feed);
|
||||||
|
|
||||||
feedEntryDAO.saveOrUpdate(entry);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -34,6 +35,7 @@ public class MailService {
|
|||||||
|
|
||||||
final String username = settings.getSmtpUserName();
|
final String username = settings.getSmtpUserName();
|
||||||
final String password = settings.getSmtpPassword();
|
final String password = settings.getSmtpPassword();
|
||||||
|
final String fromAddress = Optional.ofNullable(settings.getSmtpFromAddress()).orElse(settings.getSmtpUserName());
|
||||||
|
|
||||||
String dest = user.getEmail();
|
String dest = user.getEmail();
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ public class MailService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Message message = new MimeMessage(session);
|
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.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest));
|
||||||
message.setSubject("CommaFeed - " + subject);
|
message.setSubject("CommaFeed - " + subject);
|
||||||
message.setContent(content, "text/html; charset=utf-8");
|
message.setContent(content, "text/html; charset=utf-8");
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import javax.inject.Singleton;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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
|
// taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -10,7 +11,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
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.HttpHeaders;
|
||||||
import org.apache.http.NameValuePair;
|
import org.apache.http.NameValuePair;
|
||||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
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.feed.FeedUtils;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
|
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends push subscription requests. Callback is handled by {@link PubSubHubbubCallbackREST}
|
* 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);
|
log.debug("sending new pubsub subscription to {} for {}", hub, topic);
|
||||||
|
|
||||||
HttpPost post = new HttpPost(hub);
|
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.callback", publicUrl + "/rest/push/callback"));
|
||||||
nvp.add(new BasicNameValuePair("hub.topic", topic));
|
nvp.add(new BasicNameValuePair("hub.topic", topic));
|
||||||
nvp.add(new BasicNameValuePair("hub.mode", "subscribe"));
|
nvp.add(new BasicNameValuePair("hub.mode", "subscribe"));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
|
|||||||
import org.hibernate.internal.SessionFactoryImpl;
|
import org.hibernate.internal.SessionFactoryImpl;
|
||||||
|
|
||||||
import com.commafeed.CommaFeedApplication;
|
import com.commafeed.CommaFeedApplication;
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.dao.UnitOfWork;
|
import com.commafeed.backend.dao.UnitOfWork;
|
||||||
import com.commafeed.backend.dao.UserDAO;
|
import com.commafeed.backend.dao.UserDAO;
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
import com.commafeed.backend.model.UserRole.Role;
|
||||||
@@ -38,19 +39,15 @@ public class StartupService implements Managed {
|
|||||||
private final SessionFactory sessionFactory;
|
private final SessionFactory sessionFactory;
|
||||||
private final UserDAO userDAO;
|
private final UserDAO userDAO;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() throws Exception {
|
public void start() throws Exception {
|
||||||
updateSchema();
|
updateSchema();
|
||||||
new UnitOfWork<Void>(sessionFactory) {
|
long count = UnitOfWork.run(sessionFactory, () -> userDAO.count());
|
||||||
@Override
|
if (count == 0) {
|
||||||
protected Void runInSession() throws Exception {
|
UnitOfWork.run(sessionFactory, () -> initialData());
|
||||||
if (userDAO.count() == 0) {
|
}
|
||||||
initialData();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSchema() {
|
private void updateSchema() {
|
||||||
@@ -95,7 +92,9 @@ public class StartupService implements Managed {
|
|||||||
try {
|
try {
|
||||||
userService.register(CommaFeedApplication.USERNAME_ADMIN, "admin", "admin@commafeed.com", Arrays.asList(Role.ADMIN, Role.USER),
|
userService.register(CommaFeedApplication.USERNAME_ADMIN, "admin", "admin@commafeed.com", Arrays.asList(Role.ADMIN, Role.USER),
|
||||||
true);
|
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) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.commafeed.backend.service;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -10,17 +12,18 @@ import javax.inject.Singleton;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
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.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||||
|
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||||
import com.commafeed.backend.dao.UserDAO;
|
import com.commafeed.backend.dao.UserDAO;
|
||||||
|
import com.commafeed.backend.dao.UserRoleDAO;
|
||||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.model.UserRole;
|
import com.commafeed.backend.model.UserRole;
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
import com.commafeed.backend.model.UserRole.Role;
|
||||||
import com.commafeed.backend.service.internal.PostLoginActivities;
|
import com.commafeed.backend.service.internal.PostLoginActivities;
|
||||||
import com.google.common.base.Optional;
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@@ -28,12 +31,14 @@ import com.google.common.base.Preconditions;
|
|||||||
public class UserService {
|
public class UserService {
|
||||||
|
|
||||||
private final FeedCategoryDAO feedCategoryDAO;
|
private final FeedCategoryDAO feedCategoryDAO;
|
||||||
|
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||||
private final UserDAO userDAO;
|
private final UserDAO userDAO;
|
||||||
|
private final UserRoleDAO userRoleDAO;
|
||||||
private final UserSettingsDAO userSettingsDAO;
|
private final UserSettingsDAO userSettingsDAO;
|
||||||
|
|
||||||
private final PasswordEncryptionService encryptionService;
|
private final PasswordEncryptionService encryptionService;
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
private final PostLoginActivities postLoginActivities;
|
private final PostLoginActivities postLoginActivities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +46,7 @@ public class UserService {
|
|||||||
*/
|
*/
|
||||||
public Optional<User> login(String nameOrEmail, String password) {
|
public Optional<User> login(String nameOrEmail, String password) {
|
||||||
if (nameOrEmail == null || password == null) {
|
if (nameOrEmail == null || password == null) {
|
||||||
return Optional.absent();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userDAO.findByName(nameOrEmail);
|
User user = userDAO.findByName(nameOrEmail);
|
||||||
@@ -55,15 +60,15 @@ public class UserService {
|
|||||||
return Optional.of(user);
|
return Optional.of(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Optional.absent();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* try to log in with given api key
|
* try to log in with given api key
|
||||||
*/
|
*/
|
||||||
public Optional<User> login(String apiKey) {
|
public Optional<User> login(String apiKey) {
|
||||||
if (apiKey == null) {
|
if (apiKey == null) {
|
||||||
return Optional.absent();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userDAO.findByApiKey(apiKey);
|
User user = userDAO.findByApiKey(apiKey);
|
||||||
@@ -71,7 +76,7 @@ public class UserService {
|
|||||||
performPostLoginActivities(user);
|
performPostLoginActivities(user);
|
||||||
return Optional.of(user);
|
return Optional.of(user);
|
||||||
}
|
}
|
||||||
return Optional.absent();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +97,7 @@ public class UserService {
|
|||||||
Preconditions.checkNotNull(password);
|
Preconditions.checkNotNull(password);
|
||||||
|
|
||||||
if (!forceRegistration) {
|
if (!forceRegistration) {
|
||||||
Preconditions.checkState(config.getApplicationSettings().isAllowRegistrations(),
|
Preconditions.checkState(config.getApplicationSettings().getAllowRegistrations(),
|
||||||
"Registrations are closed on this CommaFeed instance");
|
"Registrations are closed on this CommaFeed instance");
|
||||||
|
|
||||||
Preconditions.checkNotNull(email);
|
Preconditions.checkNotNull(email);
|
||||||
@@ -114,16 +119,18 @@ public class UserService {
|
|||||||
user.setCreated(new Date());
|
user.setCreated(new Date());
|
||||||
user.setSalt(salt);
|
user.setSalt(salt);
|
||||||
user.setPassword(encryptionService.getEncryptedPassword(password, salt));
|
user.setPassword(encryptionService.getEncryptedPassword(password, salt));
|
||||||
for (Role role : roles) {
|
|
||||||
user.getRoles().add(new UserRole(user, role));
|
|
||||||
}
|
|
||||||
userDAO.saveOrUpdate(user);
|
userDAO.saveOrUpdate(user);
|
||||||
|
for (Role role : roles) {
|
||||||
|
userRoleDAO.saveOrUpdate(new UserRole(user, role));
|
||||||
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unregister(User user) {
|
public void unregister(User user) {
|
||||||
feedCategoryDAO.delete(feedCategoryDAO.findAll(user));
|
feedCategoryDAO.delete(feedCategoryDAO.findAll(user));
|
||||||
userSettingsDAO.delete(userSettingsDAO.findByUser(user));
|
userSettingsDAO.delete(userSettingsDAO.findByUser(user));
|
||||||
|
userRoleDAO.delete(userRoleDAO.findAll(user));
|
||||||
|
feedSubscriptionDAO.delete(feedSubscriptionDAO.findAll(user));
|
||||||
userDAO.delete(user);
|
userDAO.delete(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,4 +138,8 @@ public class UserService {
|
|||||||
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt());
|
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt());
|
||||||
return DigestUtils.sha1Hex(key);
|
return DigestUtils.sha1Hex(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<Role> getRoles(User user) {
|
||||||
|
return userRoleDAO.findRoles(user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import javax.inject.Singleton;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import org.apache.commons.lang.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.dao.UserDAO;
|
import com.commafeed.backend.dao.UserDAO;
|
||||||
@@ -17,11 +17,11 @@ import com.commafeed.backend.service.FeedSubscriptionService;
|
|||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
public class PostLoginActivities {
|
public class PostLoginActivities {
|
||||||
|
|
||||||
private final UserDAO userDAO;
|
private final UserDAO userDAO;
|
||||||
private final FeedSubscriptionService feedSubscriptionService;
|
private final FeedSubscriptionService feedSubscriptionService;
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
public void executeFor(User user) {
|
public void executeFor(User user) {
|
||||||
Date lastLogin = user.getLastLogin();
|
Date lastLogin = user.getLastLogin();
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
@@ -33,13 +33,13 @@ public class PostLoginActivities {
|
|||||||
user.setLastLogin(now);
|
user.setLastLogin(now);
|
||||||
saveUser = true;
|
saveUser = true;
|
||||||
}
|
}
|
||||||
if (config.getApplicationSettings().isHeavyLoad() && user.shouldRefreshFeedsAt(now)) {
|
if (config.getApplicationSettings().getHeavyLoad() && user.shouldRefreshFeedsAt(now)) {
|
||||||
feedSubscriptionService.refreshAll(user);
|
feedSubscriptionService.refreshAll(user);
|
||||||
user.setLastFullRefresh(now);
|
user.setLastFullRefresh(now);
|
||||||
saveUser = true;
|
saveUser = true;
|
||||||
}
|
}
|
||||||
if (saveUser) {
|
if (saveUser) {
|
||||||
userDAO.merge(user);
|
userDAO.saveOrUpdate(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.commafeed.backend.task;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
|
import com.commafeed.backend.service.DatabaseCleaningService;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
|
@Singleton
|
||||||
|
public class OldEntriesCleanupTask extends ScheduledTask {
|
||||||
|
|
||||||
|
private final CommaFeedConfiguration config;
|
||||||
|
private final DatabaseCleaningService cleaner;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
|
||||||
|
if (maxFeedCapacity > 0) {
|
||||||
|
cleaner.cleanEntriesForFeedsExceedingCapacity(maxFeedCapacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getInitialDelay() {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPeriod() {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimeUnit getTimeUnit() {
|
||||||
|
return TimeUnit.MINUTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.commafeed.backend.task;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import com.commafeed.backend.service.DatabaseCleaningService;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
|
@Singleton
|
||||||
|
public class OrphanedContentsCleanupTask extends ScheduledTask {
|
||||||
|
|
||||||
|
private final DatabaseCleaningService cleaner;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
cleaner.cleanContentsWithoutEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getInitialDelay() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPeriod() {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimeUnit getTimeUnit() {
|
||||||
|
return TimeUnit.MINUTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,19 +11,18 @@ import com.commafeed.backend.service.DatabaseCleaningService;
|
|||||||
|
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
public class OrphansCleanupTask extends ScheduledTask {
|
public class OrphanedFeedsCleanupTask extends ScheduledTask {
|
||||||
|
|
||||||
private final DatabaseCleaningService cleaner;
|
private final DatabaseCleaningService cleaner;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
cleaner.cleanFeedsWithoutSubscriptions();
|
cleaner.cleanFeedsWithoutSubscriptions();
|
||||||
cleaner.cleanContentsWithoutEntries();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getInitialDelay() {
|
public long getInitialDelay() {
|
||||||
return 5;
|
return 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -26,7 +26,8 @@ public abstract class ScheduledTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
log.info("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(),
|
||||||
|
getTimeUnit(), getInitialDelay(), getTimeUnit());
|
||||||
executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit());
|
executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.commafeed.frontend.auth;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.util.B64Code;
|
||||||
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
|
import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory;
|
||||||
|
|
||||||
|
import com.commafeed.backend.model.User;
|
||||||
|
import com.commafeed.backend.model.UserRole.Role;
|
||||||
|
import com.commafeed.backend.service.UserService;
|
||||||
|
import com.commafeed.frontend.session.SessionHelper;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityCheckFactory extends AbstractContainerRequestValueFactory<User> {
|
||||||
|
|
||||||
|
private static final String PREFIX = "Basic";
|
||||||
|
|
||||||
|
@Context
|
||||||
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UserService userService;
|
||||||
|
|
||||||
|
private final Role role;
|
||||||
|
private final boolean apiKeyAllowed;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User provide() {
|
||||||
|
Optional<User> user = apiKeyLogin();
|
||||||
|
if (!user.isPresent()) {
|
||||||
|
user = basicAuthenticationLogin();
|
||||||
|
}
|
||||||
|
if (!user.isPresent()) {
|
||||||
|
user = cookieSessionLogin(new SessionHelper(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isPresent()) {
|
||||||
|
Set<Role> roles = userService.getRoles(user.get());
|
||||||
|
if (roles.contains(role)) {
|
||||||
|
return user.get();
|
||||||
|
} else {
|
||||||
|
throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("You don't have the required role to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
|
||||||
|
.entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> cookieSessionLogin(SessionHelper sessionHelper) {
|
||||||
|
Optional<User> loggedInUser = sessionHelper.getLoggedInUser();
|
||||||
|
if (loggedInUser.isPresent()) {
|
||||||
|
userService.performPostLoginActivities(loggedInUser.get());
|
||||||
|
}
|
||||||
|
return loggedInUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<User> basicAuthenticationLogin() {
|
||||||
|
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||||
|
if (header != null) {
|
||||||
|
int space = header.indexOf(' ');
|
||||||
|
if (space > 0) {
|
||||||
|
String method = header.substring(0, space);
|
||||||
|
if (PREFIX.equalsIgnoreCase(method)) {
|
||||||
|
String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1);
|
||||||
|
int i = decoded.indexOf(':');
|
||||||
|
if (i > 0) {
|
||||||
|
String username = decoded.substring(0, i);
|
||||||
|
String password = decoded.substring(i + 1);
|
||||||
|
return userService.login(username, password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<User> apiKeyLogin() {
|
||||||
|
String apiKey = request.getParameter("apiKey");
|
||||||
|
if (apiKey != null && apiKeyAllowed) {
|
||||||
|
return userService.login(apiKey);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.commafeed.frontend.auth;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import org.glassfish.hk2.api.Factory;
|
||||||
|
import org.glassfish.hk2.api.InjectionResolver;
|
||||||
|
import org.glassfish.hk2.api.ServiceLocator;
|
||||||
|
import org.glassfish.hk2.api.TypeLiteral;
|
||||||
|
import org.glassfish.hk2.utilities.binding.AbstractBinder;
|
||||||
|
import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider;
|
||||||
|
import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
|
||||||
|
import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver;
|
||||||
|
import org.glassfish.jersey.server.model.Parameter;
|
||||||
|
import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider;
|
||||||
|
|
||||||
|
import com.commafeed.backend.model.User;
|
||||||
|
import com.commafeed.backend.service.UserService;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class SecurityCheckFactoryProvider extends AbstractValueFactoryProvider {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, final ServiceLocator injector) {
|
||||||
|
super(extractorProvider, injector, Parameter.Source.UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Factory<?> createValueFactory(final Parameter parameter) {
|
||||||
|
final Class<?> classType = parameter.getRawType();
|
||||||
|
|
||||||
|
SecurityCheck securityCheck = parameter.getAnnotation(SecurityCheck.class);
|
||||||
|
if (securityCheck == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (classType.isAssignableFrom(User.class)) {
|
||||||
|
return new SecurityCheckFactory(securityCheck.value(), securityCheck.apiKeyAllowed());
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SecurityCheckInjectionResolver extends ParamInjectionResolver<SecurityCheck> {
|
||||||
|
public SecurityCheckInjectionResolver() {
|
||||||
|
super(SecurityCheckFactoryProvider.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class Binder extends AbstractBinder {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(SecurityCheckFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class);
|
||||||
|
bind(SecurityCheckInjectionResolver.class).to(new TypeLiteral<InjectionResolver<SecurityCheck>>() {
|
||||||
|
}).in(Singleton.class);
|
||||||
|
bind(userService).to(UserService.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package com.commafeed.frontend.auth;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.ws.rs.WebApplicationException;
|
|
||||||
import javax.ws.rs.core.Context;
|
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
|
||||||
import javax.ws.rs.core.MediaType;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.util.B64Code;
|
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
|
||||||
|
|
||||||
import com.commafeed.backend.model.User;
|
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
|
||||||
import com.commafeed.backend.service.UserService;
|
|
||||||
import com.commafeed.frontend.session.SessionHelper;
|
|
||||||
import com.google.common.base.Optional;
|
|
||||||
import com.sun.jersey.api.core.HttpContext;
|
|
||||||
import com.sun.jersey.api.model.Parameter;
|
|
||||||
import com.sun.jersey.core.spi.component.ComponentContext;
|
|
||||||
import com.sun.jersey.core.spi.component.ComponentScope;
|
|
||||||
import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
|
|
||||||
import com.sun.jersey.spi.inject.Injectable;
|
|
||||||
import com.sun.jersey.spi.inject.InjectableProvider;
|
|
||||||
import com.sun.jersey.spi.inject.SingletonTypeInjectableProvider;
|
|
||||||
|
|
||||||
public class SecurityCheckProvider implements InjectableProvider<SecurityCheck, Parameter> {
|
|
||||||
|
|
||||||
public static class SecurityCheckUserServiceProvider extends SingletonTypeInjectableProvider<Context, UserService> {
|
|
||||||
|
|
||||||
public SecurityCheckUserServiceProvider(UserService userService) {
|
|
||||||
super(UserService.class, userService);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
static class SecurityCheckInjectable extends AbstractHttpContextInjectable<User> {
|
|
||||||
private static final String PREFIX = "Basic";
|
|
||||||
|
|
||||||
private final SessionHelper sessionHelper;
|
|
||||||
private final UserService userService;
|
|
||||||
private final Role role;
|
|
||||||
private final boolean apiKeyAllowed;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public User getValue(HttpContext c) {
|
|
||||||
Optional<User> user = apiKeyLogin(c);
|
|
||||||
if (!user.isPresent()) {
|
|
||||||
user = basicAuthenticationLogin(c);
|
|
||||||
}
|
|
||||||
if (!user.isPresent()) {
|
|
||||||
user = cookieSessionLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isPresent()) {
|
|
||||||
if (user.get().hasRole(role)) {
|
|
||||||
return user.get();
|
|
||||||
} else {
|
|
||||||
throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN)
|
|
||||||
.entity("You don't have the required role to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
|
|
||||||
.entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<User> cookieSessionLogin() {
|
|
||||||
Optional<User> loggedInUser = sessionHelper.getLoggedInUser();
|
|
||||||
if (loggedInUser.isPresent()) {
|
|
||||||
userService.performPostLoginActivities(loggedInUser.get());
|
|
||||||
}
|
|
||||||
return loggedInUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<User> basicAuthenticationLogin(HttpContext c) {
|
|
||||||
String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION);
|
|
||||||
if (header != null) {
|
|
||||||
int space = header.indexOf(' ');
|
|
||||||
if (space > 0) {
|
|
||||||
String method = header.substring(0, space);
|
|
||||||
if (PREFIX.equalsIgnoreCase(method)) {
|
|
||||||
String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1);
|
|
||||||
int i = decoded.indexOf(':');
|
|
||||||
if (i > 0) {
|
|
||||||
String username = decoded.substring(0, i);
|
|
||||||
String password = decoded.substring(i + 1);
|
|
||||||
return userService.login(username, password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<User> apiKeyLogin(HttpContext c) {
|
|
||||||
String apiKey = c.getUriInfo().getQueryParameters().getFirst("apiKey");
|
|
||||||
if (apiKey != null && apiKeyAllowed) {
|
|
||||||
return userService.login(apiKey);
|
|
||||||
}
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SessionHelper sessionHelper;
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
public SecurityCheckProvider(@Context HttpServletRequest req, @Context UserService userService) {
|
|
||||||
this.sessionHelper = new SessionHelper(req);
|
|
||||||
this.userService = userService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ComponentScope getScope() {
|
|
||||||
return ComponentScope.PerRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Injectable<?> getInjectable(ComponentContext ic, SecurityCheck sc, Parameter c) {
|
|
||||||
return new SecurityCheckInjectable(sessionHelper, userService, sc.value(), sc.apiKeyAllowed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.commafeed.frontend.model;
|
package com.commafeed.frontend.model;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.wordnik.swagger.annotations.ApiModel;
|
import com.wordnik.swagger.annotations.ApiModel;
|
||||||
import com.wordnik.swagger.annotations.ApiModelProperty;
|
import com.wordnik.swagger.annotations.ApiModelProperty;
|
||||||
|
|
||||||
@@ -24,10 +24,10 @@ public class Category implements Serializable {
|
|||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@ApiModelProperty("category children categories")
|
@ApiModelProperty("category children categories")
|
||||||
private List<Category> children = Lists.newArrayList();
|
private List<Category> children = new ArrayList<>();
|
||||||
|
|
||||||
@ApiModelProperty("category feeds")
|
@ApiModelProperty("category feeds")
|
||||||
private List<Subscription> feeds = Lists.newArrayList();
|
private List<Subscription> feeds = new ArrayList<>();
|
||||||
|
|
||||||
@ApiModelProperty("wether the category is expanded or collapsed")
|
@ApiModelProperty("wether the category is expanded or collapsed")
|
||||||
private boolean expanded;
|
private boolean expanded;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.commafeed.frontend.model;
|
package com.commafeed.frontend.model;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.wordnik.swagger.annotations.ApiModel;
|
import com.wordnik.swagger.annotations.ApiModel;
|
||||||
import com.wordnik.swagger.annotations.ApiModelProperty;
|
import com.wordnik.swagger.annotations.ApiModelProperty;
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ public class Entries implements Serializable {
|
|||||||
private int limit;
|
private int limit;
|
||||||
|
|
||||||
@ApiModelProperty("list of entries")
|
@ApiModelProperty("list of entries")
|
||||||
private List<Entry> entries = Lists.newArrayList();
|
private List<Entry> entries = new ArrayList<>();
|
||||||
|
|
||||||
@ApiModelProperty("if true, the unread flag was ignored in the request, all entries are returned regardless of their read status")
|
@ApiModelProperty("if true, the unread flag was ignored in the request, all entries are returned regardless of their read status")
|
||||||
private boolean ignoredReadStatus;
|
private boolean ignoredReadStatus;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user