mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbcd79e49f | ||
|
|
4dabf47822 | ||
|
|
db258d4ecc | ||
|
|
8b237db690 | ||
|
|
416350c004 | ||
|
|
8d63377e78 | ||
|
|
377176df05 | ||
|
|
95da0078b3 | ||
|
|
6392b87afc | ||
|
|
ba04d2adfe | ||
|
|
517ce1a726 | ||
|
|
36492cbff5 | ||
|
|
4b46aa08ac | ||
|
|
1a9a80c0da | ||
|
|
32a30019a7 | ||
|
|
bb72131354 | ||
|
|
3a8d72cab4 | ||
|
|
f5f7a8e63b | ||
|
|
570c4f3a1f | ||
|
|
172164b74b | ||
|
|
49835ae234 | ||
|
|
c4f1e910f8 | ||
|
|
3a621b61c6 | ||
|
|
c28f0d6788 | ||
|
|
2db9224ffc | ||
|
|
043b1df585 | ||
|
|
0626200787 | ||
|
|
b7ee61a8df | ||
|
|
6e1cdaf50e | ||
|
|
e770f802e7 | ||
|
|
8e4cf77fcb | ||
|
|
bc3bd42ce3 | ||
|
|
f73e0ba307 | ||
|
|
5703b5e8d4 | ||
|
|
cecbb2cf72 | ||
|
|
8638e4751d | ||
|
|
3b69e3b029 | ||
|
|
dced21c8e4 | ||
|
|
dab26af294 | ||
|
|
65f118e561 | ||
|
|
67f533b9f6 | ||
|
|
93573bcdb7 | ||
|
|
2263801c55 | ||
|
|
10c34d0440 | ||
|
|
4430ef3847 | ||
|
|
8e331b908d | ||
|
|
dbc6fb58e0 | ||
|
|
db298ab684 | ||
|
|
170a6095e6 | ||
|
|
6dd1bf3281 | ||
|
|
b1500cebfd | ||
|
|
6202bdbc28 | ||
|
|
39bfb61b95 | ||
|
|
fa79524ed4 | ||
|
|
ab5b70e52b | ||
|
|
4f8cd53b83 | ||
|
|
afb6221e5e | ||
|
|
f78aedc30d | ||
|
|
80ff2c8ff7 | ||
|
|
579a77dfc9 | ||
|
|
f902d967a6 | ||
|
|
0899e0b0bf | ||
|
|
65d6f8616b | ||
|
|
5c27f0834c | ||
|
|
a5f7b56bf2 | ||
|
|
63ec92038c | ||
|
|
464ac36ddb | ||
|
|
840bc2ef7a | ||
|
|
e248504528 | ||
|
|
f4f3d9ca48 | ||
|
|
e727ee414b | ||
|
|
1e9295b386 | ||
|
|
b980cdc2c2 | ||
|
|
fbe722facd | ||
|
|
1897d8e0c0 | ||
|
|
3745a152aa | ||
|
|
a7731acb08 | ||
|
|
16dd5deed4 | ||
|
|
c9f70650a0 | ||
|
|
eaa84253df | ||
|
|
45abcd7385 | ||
|
|
8a633aa648 | ||
|
|
05e092062d | ||
|
|
e83602a05c | ||
|
|
abf8666e24 | ||
|
|
af1ccc6669 | ||
|
|
cdcbfbff68 | ||
|
|
6860940afc | ||
|
|
bfc2ee3663 | ||
|
|
b104622081 | ||
|
|
a861387bd7 | ||
|
|
b0f2260fad | ||
|
|
97f0d98ffd | ||
|
|
1ad58a029c | ||
|
|
4c27da0433 | ||
|
|
faf69b43c3 | ||
|
|
7fff561268 | ||
|
|
5e1360a65b | ||
|
|
cc92d2f546 | ||
|
|
def75a250f | ||
|
|
15cd7caf9b | ||
|
|
41a51530ef | ||
|
|
3a101941b3 | ||
|
|
0976fee4df | ||
|
|
f87da777da | ||
|
|
e1c2bf0890 | ||
|
|
b829defb30 | ||
|
|
fa8770d2a7 | ||
|
|
222c8a65af | ||
|
|
76f5b67ac4 | ||
|
|
1791d49efe | ||
|
|
64e1b5df09 | ||
|
|
e1ff077623 | ||
|
|
1361072558 | ||
|
|
5119434d21 | ||
|
|
b29540b14e | ||
|
|
e69785bb89 | ||
|
|
76465fee07 | ||
|
|
b52c459ebb | ||
|
|
1d73982545 | ||
|
|
74f6c45f36 | ||
|
|
0490b528e4 | ||
|
|
ffa1e14449 | ||
|
|
b8fe89b2f4 | ||
|
|
94b293202c | ||
|
|
7ef143a642 | ||
|
|
057f6916e9 | ||
|
|
e24e892cb3 | ||
|
|
78976b06e2 | ||
|
|
96cfcd5b2b | ||
|
|
12bda0122c | ||
|
|
4ac4e5abf2 | ||
|
|
268f0f53a8 | ||
|
|
71521f3428 | ||
|
|
6101fb2bef | ||
|
|
8f6aa0896b | ||
|
|
b8f0af5b2e | ||
|
|
32730f6c41 | ||
|
|
7caa99f8f2 | ||
|
|
4f8e2ab478 | ||
|
|
5c44f392ca | ||
|
|
174d21fd4e | ||
|
|
c2ed6d47f1 | ||
|
|
0f6f717d09 | ||
|
|
d7fb637f68 | ||
|
|
fce9086b27 | ||
|
|
97586cd2c8 | ||
|
|
b74458f0b0 | ||
|
|
7c7a0fceaf | ||
|
|
425a8880cd | ||
|
|
23fe90ec64 | ||
|
|
c01ec5d039 | ||
|
|
4f284165c2 | ||
|
|
2a62ccff11 | ||
|
|
d09cf472dd | ||
|
|
5c721ae6f5 | ||
|
|
2bb8fcdb5f | ||
|
|
6eda93098b | ||
|
|
6344f554d6 | ||
|
|
7e4c1f374c | ||
|
|
28eaab7f7d | ||
|
|
1937944f7e | ||
|
|
3b4b84fdab | ||
|
|
32325bb49c | ||
|
|
c01c1e93f9 | ||
|
|
eac096019f | ||
|
|
9f9389e846 | ||
|
|
a71317881f | ||
|
|
7092824c96 | ||
|
|
0ff998bbd7 | ||
|
|
fc318ad211 | ||
|
|
73323335cb | ||
|
|
ef57c5523d | ||
|
|
846f4a7222 | ||
|
|
05036778d6 | ||
|
|
52df661238 | ||
|
|
7957dc237e | ||
|
|
3fe419ba2f | ||
|
|
61944656b8 | ||
|
|
1cb997b66d | ||
|
|
89463808db | ||
|
|
6aca66d8cf | ||
|
|
38f8102fb3 | ||
|
|
e709499240 | ||
|
|
0b714d5e52 | ||
|
|
98e4f0c6dc | ||
|
|
d82d0af565 | ||
|
|
d8abb7039d | ||
|
|
84dc11048d | ||
|
|
bad915bbaa | ||
|
|
287dea2d36 | ||
|
|
a0b937769d | ||
|
|
6acef4a406 | ||
|
|
8b77eb9850 | ||
|
|
6f22836dcb | ||
|
|
a4347c8878 | ||
|
|
836f7eff09 | ||
|
|
c993bd472d | ||
|
|
431ab92a02 | ||
|
|
94f469a6b1 | ||
|
|
3fec1c6890 | ||
|
|
f8316911bd | ||
|
|
642d1f6be5 | ||
|
|
5a82c3a130 | ||
|
|
6a8174afac | ||
|
|
f4c86634f7 | ||
|
|
322e588a4e | ||
|
|
822dee7a13 | ||
|
|
101e179788 | ||
|
|
57abee6cf0 | ||
|
|
b615847b09 | ||
|
|
ffef87e249 | ||
|
|
ba3b8df4c9 | ||
|
|
40175d3e54 | ||
|
|
06b047cfe6 | ||
|
|
1f4d62ab47 | ||
|
|
a7b826bd4f | ||
|
|
407481faa6 | ||
|
|
305b68546c | ||
|
|
136c41c6aa | ||
|
|
587b25b18b | ||
|
|
beaa40ad65 | ||
|
|
1389a5a238 | ||
|
|
2f34ff8a9f | ||
|
|
d3626b0e7c | ||
|
|
bb4529b6f1 | ||
|
|
dd94125d52 | ||
|
|
a7149e3740 | ||
|
|
b64d041385 | ||
|
|
cc04bdfbc5 | ||
|
|
d8c772ed5e | ||
|
|
dfcc4eeebd | ||
|
|
e491841d4a | ||
|
|
ccb72837b3 | ||
|
|
6560fc9d05 | ||
|
|
14d5879735 | ||
|
|
7fa8bef3de | ||
|
|
966caae727 | ||
|
|
a14484ee03 | ||
|
|
fb9b42ab12 | ||
|
|
6974abdb95 | ||
|
|
65efdeb1df | ||
|
|
54a39ea0a9 | ||
|
|
641350cbde | ||
|
|
06ece8f5ee | ||
|
|
ca87f1c47a | ||
|
|
c38ddb5d00 | ||
|
|
1acd7c4a01 | ||
|
|
d92c2ebdf7 | ||
|
|
8f19e9408e | ||
|
|
3ecb47da5a | ||
|
|
ae03b42c6d | ||
|
|
ee4eb9bb07 | ||
|
|
a0be2e0879 | ||
|
|
a3414d7156 |
@@ -261,7 +261,7 @@
|
||||
<property name="external_port">${env.OPENSHIFT_JBOSSEAP_CLUSTER_PROXY_PORT}
|
||||
</property>
|
||||
<property name="bind_port">7600</property>
|
||||
<property name="bind_addr">${env.OPENSHIFT_INTERNAL_IP}</property>
|
||||
<property name="bind_addr">${env.OPENSHIFT_JBOSSEAP_IP}</property>
|
||||
</transport>
|
||||
<protocol type="TCPPING">
|
||||
<property name="timeout">3000</property>
|
||||
@@ -476,15 +476,15 @@
|
||||
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" />
|
||||
<loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" />
|
||||
<loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
|
||||
</interface>
|
||||
<interface name="unsecure">
|
||||
<!-- Used for IIOP sockets in the standarad configuration. To secure JacORB
|
||||
you need to setup SSL -->
|
||||
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" />
|
||||
<loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
rm -rf $OPENSHIFT_JBOSSAS_LOG_DIR\*.log.*
|
||||
if [ $OPENSHIFT_JBOSSAS_LOG_DIR ]; then
|
||||
rm -rf $OPENSHIFT_JBOSSAS_LOG_DIR/*.log.*
|
||||
fi
|
||||
|
||||
if [ $OPENSHIFT_JBOSSEAP_LOG_DIR ]; then
|
||||
rm -rf $OPENSHIFT_JBOSSEAP_LOG_DIR/*.log.*
|
||||
fi
|
||||
15
README.md
15
README.md
@@ -41,7 +41,7 @@ To install maven and openjdk on Ubuntu, issue the following commands
|
||||
sudo apt-get update
|
||||
sudo apt-get install openjdk-7-jdk maven3
|
||||
|
||||
Not required but if you don't, use 'mvn3' instead of 'mvn' for the rest of the instructions.
|
||||
# Not required but if you don't, use 'mvn3' instead of 'mvn' for the rest of the instructions.
|
||||
sudo ln -s /usr/bin/mvn3 /usr/bin/mvn
|
||||
|
||||
On Windows and other operating systems, just download maven 3.x from the [official site](http://maven.apache.org/), extract it somewhere and add the `bin` directory to your `PATH` environment variable.
|
||||
@@ -54,16 +54,16 @@ If you don't have git you can download the sources as a zip file from [here](htt
|
||||
|
||||
Now build the application
|
||||
|
||||
Embedded HSQL database:
|
||||
# Embedded HSQL database:
|
||||
mvn clean package tomee:build -Pprod
|
||||
|
||||
External MySQL database:
|
||||
# External MySQL database:
|
||||
mvn clean package tomee:build -Pprod -Pmysql
|
||||
|
||||
External PostgreSQL database:
|
||||
# External PostgreSQL database:
|
||||
mvn clean package tomee:build -Pprod -Ppgsql
|
||||
|
||||
External Microsoft SQL Server database:
|
||||
# External Microsoft SQL Server database:
|
||||
mvn clean package tomee:build -Pprod -Pmssql
|
||||
|
||||
It will generate a zip file at `target/commafeed.zip` with everything you need to run the application.
|
||||
@@ -74,12 +74,15 @@ It will generate a zip file at `target/commafeed.zip` with everything you need t
|
||||
* If you don't use the embedded database, create a database in your external database instance, then uncomment the `Resource` element corresponding to the database engine you use from `conf/tomee.xml` and edit the default credentials.
|
||||
* If you'd like to change the default port (8082), edit `conf/server.xml` and look for `<Connector port="8082" protocol="HTTP/1.1"`. Change the port to the value you'd like to use.
|
||||
* CommaFeed will run on the `/commafeed` context. If you'd like to change the context, go to `webapps` and rename `commafeed.war`. Use the special name `ROOT.war` to deploy to the root context.
|
||||
* To start and stop the application, use `bin/startup.sh` and `bin/shutdown.sh` on Linux (you need to `chmod +x bin/*.sh`) or `bin\startup.bat` and `bin\shutdown.bat` on Windows.
|
||||
* To start and stop the application, use `bin/startup.sh` and `bin/shutdown.sh` on Linux (you need to `chmod +x bin/*.sh`) or `bin\startup.bat` and `bin\shutdown.bat` on Windows.
|
||||
If you use the embedded database, note that the database file will be created in the current directory, so make sure you always start the app in the same directory. You can optionally set an absolute path instead of a relative one in `tomee.xml`.
|
||||
* To update the application with a newer version, pull the latest changes and use the same command you used to build the complete TomEE package, but without the `tomee:build` part (keep `-Pprod -P<database>`).
|
||||
This will generate the file `target/commafeed.war`. Copy this file to your tomee `webapps/` directory.
|
||||
* The application is online at [http://localhost:8082/commafeed](http://localhost:8082/commafeed). Don't forget to set the public URL in the admin settings.
|
||||
* The default user is `admin` and the password is `admin`.
|
||||
|
||||
You can use nginx or apache as a proxy http server. Note that when using apache, the `ProxyPreserveHost on` option should be set in your config file.
|
||||
|
||||
Local development
|
||||
-----------------
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
set JAVA_OPTS=-Djava.net.preferIPv4Stack=true -Xmx1024m -XX:MaxPermSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC
|
||||
set JAVA_OPTS=-Djava.net.preferIPv4Stack=true -Xmx1024m -XX:MaxPermSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Djsse.enableSNIExtension=false
|
||||
@@ -1 +1 @@
|
||||
export JAVA_OPTS="-Djava.net.preferIPv4Stack=true -Xmx1024m -XX:MaxPermSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC"
|
||||
export JAVA_OPTS="-Djava.net.preferIPv4Stack=true -Xmx1024m -XX:MaxPermSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Djsse.enableSNIExtension=false"
|
||||
137
pom.xml
137
pom.xml
@@ -4,7 +4,7 @@
|
||||
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<version>1.5.0-SNAPSHOT</version>
|
||||
<packaging>war</packaging>
|
||||
<name>CommaFeed</name>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<artifactId>tomee-maven-plugin</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<configuration>
|
||||
<tomeeVersion>1.5.2</tomeeVersion>
|
||||
<tomeeVersion>1.6.0</tomeeVersion>
|
||||
<tomeeClassifier>plus</tomeeClassifier>
|
||||
<tomeeHttpPort>8082</tomeeHttpPort>
|
||||
<args>-Xmx1024m -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled</args>
|
||||
@@ -104,13 +104,18 @@
|
||||
<lib>org.hibernate.common:hibernate-commons-annotations:4.0.1.Final</lib>
|
||||
<lib>org.hibernate:hibernate-validator:4.3.1.Final</lib>
|
||||
<lib>org.jboss.logging:jboss-logging:3.1.3.GA</lib>
|
||||
<lib>org.javassist:javassist:3.15.0-GA</lib>
|
||||
|
||||
<lib>org.apache.openejb:openejb-bonecp:4.6.0</lib>
|
||||
<lib>com.jolbox:bonecp:0.8.0.RELEASE</lib>
|
||||
<lib>com.google.guava:guava:14.0.1</lib>
|
||||
|
||||
<lib>dom4j:dom4j:1.6.1</lib>
|
||||
<lib>antlr:antlr:2.7.7</lib>
|
||||
<lib>remove:openjpa-</lib>
|
||||
<lib>remove:hsqldb</lib>
|
||||
<lib>org.hsqldb:hsqldb:2.3.0</lib>
|
||||
<lib>mysql:mysql-connector-java:5.1.24</lib>
|
||||
<lib>mysql:mysql-connector-java:5.1.26</lib>
|
||||
<lib>postgresql:postgresql:9.1-901.jdbc4</lib>
|
||||
<lib>net.sourceforge.jtds:jtds:1.3.1</lib>
|
||||
|
||||
@@ -169,7 +174,7 @@
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<version>2.1.5</version>
|
||||
<version>2.1.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -189,7 +194,7 @@
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>0.12.0</version>
|
||||
<version>1.12.6</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -214,28 +219,28 @@
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<version>2.2.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>14.0.1</version>
|
||||
<version>16.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
<version>1.8.3</version>
|
||||
<version>1.9.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.8</version>
|
||||
<version>1.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-collections</groupId>
|
||||
@@ -260,7 +265,7 @@
|
||||
<dependency>
|
||||
<groupId>commons-fileupload</groupId>
|
||||
<artifactId>commons-fileupload</artifactId>
|
||||
<version>1.3</version>
|
||||
<version>1.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -298,34 +303,34 @@
|
||||
<dependency>
|
||||
<groupId>com.google.gwt</groupId>
|
||||
<artifactId>gwt-servlet</artifactId>
|
||||
<version>2.5.1</version>
|
||||
<version>2.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.cssparser</groupId>
|
||||
<artifactId>cssparser</artifactId>
|
||||
<version>0.9.9</version>
|
||||
<version>0.9.13</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.2.5</version>
|
||||
<version>4.3.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.7.2</version>
|
||||
<version>1.7.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.2.2</version>
|
||||
<version>2.3.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
<version>1.7.5</version>
|
||||
<version>1.7.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>log4j</groupId>
|
||||
@@ -336,28 +341,28 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.wicket</groupId>
|
||||
<artifactId>wicket-core</artifactId>
|
||||
<version>6.9.1</version>
|
||||
<version>6.14.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.wicket</groupId>
|
||||
<artifactId>wicket-auth-roles</artifactId>
|
||||
<version>6.9.1</version>
|
||||
<version>6.14.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.wicket</groupId>
|
||||
<artifactId>wicket-extensions</artifactId>
|
||||
<version>6.9.1</version>
|
||||
<version>6.14.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.wicket</groupId>
|
||||
<artifactId>wicket-cdi</artifactId>
|
||||
<version>6.9.1</version>
|
||||
<version>6.14.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ro.isdc.wro4j</groupId>
|
||||
<artifactId>wro4j-extensions</artifactId>
|
||||
<version>1.6.3</version>
|
||||
<version>1.7.5</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -372,6 +377,88 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.codahale.metrics</groupId>
|
||||
<artifactId>metrics-core</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.codahale.metrics</groupId>
|
||||
<artifactId>metrics-json</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>lodash</artifactId>
|
||||
<version>2.4.1-3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>jquery</artifactId>
|
||||
<version>1.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>bootstrap</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>jquery-mousewheel</artifactId>
|
||||
<version>3.1.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>angularjs</artifactId>
|
||||
<version>1.2.16</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>angular-ui-router</artifactId>
|
||||
<version>0.2.8-2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>angular-ui-utils</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>ui-select2</artifactId>
|
||||
<version>0.0.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>angular-ui-bootstrap</artifactId>
|
||||
<version>0.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>mousetrap</artifactId>
|
||||
<version>1.4.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>momentjs</artifactId>
|
||||
<version>2.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>ng-grid</artifactId>
|
||||
<version>2.0.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>device.js</artifactId>
|
||||
<version>139f208</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>ngInfiniteScroll</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
@@ -391,7 +478,7 @@
|
||||
<plugin>
|
||||
<groupId>ro.isdc.wro4j</groupId>
|
||||
<artifactId>wro4j-maven-plugin</artifactId>
|
||||
<version>1.6.3</version>
|
||||
<version>1.7.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>js</id>
|
||||
@@ -401,7 +488,7 @@
|
||||
</goals>
|
||||
<configuration>
|
||||
<targetGroups>app</targetGroups>
|
||||
<options>indent,devel,noarg,quotmark,laxcomma,laxbreak</options>
|
||||
<options>devel,noarg,quotmark,laxcomma,laxbreak</options>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@@ -538,7 +625,7 @@
|
||||
<plugin>
|
||||
<groupId>ro.isdc.wro4j</groupId>
|
||||
<artifactId>wro4j-maven-plugin</artifactId>
|
||||
<version>1.6.3</version>
|
||||
<version>1.7.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
|
||||
@@ -4,42 +4,45 @@ import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.http.Consts;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HeaderElement;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpException;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpResponseInterceptor;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.HttpResponseException;
|
||||
import org.apache.http.client.config.CookieSpecs;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.params.CookiePolicy;
|
||||
import org.apache.http.client.params.HttpClientParams;
|
||||
import org.apache.http.conn.ClientConnectionManager;
|
||||
import org.apache.http.conn.scheme.Scheme;
|
||||
import org.apache.http.conn.scheme.SchemeRegistry;
|
||||
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||
import org.apache.http.impl.client.DecompressingHttpClient;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
|
||||
import org.apache.http.impl.client.SystemDefaultHttpClient;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
import org.apache.http.params.HttpProtocolParams;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.config.ConnectionConfig;
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.http.entity.HttpEntityWrapper;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.wicket.util.io.IOUtils;
|
||||
|
||||
/**
|
||||
* Smart HTTP getter: handles gzip, ssl, last modified and etag headers
|
||||
@@ -52,8 +55,32 @@ public class HttpGetter {
|
||||
private static final String ACCEPT_LANGUAGE = "en";
|
||||
private static final String PRAGMA_NO_CACHE = "No-cache";
|
||||
private static final String CACHE_CONTROL_NO_CACHE = "no-cache";
|
||||
private static final String UTF8 = "UTF-8";
|
||||
private static final String HTTPS = "https";
|
||||
|
||||
private static final List<String> ALLOWED_CONTENT_ENCODINGS = Arrays.asList("gzip", "x-gzip", "deflate", "identity");
|
||||
private static final HttpResponseInterceptor REMOVE_INCORRECT_CONTENT_ENCODING = new HttpResponseInterceptor() {
|
||||
|
||||
@Override
|
||||
public void process(HttpResponse response, HttpContext context) throws HttpException, IOException {
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity != null && entity.getContentLength() != 0) {
|
||||
Header header = entity.getContentEncoding();
|
||||
if (header != null) {
|
||||
HeaderElement[] codecs = header.getElements();
|
||||
for (final HeaderElement codec : codecs) {
|
||||
String codecName = codec.getName().toLowerCase(Locale.US);
|
||||
if (!ALLOWED_CONTENT_ENCODINGS.contains(codecName)) {
|
||||
response.setEntity(new HttpEntityWrapper(entity) {
|
||||
@Override
|
||||
public Header getContentEncoding() {
|
||||
return null;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static SSLContext SSL_CONTEXT = null;
|
||||
static {
|
||||
@@ -65,8 +92,6 @@ public class HttpGetter {
|
||||
}
|
||||
}
|
||||
|
||||
private static final X509HostnameVerifier VERIFIER = new DefaultHostnameVerifier();
|
||||
|
||||
public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
|
||||
return getBinary(url, null, null, timeout);
|
||||
}
|
||||
@@ -90,9 +115,12 @@ public class HttpGetter {
|
||||
HttpResult result = null;
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
HttpClient client = newClient(timeout);
|
||||
CloseableHttpClient client = newClient(timeout);
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
HttpGet httpget = new HttpGet(url);
|
||||
HttpClientContext context = HttpClientContext.create();
|
||||
|
||||
httpget.addHeader(HttpHeaders.ACCEPT_LANGUAGE, ACCEPT_LANGUAGE);
|
||||
httpget.addHeader(HttpHeaders.PRAGMA, PRAGMA_NO_CACHE);
|
||||
httpget.addHeader(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
@@ -105,9 +133,8 @@ public class HttpGetter {
|
||||
httpget.addHeader(HttpHeaders.IF_NONE_MATCH, eTag);
|
||||
}
|
||||
|
||||
HttpResponse response = null;
|
||||
try {
|
||||
response = client.execute(httpget);
|
||||
response = client.execute(httpget, context);
|
||||
int code = response.getStatusLine().getStatusCode();
|
||||
if (code == HttpStatus.SC_NOT_MODIFIED) {
|
||||
throw new NotModifiedException("'304 - not modified' http code received");
|
||||
@@ -123,15 +150,14 @@ public class HttpGetter {
|
||||
}
|
||||
}
|
||||
Header lastModifiedHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED);
|
||||
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
|
||||
|
||||
String lastModifiedResponse = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
|
||||
if (lastModified != null && StringUtils.equals(lastModified, lastModifiedResponse)) {
|
||||
String lastModifiedHeaderValue = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
|
||||
if (lastModifiedHeaderValue != null && StringUtils.equals(lastModified, lastModifiedHeaderValue)) {
|
||||
throw new NotModifiedException("lastModifiedHeader is the same");
|
||||
}
|
||||
|
||||
String eTagResponse = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue());
|
||||
if (eTag != null && StringUtils.equals(eTag, eTagResponse)) {
|
||||
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
|
||||
String eTagHeaderValue = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue());
|
||||
if (eTag != null && StringUtils.equals(eTag, eTagHeaderValue)) {
|
||||
throw new NotModifiedException("eTagHeader is the same");
|
||||
}
|
||||
|
||||
@@ -144,12 +170,15 @@ public class HttpGetter {
|
||||
contentType = entity.getContentType().getValue();
|
||||
}
|
||||
}
|
||||
HttpUriRequest req = (HttpUriRequest) context.getRequest();
|
||||
HttpHost host = context.getTargetHost();
|
||||
String urlAfterRedirect = req.getURI().isAbsolute() ? req.getURI().toString() : host.toURI() + req.getURI();
|
||||
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
result = new HttpResult(content, contentType, lastModifiedHeader == null ? null : lastModifiedHeader.getValue(),
|
||||
eTagHeader == null ? null : eTagHeader.getValue(), duration);
|
||||
result = new HttpResult(content, contentType, lastModifiedHeaderValue, eTagHeaderValue, duration, urlAfterRedirect);
|
||||
} finally {
|
||||
client.getConnectionManager().shutdown();
|
||||
IOUtils.closeQuietly(response);
|
||||
IOUtils.closeQuietly(client);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -161,13 +190,15 @@ public class HttpGetter {
|
||||
private String lastModifiedSince;
|
||||
private String eTag;
|
||||
private long duration;
|
||||
private String urlAfterRedirect;
|
||||
|
||||
public HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, long duration) {
|
||||
public HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, long duration, String urlAfterRedirect) {
|
||||
this.content = content;
|
||||
this.contentType = contentType;
|
||||
this.lastModifiedSince = lastModifiedSince;
|
||||
this.eTag = eTag;
|
||||
this.duration = duration;
|
||||
this.urlAfterRedirect = urlAfterRedirect;
|
||||
}
|
||||
|
||||
public byte[] getContent() {
|
||||
@@ -190,23 +221,30 @@ public class HttpGetter {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public String getUrlAfterRedirect() {
|
||||
return urlAfterRedirect;
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpClient newClient(int timeout) {
|
||||
DefaultHttpClient client = new SystemDefaultHttpClient();
|
||||
public static CloseableHttpClient newClient(int timeout) {
|
||||
HttpClientBuilder builder = HttpClients.custom();
|
||||
builder.useSystemProperties();
|
||||
builder.addInterceptorFirst(REMOVE_INCORRECT_CONTENT_ENCODING);
|
||||
builder.disableAutomaticRetries();
|
||||
|
||||
SSLSocketFactory ssf = new SSLSocketFactory(SSL_CONTEXT, VERIFIER);
|
||||
ClientConnectionManager ccm = client.getConnectionManager();
|
||||
SchemeRegistry sr = ccm.getSchemeRegistry();
|
||||
sr.register(new Scheme(HTTPS, 443, ssf));
|
||||
builder.setSslcontext(SSL_CONTEXT);
|
||||
builder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
|
||||
|
||||
HttpParams params = client.getParams();
|
||||
HttpClientParams.setCookiePolicy(params, CookiePolicy.IGNORE_COOKIES);
|
||||
HttpProtocolParams.setContentCharset(params, UTF8);
|
||||
HttpConnectionParams.setConnectionTimeout(params, timeout);
|
||||
HttpConnectionParams.setSoTimeout(params, timeout);
|
||||
client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
|
||||
return new DecompressingHttpClient(client);
|
||||
RequestConfig.Builder configBuilder = RequestConfig.custom();
|
||||
configBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES);
|
||||
configBuilder.setSocketTimeout(timeout);
|
||||
configBuilder.setConnectTimeout(timeout);
|
||||
configBuilder.setConnectionRequestTimeout(timeout);
|
||||
builder.setDefaultRequestConfig(configBuilder.build());
|
||||
|
||||
builder.setDefaultConnectionConfig(ConnectionConfig.custom().setCharset(Consts.ISO_8859_1).build());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static class NotModifiedException extends Exception {
|
||||
@@ -232,24 +270,4 @@ public class HttpGetter {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultHostnameVerifier implements X509HostnameVerifier {
|
||||
|
||||
@Override
|
||||
public void verify(String string, SSLSocket ssls) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(String string, X509Certificate xc) throws SSLException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(String string, String[] strings, String[] strings1) throws SSLException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String string, SSLSession ssls) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
package com.commafeed.backend;
|
||||
|
||||
import javax.ejb.Singleton;
|
||||
import javax.interceptor.AroundInvoke;
|
||||
import javax.interceptor.InvocationContext;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.stat.Statistics;
|
||||
|
||||
@Singleton
|
||||
public class MetricsBean {
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
|
||||
private Metric lastMinute = new Metric();
|
||||
private Metric thisMinute = new Metric();
|
||||
|
||||
private Metric lastHour = new Metric();
|
||||
private Metric thisHour = new Metric();
|
||||
|
||||
private long minuteTimestamp;
|
||||
private long hourTimestamp;
|
||||
|
||||
@AroundInvoke
|
||||
private Object roll(InvocationContext context) throws Exception {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - minuteTimestamp > 60000) {
|
||||
lastMinute = thisMinute;
|
||||
thisMinute = new Metric();
|
||||
minuteTimestamp = now;
|
||||
|
||||
}
|
||||
|
||||
if (now - hourTimestamp > 60000 * 60) {
|
||||
lastHour = thisHour;
|
||||
thisHour = new Metric();
|
||||
hourTimestamp = now;
|
||||
|
||||
}
|
||||
return context.proceed();
|
||||
}
|
||||
|
||||
public void feedRefreshed() {
|
||||
thisMinute.feedsRefreshed++;
|
||||
thisHour.feedsRefreshed++;
|
||||
}
|
||||
|
||||
public void feedUpdated() {
|
||||
thisHour.feedsUpdated++;
|
||||
thisMinute.feedsUpdated++;
|
||||
|
||||
}
|
||||
|
||||
public void entryInserted() {
|
||||
|
||||
thisHour.entriesInserted++;
|
||||
thisMinute.entriesInserted++;
|
||||
|
||||
}
|
||||
|
||||
public void entryCacheHit() {
|
||||
thisHour.entryCacheHit++;
|
||||
thisMinute.entryCacheHit++;
|
||||
}
|
||||
|
||||
public void entryCacheMiss() {
|
||||
thisHour.entryCacheMiss++;
|
||||
thisMinute.entryCacheMiss++;
|
||||
}
|
||||
|
||||
public void pushReceived(int feedCount) {
|
||||
|
||||
thisHour.pushNotificationsReceived++;
|
||||
thisMinute.pushNotificationsReceived++;
|
||||
|
||||
thisHour.pushFeedsQueued += feedCount;
|
||||
thisMinute.pushFeedsQueued += feedCount;
|
||||
}
|
||||
|
||||
public void threadWaited() {
|
||||
thisHour.threadWaited++;
|
||||
thisMinute.threadWaited++;
|
||||
}
|
||||
|
||||
public Metric getLastMinute() {
|
||||
return lastMinute;
|
||||
}
|
||||
|
||||
public Metric getLastHour() {
|
||||
return lastHour;
|
||||
}
|
||||
|
||||
public String getCacheStats() {
|
||||
Session session = em.unwrap(Session.class);
|
||||
SessionFactory sessionFactory = session.getSessionFactory();
|
||||
Statistics statistics = sessionFactory.getStatistics();
|
||||
return statistics.toString();
|
||||
}
|
||||
|
||||
public static class Metric {
|
||||
private int feedsRefreshed;
|
||||
private int feedsUpdated;
|
||||
private int entriesInserted;
|
||||
private int threadWaited;
|
||||
private int pushNotificationsReceived;
|
||||
private int pushFeedsQueued;
|
||||
private int entryCacheHit;
|
||||
private int entryCacheMiss;
|
||||
|
||||
public int getFeedsRefreshed() {
|
||||
return feedsRefreshed;
|
||||
}
|
||||
|
||||
public void setFeedsRefreshed(int feedsRefreshed) {
|
||||
this.feedsRefreshed = feedsRefreshed;
|
||||
}
|
||||
|
||||
public int getFeedsUpdated() {
|
||||
return feedsUpdated;
|
||||
}
|
||||
|
||||
public void setFeedsUpdated(int feedsUpdated) {
|
||||
this.feedsUpdated = feedsUpdated;
|
||||
}
|
||||
|
||||
public int getEntriesInserted() {
|
||||
return entriesInserted;
|
||||
}
|
||||
|
||||
public void setEntriesInserted(int entriesInserted) {
|
||||
this.entriesInserted = entriesInserted;
|
||||
}
|
||||
|
||||
public int getThreadWaited() {
|
||||
return threadWaited;
|
||||
}
|
||||
|
||||
public void setThreadWaited(int threadWaited) {
|
||||
this.threadWaited = threadWaited;
|
||||
}
|
||||
|
||||
public int getPushNotificationsReceived() {
|
||||
return pushNotificationsReceived;
|
||||
}
|
||||
|
||||
public void setPushNotificationsReceived(int pushNotificationsReceived) {
|
||||
this.pushNotificationsReceived = pushNotificationsReceived;
|
||||
}
|
||||
|
||||
public int getPushFeedsQueued() {
|
||||
return pushFeedsQueued;
|
||||
}
|
||||
|
||||
public void setPushFeedsQueued(int pushFeedsQueued) {
|
||||
this.pushFeedsQueued = pushFeedsQueued;
|
||||
}
|
||||
|
||||
public int getEntryCacheHit() {
|
||||
return entryCacheHit;
|
||||
}
|
||||
|
||||
public void setEntryCacheHit(int entryCacheHit) {
|
||||
this.entryCacheHit = entryCacheHit;
|
||||
}
|
||||
|
||||
public int getEntryCacheMiss() {
|
||||
return entryCacheMiss;
|
||||
}
|
||||
|
||||
public void setEntryCacheMiss(int entryCacheMiss) {
|
||||
this.entryCacheMiss = entryCacheMiss;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,36 +4,45 @@ import java.util.Date;
|
||||
|
||||
import javax.ejb.Schedule;
|
||||
import javax.ejb.Stateless;
|
||||
import javax.ejb.TransactionManagement;
|
||||
import javax.ejb.TransactionManagementType;
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.DatabaseCleaningService;
|
||||
|
||||
/**
|
||||
* Contains all scheduled tasks
|
||||
*
|
||||
*/
|
||||
@Stateless
|
||||
@TransactionManagement(TransactionManagementType.BEAN)
|
||||
public class ScheduledTasks {
|
||||
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
DatabaseCleaner cleaner;
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
DatabaseCleaningService cleaner;
|
||||
|
||||
/**
|
||||
* clean old read statuses, runs every day at midnight
|
||||
* clean old read statuses
|
||||
*/
|
||||
@Schedule(hour = "0", persistent = false)
|
||||
@Schedule(hour = "*", persistent = false)
|
||||
private void cleanupOldStatuses() {
|
||||
Date threshold = applicationSettingsService.getUnreadThreshold();
|
||||
if (threshold != null) {
|
||||
cleaner.cleanStatusesOlderThan(threshold);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* clean feeds without subscriptions, then clean contents without entries
|
||||
*/
|
||||
@Schedule(hour = "*", persistent = false)
|
||||
private void cleanFeedsAndContents() {
|
||||
cleaner.cleanEntriesWithoutSubscriptions();
|
||||
cleaner.cleanFeedsWithoutSubscriptions();
|
||||
cleaner.cleanContentsWithoutEntries();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.enterprise.inject.Alternative;
|
||||
|
||||
import org.apache.commons.pool.impl.GenericObjectPool;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
@@ -30,7 +33,14 @@ public class RedisCacheService extends CacheService {
|
||||
|
||||
private static ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
|
||||
private JedisPool pool;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
JedisPoolConfig config = new JedisPoolConfig();
|
||||
config.setWhenExhaustedAction(GenericObjectPool.WHEN_EXHAUSTED_GROW);
|
||||
pool = new JedisPool(config, "localhost");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLastEntries(Feed feed) {
|
||||
|
||||
@@ -6,15 +6,12 @@ import java.util.List;
|
||||
import javax.ejb.Stateless;
|
||||
import javax.persistence.TypedQuery;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Expression;
|
||||
import javax.persistence.criteria.Join;
|
||||
import javax.persistence.criteria.JoinType;
|
||||
import javax.persistence.criteria.Path;
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import javax.persistence.criteria.Root;
|
||||
import javax.persistence.criteria.SetJoin;
|
||||
import javax.persistence.criteria.Subquery;
|
||||
import javax.persistence.metamodel.SingularAttribute;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
@@ -26,7 +23,6 @@ import com.commafeed.backend.model.FeedSubscription_;
|
||||
import com.commafeed.backend.model.Feed_;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.User_;
|
||||
import com.commafeed.frontend.model.FeedCount;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
@@ -95,8 +91,8 @@ public class FeedDAO extends GenericDAO<Feed> {
|
||||
public List<Feed> findByTopic(String topic) {
|
||||
return findByField(Feed_.pushTopicHash, DigestUtils.sha1Hex(topic));
|
||||
}
|
||||
|
||||
public int deleteWithoutSubscriptions(int max) {
|
||||
|
||||
public List<Feed> findWithoutSubscriptions(int max) {
|
||||
CriteriaQuery<Feed> query = builder.createQuery(getType());
|
||||
Root<Feed> root = query.from(getType());
|
||||
|
||||
@@ -105,54 +101,6 @@ public class FeedDAO extends GenericDAO<Feed> {
|
||||
TypedQuery<Feed> q = em.createQuery(query);
|
||||
q.setMaxResults(max);
|
||||
|
||||
List<Feed> list = q.getResultList();
|
||||
int deleted = list.size();
|
||||
|
||||
delete(list);
|
||||
return deleted;
|
||||
|
||||
}
|
||||
|
||||
public static enum DuplicateMode {
|
||||
NORMALIZED_URL(Feed_.normalizedUrlHash), LAST_CONTENT(Feed_.lastContentHash), PUSH_TOPIC(Feed_.pushTopicHash);
|
||||
private SingularAttribute<Feed, String> path;
|
||||
|
||||
private DuplicateMode(SingularAttribute<Feed, String> path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public SingularAttribute<Feed, String> getPath() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public List<FeedCount> findDuplicates(DuplicateMode mode, int offset, int limit, long minCount) {
|
||||
CriteriaQuery<String> query = builder.createQuery(String.class);
|
||||
Root<Feed> root = query.from(getType());
|
||||
|
||||
Path<String> path = root.get(mode.getPath());
|
||||
Expression<Long> count = builder.count(path);
|
||||
|
||||
query.select(path);
|
||||
|
||||
query.groupBy(path);
|
||||
query.having(builder.greaterThan(count, minCount));
|
||||
|
||||
TypedQuery<String> q = em.createQuery(query);
|
||||
limit(q, offset, limit);
|
||||
List<String> pathValues = q.getResultList();
|
||||
|
||||
List<FeedCount> result = Lists.newArrayList();
|
||||
for (String pathValue : pathValues) {
|
||||
FeedCount fc = new FeedCount(pathValue);
|
||||
for (Feed feed : findByField(mode.getPath(), pathValue)) {
|
||||
Feed f = new Feed();
|
||||
f.setId(feed.getId());
|
||||
f.setUrl(feed.getUrl());
|
||||
fc.getFeeds().add(f);
|
||||
}
|
||||
result.add(fc);
|
||||
}
|
||||
return result;
|
||||
return q.getResultList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.backend.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.persistence.TypedQuery;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Join;
|
||||
@@ -15,6 +16,7 @@ import com.commafeed.backend.model.FeedEntryContent_;
|
||||
import com.commafeed.backend.model.FeedEntry_;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
@Stateless
|
||||
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
||||
|
||||
public Long findExisting(String contentHash, String titleHash) {
|
||||
@@ -44,6 +46,7 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
||||
|
||||
List<FeedEntryContent> list = q.getResultList();
|
||||
int deleted = list.size();
|
||||
delete(list);
|
||||
return deleted;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,13 +6,19 @@ import java.util.List;
|
||||
import javax.ejb.Stateless;
|
||||
import javax.persistence.TypedQuery;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Join;
|
||||
import javax.persistence.criteria.JoinType;
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import javax.persistence.criteria.Root;
|
||||
import javax.persistence.criteria.SetJoin;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
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.backend.model.Feed_;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
@@ -36,6 +42,34 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
return Iterables.getFirst(list, null);
|
||||
}
|
||||
|
||||
public List<FeedEntry> findWithoutSubscriptions(int max) {
|
||||
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
|
||||
Root<FeedEntry> root = query.from(getType());
|
||||
|
||||
Join<FeedEntry, Feed> feedJoin = root.join(FeedEntry_.feed);
|
||||
SetJoin<Feed, FeedSubscription> subJoin = feedJoin.join(Feed_.subscriptions, JoinType.LEFT);
|
||||
query.where(builder.isNull(subJoin.get(FeedSubscription_.id)));
|
||||
TypedQuery<FeedEntry> q = em.createQuery(query);
|
||||
q.setMaxResults(max);
|
||||
|
||||
return q.getResultList();
|
||||
}
|
||||
|
||||
public int delete(Feed feed, int max) {
|
||||
|
||||
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
|
||||
Root<FeedEntry> root = query.from(getType());
|
||||
|
||||
query.where(builder.equal(root.get(FeedEntry_.feed), feed));
|
||||
TypedQuery<FeedEntry> q = em.createQuery(query);
|
||||
q.setMaxResults(max);
|
||||
|
||||
List<FeedEntry> list = q.getResultList();
|
||||
int deleted = list.size();
|
||||
delete(list);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public int delete(Date olderThan, int max) {
|
||||
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
|
||||
Root<FeedEntry> root = query.from(getType());
|
||||
|
||||
@@ -15,8 +15,9 @@ import javax.persistence.criteria.Predicate;
|
||||
import javax.persistence.criteria.Root;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.builder.CompareToBuilder;
|
||||
import org.hibernate.Criteria;
|
||||
import org.hibernate.criterion.Conjunction;
|
||||
import org.hibernate.criterion.Disjunction;
|
||||
import org.hibernate.criterion.MatchMode;
|
||||
import org.hibernate.criterion.Order;
|
||||
@@ -31,6 +32,8 @@ import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryContent_;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedEntryStatus_;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.FeedEntryTag_;
|
||||
import com.commafeed.backend.model.FeedEntry_;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.Models;
|
||||
@@ -40,31 +43,34 @@ import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.frontend.model.UnreadCount;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Ordering;
|
||||
|
||||
@Stateless
|
||||
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
|
||||
private static final String ALIAS_STATUS = "status";
|
||||
private static final String ALIAS_ENTRY = "entry";
|
||||
private static final String ALIAS_TAG = "tag";
|
||||
|
||||
@Inject
|
||||
FeedEntryTagDAO feedEntryTagDAO;
|
||||
|
||||
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_DESC = new Comparator<FeedEntryStatus>() {
|
||||
@Override
|
||||
public int compare(FeedEntryStatus o1, FeedEntryStatus o2) {
|
||||
return ObjectUtils.compare(o2.getEntryUpdated(), o1.getEntryUpdated());
|
||||
CompareToBuilder builder = new CompareToBuilder();
|
||||
builder.append(o2.getEntryUpdated(), o1.getEntryUpdated());
|
||||
builder.append(o2.getId(), o1.getId());
|
||||
return builder.build();
|
||||
};
|
||||
};
|
||||
|
||||
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = new Comparator<FeedEntryStatus>() {
|
||||
@Override
|
||||
public int compare(FeedEntryStatus o1, FeedEntryStatus o2) {
|
||||
return ObjectUtils.compare(o1.getEntryUpdated(), o2.getEntryUpdated());
|
||||
};
|
||||
};
|
||||
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse();
|
||||
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
public FeedEntryStatus getStatus(FeedSubscription sub, FeedEntry entry) {
|
||||
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
|
||||
|
||||
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
|
||||
Root<FeedEntryStatus> root = query.from(getType());
|
||||
@@ -77,14 +83,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
List<FeedEntryStatus> statuses = em.createQuery(query).getResultList();
|
||||
FeedEntryStatus status = Iterables.getFirst(statuses, null);
|
||||
|
||||
return handleStatus(status, sub, entry);
|
||||
return handleStatus(user, status, sub, entry);
|
||||
}
|
||||
|
||||
private FeedEntryStatus handleStatus(FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
|
||||
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
|
||||
if (status == null) {
|
||||
Date unreadThreshold = applicationSettingsService.getUnreadThreshold();
|
||||
boolean read = unreadThreshold == null ? false : entry.getUpdated().before(unreadThreshold);
|
||||
status = new FeedEntryStatus(sub.getUser(), sub, entry);
|
||||
status = new FeedEntryStatus(user, sub, entry);
|
||||
status.setRead(read);
|
||||
status.setMarkable(!read);
|
||||
} else {
|
||||
@@ -93,6 +99,12 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
return status;
|
||||
}
|
||||
|
||||
private FeedEntryStatus fetchTags(User user, FeedEntryStatus status) {
|
||||
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, status.getEntry());
|
||||
status.setTags(tags);
|
||||
return status;
|
||||
}
|
||||
|
||||
public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) {
|
||||
|
||||
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
|
||||
@@ -102,11 +114,12 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
|
||||
predicates.add(builder.equal(root.get(FeedEntryStatus_.user), user));
|
||||
predicates.add(builder.equal(root.get(FeedEntryStatus_.starred), true));
|
||||
query.where(predicates.toArray(new Predicate[0]));
|
||||
|
||||
if (newerThan != null) {
|
||||
predicates.add(builder.greaterThanOrEqualTo(root.get(FeedEntryStatus_.entryInserted), newerThan));
|
||||
}
|
||||
|
||||
query.where(predicates.toArray(new Predicate[0]));
|
||||
|
||||
orderStatusesBy(query, root, order);
|
||||
|
||||
@@ -115,13 +128,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
setTimeout(q);
|
||||
List<FeedEntryStatus> statuses = q.getResultList();
|
||||
for (FeedEntryStatus status : statuses) {
|
||||
status = handleStatus(status, status.getSubscription(), status.getEntry());
|
||||
status = handleStatus(user, status, status.getSubscription(), status.getEntry());
|
||||
status = fetchTags(user, status);
|
||||
}
|
||||
return lazyLoadContent(includeContent, statuses);
|
||||
}
|
||||
|
||||
private Criteria buildSearchCriteria(FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset, int limit,
|
||||
ReadingOrder order, Date last) {
|
||||
private Criteria buildSearchCriteria(User user, FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset, int limit,
|
||||
ReadingOrder order, Date last, String tag) {
|
||||
Criteria criteria = getSession().createCriteria(FeedEntry.class, ALIAS_ENTRY);
|
||||
|
||||
criteria.add(Restrictions.eq(FeedEntry_.feed.getName(), sub.getFeed()));
|
||||
@@ -139,7 +153,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
Criteria statusJoin = criteria.createCriteria(FeedEntry_.statuses.getName(), ALIAS_STATUS, JoinType.LEFT_OUTER_JOIN,
|
||||
Restrictions.eq(FeedEntryStatus_.subscription.getName(), sub));
|
||||
|
||||
if (unreadOnly) {
|
||||
if (unreadOnly && tag == null) {
|
||||
|
||||
Disjunction or = Restrictions.disjunction();
|
||||
or.add(Restrictions.isNull(FeedEntryStatus_.read.getName()));
|
||||
@@ -152,6 +166,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
}
|
||||
}
|
||||
|
||||
if (tag != null) {
|
||||
Conjunction and = Restrictions.conjunction();
|
||||
and.add(Restrictions.eq(FeedEntryTag_.user.getName(), user));
|
||||
and.add(Restrictions.eq(FeedEntryTag_.name.getName(), tag));
|
||||
criteria.createCriteria(FeedEntry_.tags.getName(), ALIAS_TAG, JoinType.INNER_JOIN, and);
|
||||
}
|
||||
|
||||
if (newerThan != null) {
|
||||
criteria.add(Restrictions.ge(FeedEntry_.inserted.getName(), newerThan));
|
||||
}
|
||||
@@ -165,13 +186,11 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
}
|
||||
|
||||
if (order != null) {
|
||||
Order o = null;
|
||||
if (order == ReadingOrder.asc) {
|
||||
o = Order.asc(FeedEntry_.updated.getName());
|
||||
criteria.addOrder(Order.asc(FeedEntry_.updated.getName())).addOrder(Order.asc(FeedEntry_.id.getName()));
|
||||
} else {
|
||||
o = Order.desc(FeedEntry_.updated.getName());
|
||||
criteria.addOrder(Order.desc(FeedEntry_.updated.getName())).addOrder(Order.desc(FeedEntry_.id.getName()));
|
||||
}
|
||||
criteria.addOrder(o);
|
||||
}
|
||||
if (offset > -1) {
|
||||
criteria.setFirstResult(offset);
|
||||
@@ -188,14 +207,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<FeedEntryStatus> findBySubscriptions(List<FeedSubscription> subs, boolean unreadOnly, String keywords, Date newerThan,
|
||||
int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds) {
|
||||
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly, String keywords,
|
||||
Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds, String tag) {
|
||||
int capacity = offset + limit;
|
||||
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
|
||||
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator);
|
||||
for (FeedSubscription sub : subs) {
|
||||
Date last = (order != null && set.isFull()) ? set.last().getEntryUpdated() : null;
|
||||
Criteria criteria = buildSearchCriteria(sub, unreadOnly, keywords, newerThan, -1, capacity, order, last);
|
||||
Criteria criteria = buildSearchCriteria(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag);
|
||||
ProjectionList projection = Projections.projectionList();
|
||||
projection.add(Projections.property("id"), "id");
|
||||
projection.add(Projections.property("updated"), "updated");
|
||||
@@ -237,7 +256,10 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
for (FeedEntryStatus placeholder : placeholders) {
|
||||
Long statusId = placeholder.getId();
|
||||
FeedEntry entry = em.find(FeedEntry.class, placeholder.getEntry().getId());
|
||||
statuses.add(handleStatus(statusId == null ? null : findById(statusId), placeholder.getSubscription(), entry));
|
||||
FeedEntryStatus status = handleStatus(user, statusId == null ? null : findById(statusId), placeholder.getSubscription(),
|
||||
entry);
|
||||
status = fetchTags(user, status);
|
||||
statuses.add(status);
|
||||
}
|
||||
statuses = lazyLoadContent(includeContent, statuses);
|
||||
}
|
||||
@@ -245,9 +267,9 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public UnreadCount getUnreadCount(FeedSubscription subscription) {
|
||||
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
|
||||
UnreadCount uc = null;
|
||||
Criteria criteria = buildSearchCriteria(subscription, true, null, null, -1, -1, null, null);
|
||||
Criteria criteria = buildSearchCriteria(user, subscription, true, null, null, -1, -1, null, null, null);
|
||||
ProjectionList projection = Projections.projectionList();
|
||||
projection.add(Projections.rowCount(), "count");
|
||||
projection.add(Projections.max(FeedEntry_.updated.getName()), "updated");
|
||||
@@ -273,15 +295,15 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
}
|
||||
|
||||
private void orderStatusesBy(CriteriaQuery<?> query, Path<FeedEntryStatus> statusJoin, ReadingOrder order) {
|
||||
orderBy(query, statusJoin.get(FeedEntryStatus_.entryUpdated), order);
|
||||
orderBy(query, statusJoin.get(FeedEntryStatus_.entryUpdated), statusJoin.get(FeedEntryStatus_.id), order);
|
||||
}
|
||||
|
||||
private void orderBy(CriteriaQuery<?> query, Path<Date> date, ReadingOrder order) {
|
||||
private void orderBy(CriteriaQuery<?> query, Path<Date> date, Path<Long> id, ReadingOrder order) {
|
||||
if (order != null) {
|
||||
if (order == ReadingOrder.asc) {
|
||||
query.orderBy(builder.asc(date));
|
||||
query.orderBy(builder.asc(date), builder.asc(id));
|
||||
} else {
|
||||
query.orderBy(builder.desc(date));
|
||||
query.orderBy(builder.desc(date), builder.desc(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,10 +312,17 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
setTimeout(query, applicationSettingsService.get().getQueryTimeout());
|
||||
}
|
||||
|
||||
public int deleteOldStatuses(Date olderThan) {
|
||||
Query query = em.createNamedQuery("Statuses.deleteOld");
|
||||
query.setParameter("date", olderThan);
|
||||
return query.executeUpdate();
|
||||
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) {
|
||||
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
|
||||
Root<FeedEntryStatus> root = query.from(getType());
|
||||
|
||||
Predicate p1 = builder.lessThan(root.get(FeedEntryStatus_.entryInserted), olderThan);
|
||||
Predicate p2 = builder.isFalse(root.get(FeedEntryStatus_.starred));
|
||||
|
||||
query.where(p1, p2);
|
||||
TypedQuery<FeedEntryStatus> q = em.createQuery(query);
|
||||
q.setMaxResults(limit);
|
||||
return q.getResultList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
43
src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java
Normal file
43
src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import javax.persistence.criteria.Root;
|
||||
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.FeedEntryTag_;
|
||||
import com.commafeed.backend.model.FeedEntry_;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.User_;
|
||||
|
||||
@Stateless
|
||||
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
|
||||
|
||||
public List<String> findByUser(User user) {
|
||||
CriteriaQuery<String> query = builder.createQuery(String.class);
|
||||
Root<FeedEntryTag> root = query.from(getType());
|
||||
query.select(root.get(FeedEntryTag_.name));
|
||||
query.distinct(true);
|
||||
|
||||
Predicate p1 = builder.equal(root.get(FeedEntryTag_.user).get(User_.id), user.getId());
|
||||
query.where(p1);
|
||||
|
||||
return cache(em.createQuery(query)).getResultList();
|
||||
}
|
||||
|
||||
public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) {
|
||||
CriteriaQuery<FeedEntryTag> query = builder.createQuery(getType());
|
||||
Root<FeedEntryTag> root = query.from(getType());
|
||||
|
||||
Predicate p1 = builder.equal(root.get(FeedEntryTag_.user).get(User_.id), user.getId());
|
||||
Predicate p2 = builder.equal(root.get(FeedEntryTag_.entry).get(FeedEntry_.id), entry.getId());
|
||||
|
||||
query.where(p1, p2);
|
||||
|
||||
return cache(em.createQuery(query)).getResultList();
|
||||
}
|
||||
}
|
||||
@@ -63,10 +63,11 @@ public abstract class GenericDAO<T extends AbstractModel> {
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(Collection<? extends AbstractModel> objects) {
|
||||
public int delete(Collection<? extends AbstractModel> objects) {
|
||||
for (AbstractModel object : objects) {
|
||||
delete(object);
|
||||
}
|
||||
return objects.size();
|
||||
}
|
||||
|
||||
public void deleteById(Long id) {
|
||||
|
||||
@@ -50,6 +50,8 @@ public class FeedFetcher {
|
||||
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
|
||||
content = result.getContent();
|
||||
fetchedFeed = parser.parse(feedUrl, content);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
@@ -77,6 +79,7 @@ public class FeedFetcher {
|
||||
feed.setEtagHeader(FeedUtils.truncate(result.geteTag(), 255));
|
||||
feed.setLastContentHash(hash);
|
||||
fetchedFeed.setFetchDuration(result.getDuration());
|
||||
fetchedFeed.setUrlAfterRedirect(result.getUrlAfterRedirect());
|
||||
return fetchedFeed;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ public class FeedParser {
|
||||
if (xmlString == null) {
|
||||
throw new FeedException("Input string is null for url " + feedUrl);
|
||||
}
|
||||
xmlString = FeedUtils.replaceHtmlEntitiesWithNumericEntities(xmlString);
|
||||
InputSource source = new InputSource(new StringReader(xmlString));
|
||||
SyndFeed rss = new SyndFeedInput().build(source);
|
||||
handleForeignMarkup(rss);
|
||||
@@ -66,10 +67,6 @@ public class FeedParser {
|
||||
feed.setLink(rss.getLink());
|
||||
List<SyndEntry> items = rss.getEntries();
|
||||
|
||||
if (items.isEmpty()) {
|
||||
throw new FeedException("No items in the feed.");
|
||||
}
|
||||
|
||||
for (SyndEntry item : items) {
|
||||
FeedEntry entry = new FeedEntry();
|
||||
|
||||
@@ -82,8 +79,13 @@ public class FeedParser {
|
||||
continue;
|
||||
}
|
||||
entry.setGuid(FeedUtils.truncate(guid, 2048));
|
||||
entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink()), 2048));
|
||||
entry.setUpdated(validateDate(getEntryUpdateDate(item), true));
|
||||
entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink(), feed.getUrlAfterRedirect()), 2048));
|
||||
|
||||
// if link is empty but guid is used as url
|
||||
if (StringUtils.isBlank(entry.getUrl()) && StringUtils.startsWith(entry.getGuid(), "http")) {
|
||||
entry.setUrl(entry.getGuid());
|
||||
}
|
||||
|
||||
FeedEntryContent content = new FeedEntryContent();
|
||||
content.setContent(getContent(item));
|
||||
|
||||
@@ -8,11 +8,11 @@ import com.commafeed.backend.model.FeedEntry;
|
||||
public class FeedRefreshContext {
|
||||
private Feed feed;
|
||||
private List<FeedEntry> entries;
|
||||
private boolean isUrgent;
|
||||
private boolean urgent;
|
||||
|
||||
public FeedRefreshContext(Feed feed, boolean isUrgent) {
|
||||
this.feed = feed;
|
||||
this.isUrgent = isUrgent;
|
||||
this.urgent = isUrgent;
|
||||
}
|
||||
|
||||
public Feed getFeed() {
|
||||
@@ -24,11 +24,11 @@ public class FeedRefreshContext {
|
||||
}
|
||||
|
||||
public boolean isUrgent() {
|
||||
return isUrgent;
|
||||
return urgent;
|
||||
}
|
||||
|
||||
public void setUrgent(boolean isUrgent) {
|
||||
this.isUrgent = isUrgent;
|
||||
public void setUrgent(boolean urgent) {
|
||||
this.urgent = urgent;
|
||||
}
|
||||
|
||||
public List<FeedEntry> getEntries() {
|
||||
|
||||
@@ -7,6 +7,9 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
|
||||
/**
|
||||
* Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using
|
||||
* {@link Task} instead of {@link Runnable}
|
||||
@@ -19,7 +22,7 @@ public class FeedRefreshExecutor {
|
||||
private ThreadPoolExecutor pool;
|
||||
private LinkedBlockingDeque<Runnable> queue;
|
||||
|
||||
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity) {
|
||||
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity, MetricRegistry metrics) {
|
||||
log.info("Creating pool {} with {} threads", poolName, threads);
|
||||
this.poolName = poolName;
|
||||
pool = new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
|
||||
@@ -51,20 +54,26 @@ public class FeedRefreshExecutor {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
metrics.register(MetricRegistry.name(getClass(), poolName, "active"), new Gauge<Integer>() {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return pool.getActiveCount();
|
||||
}
|
||||
});
|
||||
|
||||
metrics.register(MetricRegistry.name(getClass(), poolName, "pending"), new Gauge<Integer>() {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return queue.size();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void execute(Task task) {
|
||||
pool.execute(task);
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return queue.size();
|
||||
}
|
||||
|
||||
public int getActiveCount() {
|
||||
return pool.getActiveCount();
|
||||
}
|
||||
|
||||
public static interface Task extends Runnable {
|
||||
boolean isUrgent();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
|
||||
import com.commafeed.backend.MetricsBean;
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
@@ -41,7 +43,7 @@ public class FeedRefreshTaskGiver {
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
MetricsBean metricsBean;
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
FeedRefreshWorker worker;
|
||||
@@ -54,10 +56,35 @@ public class FeedRefreshTaskGiver {
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
private Meter feedRefreshed;
|
||||
private Meter threadWaited;
|
||||
private Meter refill;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
backgroundThreads = applicationSettingsService.get().getBackgroundThreads();
|
||||
executor = Executors.newFixedThreadPool(1);
|
||||
feedRefreshed = metrics.meter(MetricRegistry.name(getClass(), "feedRefreshed"));
|
||||
threadWaited = metrics.meter(MetricRegistry.name(getClass(), "threadWaited"));
|
||||
refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
|
||||
metrics.register(MetricRegistry.name(getClass(), "addQueue"), new Gauge<Integer>() {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return addQueue.size();
|
||||
}
|
||||
});
|
||||
metrics.register(MetricRegistry.name(getClass(), "takeQueue"), new Gauge<Integer>() {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return takeQueue.size();
|
||||
}
|
||||
});
|
||||
metrics.register(MetricRegistry.name(getClass(), "giveBackQueue"), new Gauge<Integer>() {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return giveBackQueue.size();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -73,26 +100,25 @@ public class FeedRefreshTaskGiver {
|
||||
}
|
||||
|
||||
public void start() {
|
||||
try {
|
||||
// sleeping for a little while, let everything settle
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("interrupted while sleeping");
|
||||
}
|
||||
log.info("starting feed refresh task giver");
|
||||
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// sleeping for a little while, let everything settle
|
||||
Thread.sleep(60000);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("interrupted while sleeping");
|
||||
}
|
||||
while (!executor.isShutdown()) {
|
||||
try {
|
||||
FeedRefreshContext context = take();
|
||||
if (context != null) {
|
||||
metricsBean.feedRefreshed();
|
||||
feedRefreshed.mark();
|
||||
worker.updateFeed(context);
|
||||
} else {
|
||||
log.debug("nothing to do, sleeping for 15s");
|
||||
metricsBean.threadWaited();
|
||||
threadWaited.mark();
|
||||
try {
|
||||
Thread.sleep(15000);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -138,22 +164,26 @@ public class FeedRefreshTaskGiver {
|
||||
* refills the refresh queue and empties the giveBack queue while at it
|
||||
*/
|
||||
private void refill() {
|
||||
int count = Math.min(100, 3 * backgroundThreads);
|
||||
refill.mark();
|
||||
|
||||
// first, get feeds that are up to refresh from the database
|
||||
List<FeedRefreshContext> contexts = Lists.newArrayList();
|
||||
if (!applicationSettingsService.get().isCrawlingPaused()) {
|
||||
List<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold());
|
||||
for (Feed feed : feeds) {
|
||||
contexts.add(new FeedRefreshContext(feed, false));
|
||||
}
|
||||
int batchSize = Math.min(100, 3 * backgroundThreads);
|
||||
|
||||
// add feeds we got from the add() method
|
||||
int addQueueSize = addQueue.size();
|
||||
for (int i = 0; i < Math.min(batchSize, addQueueSize); i++) {
|
||||
contexts.add(addQueue.poll());
|
||||
}
|
||||
|
||||
// then, add to those the feeds we got from the add() method. We add them at the beginning of the list as they probably have a
|
||||
// higher priority
|
||||
int size = addQueue.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
contexts.add(0, addQueue.poll());
|
||||
// add feeds that are up to refresh from the database
|
||||
if (!applicationSettingsService.get().isCrawlingPaused()) {
|
||||
int count = batchSize - contexts.size();
|
||||
if (count > 0) {
|
||||
List<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold());
|
||||
for (Feed feed : feeds) {
|
||||
contexts.add(new FeedRefreshContext(feed, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set the disabledDate to now as we use the disabledDate in feedDAO to decide what to refresh next. We also use a map to remove
|
||||
@@ -169,8 +199,8 @@ public class FeedRefreshTaskGiver {
|
||||
takeQueue.addAll(map.values());
|
||||
|
||||
// add feeds from the giveBack queue to the map, overriding duplicates
|
||||
size = giveBackQueue.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
int giveBackQueueSize = giveBackQueue.size();
|
||||
for (int i = 0; i < giveBackQueueSize; i++) {
|
||||
Feed feed = giveBackQueue.poll();
|
||||
map.put(feed.getId(), new FeedRefreshContext(feed, false));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
|
||||
import com.commafeed.backend.MetricsBean;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
@@ -57,7 +58,7 @@ public class FeedRefreshUpdater {
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
MetricsBean metricsBean;
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
@@ -71,12 +72,22 @@ public class FeedRefreshUpdater {
|
||||
private FeedRefreshExecutor pool;
|
||||
private Striped<Lock> locks;
|
||||
|
||||
private Meter entryCacheMiss;
|
||||
private Meter entryCacheHit;
|
||||
private Meter feedUpdated;
|
||||
private Meter entryInserted;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
ApplicationSettings settings = applicationSettingsService.get();
|
||||
int threads = Math.max(settings.getDatabaseUpdateThreads(), 1);
|
||||
pool = new FeedRefreshExecutor("feed-refresh-updater", threads, Math.min(50 * threads, 1000));
|
||||
pool = new FeedRefreshExecutor("feed-refresh-updater", threads, Math.min(50 * threads, 1000), metrics);
|
||||
locks = Striped.lazyWeakLock(threads * 100000);
|
||||
|
||||
entryCacheMiss = metrics.meter(MetricRegistry.name(getClass(), "entryCacheMiss"));
|
||||
entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit"));
|
||||
feedUpdated = metrics.meter(MetricRegistry.name(getClass(), "feedUpdated"));
|
||||
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -116,10 +127,10 @@ public class FeedRefreshUpdater {
|
||||
subscriptions = feedSubscriptionDAO.findByFeed(feed);
|
||||
}
|
||||
ok &= addEntry(feed, entry, subscriptions);
|
||||
metricsBean.entryCacheMiss();
|
||||
entryCacheMiss.mark();
|
||||
} else {
|
||||
log.debug("cache hit for {}", entry.getUrl());
|
||||
metricsBean.entryCacheHit();
|
||||
entryCacheHit.mark();
|
||||
}
|
||||
|
||||
currentEntries.add(cacheKey);
|
||||
@@ -147,7 +158,7 @@ public class FeedRefreshUpdater {
|
||||
// requeue asap
|
||||
feed.setDisabledUntil(new Date(0));
|
||||
}
|
||||
metricsBean.feedUpdated();
|
||||
feedUpdated.mark();
|
||||
taskGiver.giveBack(feed);
|
||||
}
|
||||
|
||||
@@ -180,7 +191,7 @@ public class FeedRefreshUpdater {
|
||||
if (locked1 && locked2) {
|
||||
boolean inserted = feedUpdateService.addEntry(feed, entry);
|
||||
if (inserted) {
|
||||
metricsBean.entryInserted();
|
||||
entryInserted.mark();
|
||||
}
|
||||
success = true;
|
||||
} else {
|
||||
@@ -213,13 +224,4 @@ public class FeedRefreshUpdater {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return pool.getQueueSize();
|
||||
}
|
||||
|
||||
public int getActiveCount() {
|
||||
return pool.getActiveCount();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||
import com.commafeed.backend.feeds.FeedRefreshExecutor.Task;
|
||||
import com.commafeed.backend.model.ApplicationSettings;
|
||||
@@ -37,6 +40,9 @@ public class FeedRefreshWorker {
|
||||
@Inject
|
||||
FeedRefreshTaskGiver taskGiver;
|
||||
|
||||
@Inject
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@@ -46,7 +52,7 @@ public class FeedRefreshWorker {
|
||||
private void init() {
|
||||
ApplicationSettings settings = applicationSettingsService.get();
|
||||
int threads = settings.getBackgroundThreads();
|
||||
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000));
|
||||
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000), metrics);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -58,14 +64,6 @@ public class FeedRefreshWorker {
|
||||
pool.execute(new FeedTask(context));
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return pool.getQueueSize();
|
||||
}
|
||||
|
||||
public int getActiveCount() {
|
||||
return pool.getActiveCount();
|
||||
}
|
||||
|
||||
private class FeedTask implements Task {
|
||||
|
||||
private FeedRefreshContext context;
|
||||
@@ -90,17 +88,21 @@ public class FeedRefreshWorker {
|
||||
int refreshInterval = applicationSettingsService.get().getRefreshIntervalMinutes();
|
||||
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
|
||||
try {
|
||||
FetchedFeed fetchedFeed = fetcher.fetch(feed.getUrl(), false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
||||
String url = ObjectUtils.firstNonNull(feed.getUrlAfterRedirect(), feed.getUrl());
|
||||
FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
||||
feed.getLastPublishedDate(), feed.getLastContentHash());
|
||||
// stops here if NotModifiedException or any other exception is
|
||||
// thrown
|
||||
// stops here if NotModifiedException or any other exception is thrown
|
||||
List<FeedEntry> entries = fetchedFeed.getEntries();
|
||||
|
||||
if (applicationSettingsService.get().isHeavyLoad()) {
|
||||
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
|
||||
.getAverageEntryInterval(), disabledUntil);
|
||||
}
|
||||
|
||||
String urlAfterRedirect = fetchedFeed.getUrlAfterRedirect();
|
||||
if (StringUtils.equals(url, urlAfterRedirect)) {
|
||||
urlAfterRedirect = null;
|
||||
}
|
||||
feed.setUrlAfterRedirect(urlAfterRedirect);
|
||||
feed.setLink(fetchedFeed.getFeed().getLink());
|
||||
feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader());
|
||||
feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader());
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.commafeed.backend.feeds;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
@@ -15,6 +17,7 @@ import org.apache.commons.lang.ArrayUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.apache.commons.math.stat.descriptive.SummaryStatistics;
|
||||
import org.apache.wicket.request.UrlUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Document.OutputSettings;
|
||||
@@ -50,6 +53,8 @@ public class FeedUtils {
|
||||
private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
|
||||
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
|
||||
|
||||
private static final Whitelist WHITELIST = buildWhiteList();
|
||||
|
||||
public static String truncate(String string, int length) {
|
||||
if (string != null) {
|
||||
string = string.substring(0, Math.min(length, string.length()));
|
||||
@@ -57,6 +62,39 @@ public class FeedUtils {
|
||||
return string;
|
||||
}
|
||||
|
||||
private static synchronized Whitelist buildWhiteList() {
|
||||
Whitelist whitelist = new Whitelist();
|
||||
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1",
|
||||
"h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", "sup",
|
||||
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
|
||||
|
||||
whitelist.addAttributes("div", "dir");
|
||||
whitelist.addAttributes("pre", "dir");
|
||||
whitelist.addAttributes("code", "dir");
|
||||
whitelist.addAttributes("table", "dir");
|
||||
whitelist.addAttributes("p", "dir");
|
||||
whitelist.addAttributes("a", "href", "title");
|
||||
whitelist.addAttributes("blockquote", "cite");
|
||||
whitelist.addAttributes("col", "span", "width");
|
||||
whitelist.addAttributes("colgroup", "span", "width");
|
||||
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
|
||||
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
|
||||
whitelist.addAttributes("ol", "start", "type");
|
||||
whitelist.addAttributes("q", "cite");
|
||||
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
|
||||
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
|
||||
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
|
||||
whitelist.addAttributes("ul", "type");
|
||||
|
||||
whitelist.addProtocols("a", "href", "ftp", "http", "https", "mailto");
|
||||
whitelist.addProtocols("blockquote", "cite", "http", "https");
|
||||
whitelist.addProtocols("img", "src", "http", "https");
|
||||
whitelist.addProtocols("q", "cite", "http", "https");
|
||||
|
||||
whitelist.addEnforcedAttribute("a", "target", "_blank");
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
|
||||
* feed
|
||||
@@ -91,6 +129,14 @@ public class FeedUtils {
|
||||
}
|
||||
return encoding;
|
||||
}
|
||||
|
||||
public static String replaceHtmlEntitiesWithNumericEntities(String source){
|
||||
String result = source;
|
||||
for(String entity : HtmlEntities.NUMERIC_MAPPING.keySet()){
|
||||
result = result.replace(entity, HtmlEntities.NUMERIC_MAPPING.get(entity));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to identify a feed and avoid duplicates
|
||||
@@ -152,38 +198,9 @@ public class FeedUtils {
|
||||
public static String handleContent(String content, String baseUri, boolean keepTextOnly) {
|
||||
if (StringUtils.isNotBlank(content)) {
|
||||
baseUri = StringUtils.trimToEmpty(baseUri);
|
||||
Whitelist whitelist = new Whitelist();
|
||||
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em",
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong",
|
||||
"sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
|
||||
|
||||
whitelist.addAttributes("div", "dir");
|
||||
whitelist.addAttributes("pre", "dir");
|
||||
whitelist.addAttributes("code", "dir");
|
||||
whitelist.addAttributes("table", "dir");
|
||||
whitelist.addAttributes("p", "dir");
|
||||
whitelist.addAttributes("a", "href", "title");
|
||||
whitelist.addAttributes("blockquote", "cite");
|
||||
whitelist.addAttributes("col", "span", "width");
|
||||
whitelist.addAttributes("colgroup", "span", "width");
|
||||
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
|
||||
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
|
||||
whitelist.addAttributes("ol", "start", "type");
|
||||
whitelist.addAttributes("q", "cite");
|
||||
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
|
||||
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
|
||||
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
|
||||
whitelist.addAttributes("ul", "type");
|
||||
|
||||
whitelist.addProtocols("a", "href", "ftp", "http", "https", "mailto");
|
||||
whitelist.addProtocols("blockquote", "cite", "http", "https");
|
||||
whitelist.addProtocols("img", "src", "http", "https");
|
||||
whitelist.addProtocols("q", "cite", "http", "https");
|
||||
|
||||
whitelist.addEnforcedAttribute("a", "target", "_blank");
|
||||
|
||||
Document dirty = Jsoup.parseBodyFragment(content, baseUri);
|
||||
Cleaner cleaner = new Cleaner(whitelist);
|
||||
Cleaner cleaner = new Cleaner(WHITELIST);
|
||||
Document clean = cleaner.clean(dirty);
|
||||
|
||||
for (Element e : clean.select("iframe[style]")) {
|
||||
@@ -389,17 +406,37 @@ public class FeedUtils {
|
||||
return url;
|
||||
}
|
||||
|
||||
public static String toAbsoluteUrl(String url, String baseUrl) {
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
* the url of the entry
|
||||
* @param feedLink
|
||||
* the url of the feed as described in the feed
|
||||
* @param feedUrl
|
||||
* the url of the feed that we used to fetch the feed
|
||||
* @return an absolute url pointing to the entry
|
||||
*/
|
||||
public static String toAbsoluteUrl(String url, String feedLink, String feedUrl) {
|
||||
url = StringUtils.trimToNull(StringUtils.normalizeSpace(url));
|
||||
if (baseUrl == null || url == null || url.startsWith("http")) {
|
||||
if (url == null || url.startsWith("http")) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (url.startsWith("/") == false) {
|
||||
url = "/" + url;
|
||||
String baseUrl = (feedLink == null || UrlUtils.isRelative(feedLink)) ? feedUrl : feedLink;
|
||||
|
||||
if (baseUrl == null) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return baseUrl + url;
|
||||
String result = null;
|
||||
try {
|
||||
result = new URL(new URL(baseUrl), url).toString();
|
||||
} catch (MalformedURLException e) {
|
||||
log.debug("could not parse url : " + e.getMessage(), e);
|
||||
result = url;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) {
|
||||
|
||||
@@ -12,6 +12,7 @@ public class FetchedFeed {
|
||||
private List<FeedEntry> entries = Lists.newArrayList();
|
||||
|
||||
private String title;
|
||||
private String urlAfterRedirect;
|
||||
private long fetchDuration;
|
||||
|
||||
public Feed getFeed() {
|
||||
@@ -45,4 +46,13 @@ public class FetchedFeed {
|
||||
public void setFetchDuration(long fetchDuration) {
|
||||
this.fetchDuration = fetchDuration;
|
||||
}
|
||||
|
||||
public String getUrlAfterRedirect() {
|
||||
return urlAfterRedirect;
|
||||
}
|
||||
|
||||
public void setUrlAfterRedirect(String urlAfterRedirect) {
|
||||
this.urlAfterRedirect = urlAfterRedirect;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
266
src/main/java/com/commafeed/backend/feeds/HtmlEntities.java
Normal file
266
src/main/java/com/commafeed/backend/feeds/HtmlEntities.java
Normal file
@@ -0,0 +1,266 @@
|
||||
package com.commafeed.backend.feeds;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gwt.thirdparty.guava.common.collect.Maps;
|
||||
|
||||
public class HtmlEntities {
|
||||
public static final Map<String, String> NUMERIC_MAPPING = Collections.unmodifiableMap(loadMap());
|
||||
|
||||
private static synchronized Map<String, String> loadMap() {
|
||||
Map<String, String> map = Maps.newLinkedHashMap();
|
||||
map.put("Á", "Á");
|
||||
map.put("á", "á");
|
||||
map.put("Â", "Â");
|
||||
map.put("â", "â");
|
||||
map.put("´", "´");
|
||||
map.put("Æ", "Æ");
|
||||
map.put("æ", "æ");
|
||||
map.put("À", "À");
|
||||
map.put("à", "à");
|
||||
map.put("ℵ", "ℵ");
|
||||
map.put("Α", "Α");
|
||||
map.put("α", "α");
|
||||
map.put("&", "&");
|
||||
map.put("∧", "∧");
|
||||
map.put("∠", "∠");
|
||||
map.put("Å", "Å");
|
||||
map.put("å", "å");
|
||||
map.put("≈", "≈");
|
||||
map.put("Ã", "Ã");
|
||||
map.put("ã", "ã");
|
||||
map.put("Ä", "Ä");
|
||||
map.put("ä", "ä");
|
||||
map.put("„", "„");
|
||||
map.put("Β", "Β");
|
||||
map.put("β", "β");
|
||||
map.put("¦", "¦");
|
||||
map.put("•", "•");
|
||||
map.put("∩", "∩");
|
||||
map.put("Ç", "Ç");
|
||||
map.put("ç", "ç");
|
||||
map.put("¸", "¸");
|
||||
map.put("¢", "¢");
|
||||
map.put("Χ", "Χ");
|
||||
map.put("χ", "χ");
|
||||
map.put("ˆ", "ˆ");
|
||||
map.put("♣", "♣");
|
||||
map.put("≅", "≅");
|
||||
map.put("©", "©");
|
||||
map.put("↵", "↵");
|
||||
map.put("∪", "∪");
|
||||
map.put("¤", "¤");
|
||||
map.put("†", "†");
|
||||
map.put("‡", "‡");
|
||||
map.put("↓", "↓");
|
||||
map.put("⇓", "⇓");
|
||||
map.put("°", "°");
|
||||
map.put("Δ", "Δ");
|
||||
map.put("δ", "δ");
|
||||
map.put("♦", "♦");
|
||||
map.put("÷", "÷");
|
||||
map.put("É", "É");
|
||||
map.put("é", "é");
|
||||
map.put("Ê", "Ê");
|
||||
map.put("ê", "ê");
|
||||
map.put("È", "È");
|
||||
map.put("è", "è");
|
||||
map.put("∅", "∅");
|
||||
map.put(" ", " ");
|
||||
map.put(" ", " ");
|
||||
map.put("Ε", "Ε");
|
||||
map.put("ε", "ε");
|
||||
map.put("≡", "≡");
|
||||
map.put("Η", "Η");
|
||||
map.put("η", "η");
|
||||
map.put("Ð", "Ð");
|
||||
map.put("ð", "ð");
|
||||
map.put("Ë", "Ë");
|
||||
map.put("ë", "ë");
|
||||
map.put("€", "€");
|
||||
map.put("∃", "∃");
|
||||
map.put("ƒ", "ƒ");
|
||||
map.put("∀", "∀");
|
||||
map.put("½", "½");
|
||||
map.put("¼", "¼");
|
||||
map.put("¾", "¾");
|
||||
map.put("⁄", "⁄");
|
||||
map.put("Γ", "Γ");
|
||||
map.put("γ", "γ");
|
||||
map.put("≥", "≥");
|
||||
map.put("↔", "↔");
|
||||
map.put("⇔", "⇔");
|
||||
map.put("♥", "♥");
|
||||
map.put("…", "…");
|
||||
map.put("Í", "Í");
|
||||
map.put("í", "í");
|
||||
map.put("Î", "Î");
|
||||
map.put("î", "î");
|
||||
map.put("¡", "¡");
|
||||
map.put("Ì", "Ì");
|
||||
map.put("ì", "ì");
|
||||
map.put("ℑ", "ℑ");
|
||||
map.put("∞", "∞");
|
||||
map.put("∫", "∫");
|
||||
map.put("Ι", "Ι");
|
||||
map.put("ι", "ι");
|
||||
map.put("¿", "¿");
|
||||
map.put("∈", "∈");
|
||||
map.put("Ï", "Ï");
|
||||
map.put("ï", "ï");
|
||||
map.put("Κ", "Κ");
|
||||
map.put("κ", "κ");
|
||||
map.put("Λ", "Λ");
|
||||
map.put("λ", "λ");
|
||||
map.put("⟨", "〈");
|
||||
map.put("«", "«");
|
||||
map.put("←", "←");
|
||||
map.put("⇐", "⇐");
|
||||
map.put("⌈", "⌈");
|
||||
map.put("“", "“");
|
||||
map.put("≤", "≤");
|
||||
map.put("⌊", "⌊");
|
||||
map.put("∗", "∗");
|
||||
map.put("◊", "◊");
|
||||
map.put("‎", "‎");
|
||||
map.put("‹", "‹");
|
||||
map.put("‘", "‘");
|
||||
map.put("¯", "¯");
|
||||
map.put("—", "—");
|
||||
map.put("µ", "µ");
|
||||
map.put("·", "·");
|
||||
map.put("−", "−");
|
||||
map.put("Μ", "Μ");
|
||||
map.put("μ", "μ");
|
||||
map.put("∇", "∇");
|
||||
map.put(" ", " ");
|
||||
map.put("–", "–");
|
||||
map.put("≠", "≠");
|
||||
map.put("∋", "∋");
|
||||
map.put("¬", "¬");
|
||||
map.put("∉", "∉");
|
||||
map.put("⊄", "⊄");
|
||||
map.put("Ñ", "Ñ");
|
||||
map.put("ñ", "ñ");
|
||||
map.put("Ν", "Ν");
|
||||
map.put("ν", "ν");
|
||||
map.put("Ó", "Ó");
|
||||
map.put("ó", "ó");
|
||||
map.put("Ô", "Ô");
|
||||
map.put("ô", "ô");
|
||||
map.put("Œ", "Œ");
|
||||
map.put("œ", "œ");
|
||||
map.put("Ò", "Ò");
|
||||
map.put("ò", "ò");
|
||||
map.put("‾", "‾");
|
||||
map.put("Ω", "Ω");
|
||||
map.put("ω", "ω");
|
||||
map.put("Ο", "Ο");
|
||||
map.put("ο", "ο");
|
||||
map.put("⊕", "⊕");
|
||||
map.put("∨", "∨");
|
||||
map.put("ª", "ª");
|
||||
map.put("º", "º");
|
||||
map.put("Ø", "Ø");
|
||||
map.put("ø", "ø");
|
||||
map.put("Õ", "Õ");
|
||||
map.put("õ", "õ");
|
||||
map.put("⊗", "⊗");
|
||||
map.put("Ö", "Ö");
|
||||
map.put("ö", "ö");
|
||||
map.put("¶", "¶");
|
||||
map.put("∂", "∂");
|
||||
map.put("‰", "‰");
|
||||
map.put("⊥", "⊥");
|
||||
map.put("Φ", "Φ");
|
||||
map.put("φ", "φ");
|
||||
map.put("Π", "Π");
|
||||
map.put("π", "π");
|
||||
map.put("ϖ", "ϖ");
|
||||
map.put("±", "±");
|
||||
map.put("£", "£");
|
||||
map.put("′", "′");
|
||||
map.put("″", "″");
|
||||
map.put("∏", "∏");
|
||||
map.put("∝", "∝");
|
||||
map.put("Ψ", "Ψ");
|
||||
map.put("ψ", "ψ");
|
||||
map.put(""", """);
|
||||
map.put("√", "√");
|
||||
map.put("⟩", "〉");
|
||||
map.put("»", "»");
|
||||
map.put("→", "→");
|
||||
map.put("⇒", "⇒");
|
||||
map.put("⌉", "⌉");
|
||||
map.put("”", "”");
|
||||
map.put("ℜ", "ℜ");
|
||||
map.put("®", "®");
|
||||
map.put("⌋", "⌋");
|
||||
map.put("Ρ", "Ρ");
|
||||
map.put("ρ", "ρ");
|
||||
map.put("‏", "‏");
|
||||
map.put("›", "›");
|
||||
map.put("’", "’");
|
||||
map.put("‚", "‚");
|
||||
map.put("Š", "Š");
|
||||
map.put("š", "š");
|
||||
map.put("⋅", "⋅");
|
||||
map.put("§", "§");
|
||||
map.put("­", "­");
|
||||
map.put("Σ", "Σ");
|
||||
map.put("σ", "σ");
|
||||
map.put("ς", "ς");
|
||||
map.put("∼", "∼");
|
||||
map.put("♠", "♠");
|
||||
map.put("⊂", "⊂");
|
||||
map.put("⊆", "⊆");
|
||||
map.put("∑", "∑");
|
||||
map.put("¹", "¹");
|
||||
map.put("²", "²");
|
||||
map.put("³", "³");
|
||||
map.put("⊃", "⊃");
|
||||
map.put("⊇", "⊇");
|
||||
map.put("ß", "ß");
|
||||
map.put("Τ", "Τ");
|
||||
map.put("τ", "τ");
|
||||
map.put("∴", "∴");
|
||||
map.put("Θ", "Θ");
|
||||
map.put("θ", "θ");
|
||||
map.put("ϑ", "ϑ");
|
||||
map.put(" ", " ");
|
||||
map.put("Þ", "Þ");
|
||||
map.put("þ", "þ");
|
||||
map.put("˜", "˜");
|
||||
map.put("×", "×");
|
||||
map.put("™", "™");
|
||||
map.put("Ú", "Ú");
|
||||
map.put("ú", "ú");
|
||||
map.put("↑", "↑");
|
||||
map.put("⇑", "⇑");
|
||||
map.put("Û", "Û");
|
||||
map.put("û", "û");
|
||||
map.put("Ù", "Ù");
|
||||
map.put("ù", "ù");
|
||||
map.put("¨", "¨");
|
||||
map.put("ϒ", "ϒ");
|
||||
map.put("Υ", "Υ");
|
||||
map.put("υ", "υ");
|
||||
map.put("Ü", "Ü");
|
||||
map.put("ü", "ü");
|
||||
map.put("℘", "℘");
|
||||
map.put("Ξ", "Ξ");
|
||||
map.put("ξ", "ξ");
|
||||
map.put("Ý", "Ý");
|
||||
map.put("ý", "ý");
|
||||
map.put("¥", "¥");
|
||||
map.put("ÿ", "ÿ");
|
||||
map.put("Ÿ", "Ÿ");
|
||||
map.put("Ζ", "Ζ");
|
||||
map.put("ζ", "ζ");
|
||||
map.put("‍", "‍");
|
||||
map.put("‌", "‌");
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.commafeed.backend.metrics;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.enterprise.inject.Produces;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import com.codahale.metrics.JmxReporter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class MetricRegistryProducer {
|
||||
|
||||
private MetricRegistry registry;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
log.info("initializing metrics registry");
|
||||
registry = new MetricRegistry();
|
||||
JmxReporter.forRegistry(registry).build().start();
|
||||
log.info("metrics registry initialized");
|
||||
}
|
||||
|
||||
@Produces
|
||||
public MetricRegistry produceMetricsRegistry() {
|
||||
return registry;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,12 @@ public class Feed extends AbstractModel {
|
||||
@Column(length = 2048, nullable = false)
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* cache the url after potential http 30x redirects
|
||||
*/
|
||||
@Column(name = "url_after_redirect", length = 2048, nullable = false)
|
||||
private String urlAfterRedirect;
|
||||
|
||||
@Column(length = 2048, nullable = false)
|
||||
private String normalizedUrl;
|
||||
|
||||
@@ -130,11 +136,4 @@ public class Feed extends AbstractModel {
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date pushLastPing;
|
||||
|
||||
public Feed() {
|
||||
|
||||
}
|
||||
|
||||
public Feed(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,5 +55,8 @@ public class FeedEntry extends AbstractModel {
|
||||
|
||||
@OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE)
|
||||
private Set<FeedEntryStatus> statuses;
|
||||
|
||||
@OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE)
|
||||
private Set<FeedEntryTag> tags;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.commafeed.backend.model;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.persistence.Cacheable;
|
||||
import javax.persistence.Column;
|
||||
@@ -8,6 +9,8 @@ import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.NamedQueries;
|
||||
import javax.persistence.NamedQuery;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
@@ -19,6 +22,8 @@ import lombok.Setter;
|
||||
import org.hibernate.annotations.Cache;
|
||||
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
@Entity
|
||||
@Table(name = "FEEDENTRYSTATUSES")
|
||||
@SuppressWarnings("serial")
|
||||
@@ -26,6 +31,7 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
|
||||
@Getter
|
||||
@Setter
|
||||
@NamedQueries(@NamedQuery(name="Statuses.deleteOld", query="delete from FeedEntryStatus s where s.entryInserted < :date and s.starred = false"))
|
||||
public class FeedEntryStatus extends AbstractModel {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@@ -42,6 +48,9 @@ public class FeedEntryStatus extends AbstractModel {
|
||||
|
||||
@Transient
|
||||
private boolean markable;
|
||||
|
||||
@Transient
|
||||
private List<FeedEntryTag> tags = Lists.newArrayList();
|
||||
|
||||
/**
|
||||
* Denormalization starts here
|
||||
|
||||
46
src/main/java/com/commafeed/backend/model/FeedEntryTag.java
Normal file
46
src/main/java/com/commafeed/backend/model/FeedEntryTag.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.commafeed.backend.model;
|
||||
|
||||
import javax.persistence.Cacheable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import org.hibernate.annotations.Cache;
|
||||
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||
|
||||
@Entity
|
||||
@Table(name = "FEEDENTRYTAGS")
|
||||
@SuppressWarnings("serial")
|
||||
@Cacheable
|
||||
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
|
||||
@Getter
|
||||
@Setter
|
||||
public class FeedEntryTag extends AbstractModel {
|
||||
|
||||
@JoinColumn(name = "user_id")
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
private User user;
|
||||
|
||||
@JoinColumn(name = "entry_id")
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
private FeedEntry entry;
|
||||
|
||||
@Column(name = "name", length = 40)
|
||||
private String name;
|
||||
|
||||
public FeedEntryTag() {
|
||||
}
|
||||
|
||||
public FeedEntryTag(User user, FeedEntry entry, String name) {
|
||||
this.name = name;
|
||||
this.entry = entry;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class User extends AbstractModel {
|
||||
|
||||
@Column(length = 32, nullable = false, unique = true)
|
||||
private String name;
|
||||
|
||||
|
||||
@Column(length = 255, unique = true)
|
||||
private String email;
|
||||
|
||||
@@ -66,4 +66,8 @@ public class User extends AbstractModel {
|
||||
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
|
||||
private Set<FeedSubscription> subscriptions;
|
||||
|
||||
@Column(name = "last_full_refresh")
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date lastFullRefresh;
|
||||
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ public class UserSettings extends AbstractModel {
|
||||
|
||||
private boolean showRead;
|
||||
private boolean scrollMarks;
|
||||
private boolean socialButtons;
|
||||
|
||||
@Column(length = 32)
|
||||
private String theme;
|
||||
@@ -68,4 +67,18 @@ public class UserSettings extends AbstractModel {
|
||||
@Column(length = Integer.MAX_VALUE)
|
||||
private String customCss;
|
||||
|
||||
@Column(name = "scroll_speed")
|
||||
private int scrollSpeed;
|
||||
|
||||
private boolean email;
|
||||
private boolean gmail;
|
||||
private boolean facebook;
|
||||
private boolean twitter;
|
||||
private boolean googleplus;
|
||||
private boolean tumblr;
|
||||
private boolean pocket;
|
||||
private boolean instapaper;
|
||||
private boolean buffer;
|
||||
private boolean readability;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend.feeds;
|
||||
package com.commafeed.backend.opml;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -34,9 +34,14 @@ public class OPMLExporter {
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
|
||||
|
||||
// export root categories
|
||||
for (FeedCategory cat : categories) {
|
||||
opml.getOutlines().add(buildCategoryOutline(cat, subscriptions));
|
||||
if (cat.getParent() == null) {
|
||||
opml.getOutlines().add(buildCategoryOutline(cat, subscriptions));
|
||||
}
|
||||
}
|
||||
|
||||
// export root subscriptions
|
||||
for (FeedSubscription sub : subscriptions) {
|
||||
if (sub.getCategory() == null) {
|
||||
opml.getOutlines().add(buildSubscriptionOutline(sub));
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend.feeds;
|
||||
package com.commafeed.backend.opml;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
@@ -11,10 +11,12 @@ import javax.inject.Inject;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.feeds.FeedUtils;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.services.FeedSubscriptionService;
|
||||
@@ -56,8 +58,8 @@ public class OPMLImporter {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleOutline(User user, Outline outline, FeedCategory parent) {
|
||||
|
||||
if (StringUtils.isEmpty(outline.getType())) {
|
||||
List<Outline> children = outline.getChildren();
|
||||
if (CollectionUtils.isNotEmpty(children)) {
|
||||
String name = FeedUtils.truncate(outline.getText(), 128);
|
||||
if (name == null) {
|
||||
name = FeedUtils.truncate(outline.getTitle(), 128);
|
||||
@@ -75,7 +77,6 @@ public class OPMLImporter {
|
||||
feedCategoryDAO.saveOrUpdate(category);
|
||||
}
|
||||
|
||||
List<Outline> children = outline.getChildren();
|
||||
for (Outline child : children) {
|
||||
handleOutline(user, child, category);
|
||||
}
|
||||
@@ -87,7 +88,7 @@ public class OPMLImporter {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
name = "Unnamed subscription";
|
||||
}
|
||||
// make sure we continue with the import process even a feed failed
|
||||
// make sure we continue with the import process even if a feed failed
|
||||
try {
|
||||
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent);
|
||||
} catch (FeedSubscriptionException e) {
|
||||
@@ -9,13 +9,14 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.wicket.util.io.IOUtils;
|
||||
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
|
||||
@@ -67,10 +68,11 @@ public class SubscriptionHandler {
|
||||
post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed");
|
||||
post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
|
||||
|
||||
HttpClient client = HttpGetter.newClient(20000);
|
||||
CloseableHttpClient client = HttpGetter.newClient(20000);
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
post.setEntity(new UrlEncodedFormEntity(nvp));
|
||||
HttpResponse response = client.execute(post);
|
||||
response = client.execute(post);
|
||||
|
||||
int code = response.getStatusLine().getStatusCode();
|
||||
if (code != 204 && code != 202 && code != 200) {
|
||||
@@ -90,7 +92,8 @@ public class SubscriptionHandler {
|
||||
} catch (Exception e) {
|
||||
log.error("Could not subscribe to {} for {} : " + e.getMessage(), hub, topic);
|
||||
} finally {
|
||||
client.getConnectionManager().shutdown();
|
||||
IOUtils.closeQuietly(response);
|
||||
IOUtils.closeQuietly(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package com.commafeed.backend.services;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import javax.ejb.Singleton;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
@@ -15,7 +16,7 @@ import com.commafeed.backend.dao.ApplicationSettingsDAO;
|
||||
import com.commafeed.backend.model.ApplicationSettings;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
@Singleton
|
||||
@ApplicationScoped
|
||||
public class ApplicationSettingsService {
|
||||
|
||||
@Inject
|
||||
@@ -23,19 +24,21 @@ public class ApplicationSettingsService {
|
||||
|
||||
private ApplicationSettings settings;
|
||||
|
||||
public void save(ApplicationSettings settings) {
|
||||
this.settings = settings;
|
||||
applicationSettingsDAO.saveOrUpdate(settings);
|
||||
applyLogLevel();
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
|
||||
}
|
||||
|
||||
public ApplicationSettings get() {
|
||||
if (settings == null) {
|
||||
settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
public void save(ApplicationSettings settings) {
|
||||
applicationSettingsDAO.saveOrUpdate(settings);
|
||||
this.settings = settings;
|
||||
applyLogLevel();
|
||||
}
|
||||
|
||||
public Date getUnreadThreshold() {
|
||||
int keepStatusDays = get().getKeepStatusDays();
|
||||
return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.commafeed.backend;
|
||||
package com.commafeed.backend.services;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -15,15 +16,18 @@ import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
|
||||
/**
|
||||
* Contains utility methods for cleaning the database
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
public class DatabaseCleaner {
|
||||
public class DatabaseCleaningService {
|
||||
|
||||
private static final int BATCH_SIZE = 100;
|
||||
|
||||
@Inject
|
||||
FeedDAO feedDAO;
|
||||
@@ -42,13 +46,28 @@ public class DatabaseCleaner {
|
||||
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
public long cleanEntriesWithoutSubscriptions() {
|
||||
log.info("cleaning entries without subscriptions");
|
||||
long total = 0;
|
||||
int deleted = 0;
|
||||
do {
|
||||
List<FeedEntry> entries = feedEntryDAO.findWithoutSubscriptions(BATCH_SIZE);
|
||||
deleted = feedEntryDAO.delete(entries);
|
||||
total += deleted;
|
||||
log.info("removed {} entries without subscriptions", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} entries without subscriptions deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
public long cleanFeedsWithoutSubscriptions() {
|
||||
|
||||
log.info("cleaning feeds without subscriptions");
|
||||
long total = 0;
|
||||
int deleted = -1;
|
||||
int deleted = 0;
|
||||
do {
|
||||
deleted = feedDAO.deleteWithoutSubscriptions(10);
|
||||
List<Feed> feeds = feedDAO.findWithoutSubscriptions(BATCH_SIZE);
|
||||
deleted = feedDAO.delete(feeds);
|
||||
total += deleted;
|
||||
log.info("removed {} feeds without subscriptions", total);
|
||||
} while (deleted != 0);
|
||||
@@ -57,15 +76,15 @@ public class DatabaseCleaner {
|
||||
}
|
||||
|
||||
public long cleanContentsWithoutEntries() {
|
||||
|
||||
log.info("cleaning contents without entries");
|
||||
long total = 0;
|
||||
int deleted = -1;
|
||||
int deleted = 0;
|
||||
do {
|
||||
deleted = feedEntryContentDAO.deleteWithoutEntries(10);
|
||||
deleted = feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE);
|
||||
total += deleted;
|
||||
log.info("removed {} feeds without subscriptions", total);
|
||||
log.info("removed {} contents without entries", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} feeds without subscriptions deleted", total);
|
||||
log.info("cleanup done: {} contents without entries deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -74,9 +93,9 @@ public class DatabaseCleaner {
|
||||
cal.add(Calendar.MINUTE, -1 * (int) unit.toMinutes(value));
|
||||
|
||||
long total = 0;
|
||||
int deleted = -1;
|
||||
int deleted = 0;
|
||||
do {
|
||||
deleted = feedEntryDAO.delete(cal.getTime(), 100);
|
||||
deleted = feedEntryDAO.delete(cal.getTime(), BATCH_SIZE);
|
||||
total += deleted;
|
||||
log.info("removed {} entries", total);
|
||||
} while (deleted != 0);
|
||||
@@ -99,9 +118,19 @@ public class DatabaseCleaner {
|
||||
feedDAO.saveOrUpdate(into);
|
||||
}
|
||||
|
||||
public void cleanStatusesOlderThan(Date olderThan) {
|
||||
public long cleanStatusesOlderThan(Date olderThan) {
|
||||
log.info("cleaning old read statuses");
|
||||
int deleted = feedEntryStatusDAO.deleteOldStatuses(olderThan);
|
||||
log.info("cleaned {} read statuses", deleted);
|
||||
long total = 0;
|
||||
List<FeedEntryStatus> list = Collections.emptyList();
|
||||
do {
|
||||
list = feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE);
|
||||
if (!list.isEmpty()) {
|
||||
feedEntryStatusDAO.delete(list);
|
||||
total += list.size();
|
||||
log.info("cleaned {} old read statuses", total);
|
||||
}
|
||||
} while (!list.isEmpty());
|
||||
log.info("cleanup done: {} old read statuses deleted", total);
|
||||
return total;
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class FeedEntryService {
|
||||
return;
|
||||
}
|
||||
|
||||
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry);
|
||||
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
|
||||
if (status.isMarkable()) {
|
||||
status.setRead(read);
|
||||
feedEntryStatusDAO.saveOrUpdate(status);
|
||||
@@ -64,14 +64,14 @@ public class FeedEntryService {
|
||||
return;
|
||||
}
|
||||
|
||||
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry);
|
||||
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
|
||||
status.setStarred(starred);
|
||||
feedEntryStatusDAO.saveOrUpdate(status);
|
||||
}
|
||||
|
||||
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) {
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO
|
||||
.findBySubscriptions(subscriptions, true, null, null, -1, -1, null, false, false);
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, -1, -1, null, false,
|
||||
false, null);
|
||||
markList(statuses, olderThan);
|
||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||
cache.invalidateUserRootCategory(user);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.commafeed.backend.services;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryTagDAO;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
@Stateless
|
||||
public class FeedEntryTagService {
|
||||
|
||||
@Inject
|
||||
FeedEntryDAO feedEntryDAO;
|
||||
|
||||
@Inject
|
||||
FeedEntryTagDAO feedEntryTagDAO;
|
||||
|
||||
public void updateTags(User user, Long entryId, List<String> tagNames) {
|
||||
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, entry);
|
||||
Map<String, FeedEntryTag> tagMap = Maps.uniqueIndex(tags, new Function<FeedEntryTag, String>() {
|
||||
@Override
|
||||
public String apply(FeedEntryTag input) {
|
||||
return input.getName();
|
||||
}
|
||||
});
|
||||
|
||||
List<FeedEntryTag> addList = Lists.newArrayList();
|
||||
List<FeedEntryTag> removeList = Lists.newArrayList();
|
||||
|
||||
for (String tagName : tagNames) {
|
||||
FeedEntryTag tag = tagMap.get(tagName);
|
||||
if (tag == null) {
|
||||
addList.add(new FeedEntryTag(user, entry, tagName));
|
||||
}
|
||||
}
|
||||
|
||||
for (FeedEntryTag tag : tags) {
|
||||
if (!tagNames.contains(tag.getName())) {
|
||||
removeList.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
feedEntryTagDAO.saveOrUpdate(addList);
|
||||
feedEntryTagDAO.delete(removeList);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -95,11 +95,19 @@ public class FeedSubscriptionService {
|
||||
}
|
||||
}
|
||||
|
||||
public UnreadCount getUnreadCount(FeedSubscription sub) {
|
||||
public void refreshAll(User user) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
for (FeedSubscription sub : subs) {
|
||||
Feed feed = sub.getFeed();
|
||||
taskGiver.add(feed, true);
|
||||
}
|
||||
}
|
||||
|
||||
public UnreadCount getUnreadCount(User user, FeedSubscription sub) {
|
||||
UnreadCount count = cache.getUnreadCount(sub);
|
||||
if (count == null) {
|
||||
log.debug("unread count cache miss for {}", Models.getId(sub));
|
||||
count = feedEntryStatusDAO.getUnreadCount(sub);
|
||||
count = feedEntryStatusDAO.getUnreadCount(user, sub);
|
||||
cache.setUnreadCount(sub, count);
|
||||
}
|
||||
return count;
|
||||
@@ -109,7 +117,7 @@ public class FeedSubscriptionService {
|
||||
Map<Long, UnreadCount> map = Maps.newHashMap();
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
for (FeedSubscription sub : subs) {
|
||||
map.put(sub.getId(), getUnreadCount(sub));
|
||||
map.put(sub.getId(), getUnreadCount(user, sub));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ public class UserService {
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
FeedSubscriptionService feedSubscriptionService;
|
||||
|
||||
public User login(String name, String password) {
|
||||
if (name == null || password == null) {
|
||||
return null;
|
||||
@@ -52,10 +55,21 @@ public class UserService {
|
||||
if (authenticated) {
|
||||
Date lastLogin = user.getLastLogin();
|
||||
Date now = new Date();
|
||||
|
||||
boolean saveUser = false;
|
||||
// only update lastLogin field every hour in order to not
|
||||
// invalidate the cache everytime someone logs in
|
||||
if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
|
||||
user.setLastLogin(now);
|
||||
saveUser = true;
|
||||
}
|
||||
if (applicationSettingsService.get().isHeavyLoad()
|
||||
&& (user.getLastFullRefresh() == null || user.getLastFullRefresh().before(DateUtils.addMinutes(now, -30)))) {
|
||||
user.setLastFullRefresh(now);
|
||||
saveUser = true;
|
||||
feedSubscriptionService.refreshAll(user);
|
||||
}
|
||||
if (saveUser) {
|
||||
userDAO.saveOrUpdate(user);
|
||||
}
|
||||
return user;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend;
|
||||
package com.commafeed.backend.startup;
|
||||
|
||||
import java.sql.Connection;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend;
|
||||
package com.commafeed.backend.startup;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -10,6 +10,8 @@ import com.commafeed.backend.services.UserService;
|
||||
// extend Component in order to benefit from injection
|
||||
public class CommaFeedSessionServices extends Component {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Inject
|
||||
UserService userService;
|
||||
|
||||
|
||||
@@ -41,4 +41,7 @@ public class Entries implements Serializable {
|
||||
@ApiProperty("list of entries")
|
||||
private List<Entry> entries = Lists.newArrayList();
|
||||
|
||||
@ApiProperty("if true, the unread flag was ignored in the request, all entries are returned regardless of their read status")
|
||||
private boolean ignoredReadStatus;
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.commafeed.frontend.model;
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@@ -10,7 +11,9 @@ import com.commafeed.backend.feeds.FeedUtils;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.sun.syndication.feed.synd.SyndContentImpl;
|
||||
import com.sun.syndication.feed.synd.SyndEntry;
|
||||
import com.sun.syndication.feed.synd.SyndEntryImpl;
|
||||
@@ -43,6 +46,12 @@ public class Entry implements Serializable {
|
||||
entry.setFeedLink(sub.getFeed().getLink());
|
||||
entry.setIconUrl(FeedUtils.getFaviconUrl(sub, publicUrl));
|
||||
|
||||
List<String> tags = Lists.newArrayList();
|
||||
for (FeedEntryTag tag : status.getTags()) {
|
||||
tags.add(tag.getName());
|
||||
}
|
||||
entry.setTags(tags);
|
||||
|
||||
if (content != null) {
|
||||
entry.setRtl(FeedUtils.isRTL(feedEntry));
|
||||
entry.setTitle(content.getTitle());
|
||||
@@ -125,4 +134,7 @@ public class Entry implements Serializable {
|
||||
|
||||
@ApiProperty("wether the entry is still markable (old entry statuses are discarded)")
|
||||
private boolean markable;
|
||||
|
||||
@ApiProperty("tags")
|
||||
private List<String> tags;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,6 @@ public class Settings implements Serializable {
|
||||
@ApiProperty(value = "user wants category and feeds with no unread entries shown", required = true)
|
||||
private boolean showRead;
|
||||
|
||||
@ApiProperty(value = "user wants social buttons (facebook, twitter, ...) shown", required = true)
|
||||
private boolean socialButtons;
|
||||
|
||||
@ApiProperty(value = "In expanded view, scroll through entries mark them as read", required = true)
|
||||
private boolean scrollMarks;
|
||||
|
||||
@@ -38,5 +35,19 @@ public class Settings implements Serializable {
|
||||
|
||||
@ApiProperty(value = "user's custom css for the website")
|
||||
private String customCss;
|
||||
|
||||
@ApiProperty(value = "user's preferred scroll speed when navigating between entries")
|
||||
private int scrollSpeed;
|
||||
|
||||
private boolean email;
|
||||
private boolean gmail;
|
||||
private boolean facebook;
|
||||
private boolean twitter;
|
||||
private boolean googleplus;
|
||||
private boolean tumblr;
|
||||
private boolean pocket;
|
||||
private boolean instapaper;
|
||||
private boolean buffer;
|
||||
private boolean readability;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import com.wordnik.swagger.annotations.ApiClass;
|
||||
import com.wordnik.swagger.annotations.ApiProperty;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@ApiClass("Tag Request")
|
||||
@Data
|
||||
public class TagRequest implements Serializable {
|
||||
|
||||
@ApiProperty(value = "entry id", required = true)
|
||||
private Long entryId;
|
||||
|
||||
@ApiProperty(value = "tags")
|
||||
private List<String> tags;
|
||||
|
||||
}
|
||||
@@ -1,45 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html">
|
||||
<head>
|
||||
<title>CommaFeed</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="app-icon-57.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="app-icon-72.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="app-icon-114.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="app-icon-144.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="128x128" href="app-icon-128.png" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
|
||||
<meta name="application-name" content="CommaFeed" />
|
||||
<meta name="msapplication-navbutton-color" content="#F88A14" />
|
||||
<meta name="msapplication-starturl" content="/" />
|
||||
<meta name="msapplication-square70x70logo" content="metro-icon-70.png"/>
|
||||
<meta name="msapplication-square150x150logo" content="metro-icon-150.png"/>
|
||||
<link rel="fluid-icon" href="app-icon-512.png" title="CommaFeed" />
|
||||
<link rel="logo" type="image/svg" href="app-icon.svg" />
|
||||
<title>CommaFeed</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="app-icon-57.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="app-icon-72.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="app-icon-114.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="app-icon-144.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="128x128" href="app-icon-128.png" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
|
||||
<meta name="application-name" content="CommaFeed" />
|
||||
<meta name="msapplication-navbutton-color" content="#F88A14" />
|
||||
<meta name="msapplication-starturl" content="/" />
|
||||
<meta name="msapplication-square70x70logo" content="metro-icon-70.png" />
|
||||
<meta name="msapplication-square150x150logo" content="metro-icon-150.png" />
|
||||
<link rel="fluid-icon" href="app-icon-512.png" title="CommaFeed" />
|
||||
<link rel="logo" type="image/svg" href="app-icon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<wicket:child />
|
||||
<wicket:container wicket:id="footer-container"/>
|
||||
<wicket:container wicket:id="footer-container" />
|
||||
<wicket:container wicket:id="uservoice">
|
||||
<script>(function(){var uv=document.createElement('script');uv.type='text/javascript';uv.async=true;uv.src='//widget.uservoice.com/XYpTZZteqS4lHvgrTXeA.js';var s=document.getElementsByTagName('script')[0];s.parentNode.insertBefore(uv,s)})()</script>
|
||||
<script>
|
||||
(function() {
|
||||
var uv = document.createElement('script');
|
||||
uv.type = 'text/javascript';
|
||||
uv.async = true;
|
||||
uv.src = '//widget.uservoice.com/XYpTZZteqS4lHvgrTXeA.js';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(uv, s);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
UserVoice = window.UserVoice || [];
|
||||
UserVoice.push(['showTab', 'classic_widget', {
|
||||
mode: 'full',
|
||||
default_mode: 'feedback',
|
||||
primary_color: '#000',
|
||||
link_color: '#007dbf',
|
||||
forum_id: 204509,
|
||||
support_tab_name: 'Contact',
|
||||
feedback_tab_name: 'Feedback',
|
||||
tab_label: 'Feedback',
|
||||
tab_color: '#7e72db',
|
||||
tab_position: 'bottom-right',
|
||||
tab_inverted: false
|
||||
mode : 'full',
|
||||
default_mode : 'feedback',
|
||||
primary_color : '#000',
|
||||
link_color : '#007dbf',
|
||||
forum_id : 204509,
|
||||
support_tab_name : 'Contact',
|
||||
feedback_tab_name : 'Feedback',
|
||||
tab_label : 'Feedback',
|
||||
tab_color : '#7e72db',
|
||||
tab_position : 'bottom-right',
|
||||
tab_inverted : false
|
||||
}]);
|
||||
</script>
|
||||
</wicket:container>
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
|
||||
import org.apache.wicket.markup.html.WebMarkupContainer;
|
||||
import org.apache.wicket.markup.html.WebPage;
|
||||
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
@@ -29,6 +28,7 @@ import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.MailService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.CommaFeedSession;
|
||||
import com.commafeed.frontend.utils.WicketUtils;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
@@ -4,8 +4,8 @@ import javax.inject.Inject;
|
||||
|
||||
import org.apache.wicket.markup.html.WebPage;
|
||||
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.services.UserService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.CommaFeedSession;
|
||||
|
||||
public class DemoLoginPage extends WebPage {
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.apache.wicket.markup.head.CssHeaderItem;
|
||||
import org.apache.wicket.markup.head.IHeaderResponse;
|
||||
import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.frontend.CommaFeedSession;
|
||||
@@ -21,7 +22,11 @@ public class HomePage extends BasePage {
|
||||
response.render(CssHeaderItem.forReference(new UserCustomCssReference() {
|
||||
@Override
|
||||
protected String getCss() {
|
||||
UserSettings settings = userSettingsDAO.findByUser(CommaFeedSession.get().getUser());
|
||||
User user = CommaFeedSession.get().getUser();
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
return settings == null ? null : settings.getCustomCss();
|
||||
}
|
||||
}, new PageParameters().add("_t", System.currentTimeMillis()), null));
|
||||
|
||||
@@ -54,13 +54,13 @@ public class NextUnreadRedirectPage extends WebPage {
|
||||
List<FeedEntryStatus> statuses = null;
|
||||
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
statuses = feedEntryStatusDAO.findBySubscriptions(subs, true, null, null, 0, 1, order, true, false);
|
||||
statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, false, null);
|
||||
} else {
|
||||
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
|
||||
if (category != null) {
|
||||
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
|
||||
statuses = feedEntryStatusDAO.findBySubscriptions(subscriptions, true, null, null, 0, 1, order, true, false);
|
||||
statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1, order, true, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
<div class="text-center">
|
||||
<img src="images/logo_2.png" />
|
||||
<div wicket:id="feedback"></div>
|
||||
<form wicket:id="form">
|
||||
New Password:
|
||||
<input type="password" wicket:id="password" />
|
||||
<br />
|
||||
Confirm:
|
||||
<input type="password" wicket:id="confirm" />
|
||||
<br />
|
||||
<input type="submit" class="btn btn-primary" value="Submit" />
|
||||
<input type="button" class="btn" wicket:id="cancel" value="Home page" />
|
||||
<form wicket:id="form" class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label>New Password</label>
|
||||
<input type="password" wicket:id="password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirm</label>
|
||||
<input type="password" wicket:id="confirm" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Submit" />
|
||||
<input type="button" class="btn btn-default" wicket:id="cancel" value="Home page" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
<body>
|
||||
<wicket:extend>
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<div class="col-xs-6 col-xs-offset-3 text-center">
|
||||
<img src="images/logo_2.png" />
|
||||
<div wicket:id="feedback"></div>
|
||||
<form wicket:id="form">
|
||||
Email:
|
||||
<input type="email" wicket:id="email" />
|
||||
<br />
|
||||
<input type="submit" class="btn btn-primary" value="Submit" />
|
||||
<input type="button" class="btn" wicket:id="cancel" value="Cancel" />
|
||||
<form wicket:id="form" class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" wicket:id="email" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Submit" />
|
||||
<input type="button" class="btn btn-default" wicket:id="cancel" value="Cancel" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<body>
|
||||
<wicket:extend>
|
||||
<div class="welcome">
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid header">
|
||||
<div class="container">
|
||||
<div class="row header">
|
||||
<div class="pull-left">
|
||||
<a wicket:id="logo-link">
|
||||
<img src="images/logo_2.png"></img>
|
||||
@@ -23,14 +23,14 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="well" id="login-panel">
|
||||
<h3>Login</h3>
|
||||
<span wicket:id="login"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span6" wicket:enclosure="register">
|
||||
<div class="col-md-6" wicket:enclosure="register">
|
||||
<div class="well" id="register-panel">
|
||||
<h3>Register</h3>
|
||||
<span wicket:id="register"></span>
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<hr />
|
||||
<div class="footer">
|
||||
<div class="row-fluid">
|
||||
<div class="row">
|
||||
<span>
|
||||
©
|
||||
<a href="http://www.commafeed.com" target="_blank">CommaFeed</a>
|
||||
|
||||
@@ -4,24 +4,20 @@
|
||||
<wicket:panel>
|
||||
<span wicket:id="feedback"></span>
|
||||
<form wicket:id="signInForm">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="username">User Name</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="username" wicket:id="username" class="input-block-level"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username">User Name</label>
|
||||
<input type="text" id="username" wicket:id="username" class="form-control"></input>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="password">Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" id="password" wicket:id="password" class="input-block-level"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" wicket:id="password" class="form-control"></input>
|
||||
</div>
|
||||
<p class="help-block" wicket:id="rememberMeRow">
|
||||
<label class="checkbox">
|
||||
<div wicket:id="rememberMeRow">
|
||||
<label>
|
||||
<input wicket:id="rememberMe" type="checkbox" />
|
||||
Remember me
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="btn btn-primary" value="Log in" />
|
||||
<a wicket:id="recover" class="pull-right">Forgot password?</a>
|
||||
|
||||
@@ -4,23 +4,17 @@
|
||||
<wicket:panel>
|
||||
<div wicket:id="feedback"></div>
|
||||
<form wicket:id="form" autocomplete="off">
|
||||
<div class="control-group">
|
||||
<label class="control-label">User Name</label>
|
||||
<div class="controls">
|
||||
<input type="text" wicket:id="name" class="input-block-level"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>User Name</label>
|
||||
<input type="text" wicket:id="name" class="form-control"></input>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" wicket:id="password" class="input-block-level"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" wicket:id="password" class="form-control"></input>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">Email address (used for password recovery only)</label>
|
||||
<div class="controls">
|
||||
<input type="email" wicket:id="email" class="input-block-level"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email address (used for password recovery only)</label>
|
||||
<input type="email" wicket:id="email" class="form-control"></input>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="btn btn-primary" value="Register" />
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.commafeed.frontend.resources;
|
||||
|
||||
import ro.isdc.wro.model.resource.processor.impl.css.CssUrlRewritingProcessor;
|
||||
|
||||
public class CustomCssUrlRewritingProcessor extends CssUrlRewritingProcessor {
|
||||
|
||||
/**
|
||||
* ignore webjar image replacements since they won't be available at runtime anyway
|
||||
*/
|
||||
@Override
|
||||
protected String replaceImageUrl(String cssUri, String imageUrl) {
|
||||
if (cssUri.startsWith("webjar:")) {
|
||||
return imageUrl;
|
||||
}
|
||||
return super.replaceImageUrl(cssUri, imageUrl);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public class WroAdditionalProvider implements ProcessorProvider {
|
||||
map.put("sassOnlyProcessor", new SassOnlyProcessor());
|
||||
map.put("sassImport", new SassImportProcessor());
|
||||
map.put("timestamp", new TimestampProcessor());
|
||||
map.put("cssUrlRewriting", new CustomCssUrlRewritingProcessor());
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public class WroManagerFactory extends ConfigurableWroManagerFactory {
|
||||
map.put("sassOnlyProcessor", new SassOnlyProcessor());
|
||||
map.put("sassImport", new SassImportProcessor());
|
||||
map.put("timestamp", new TimestampProcessor());
|
||||
map.put("cssUrlRewriting", new CustomCssUrlRewritingProcessor());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import java.io.OutputStream;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.ext.MessageBodyReader;
|
||||
@@ -19,6 +21,7 @@ import org.apache.http.HttpHeaders;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
|
||||
@Provider
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@@ -30,6 +33,9 @@ public class JsonProvider implements MessageBodyReader<Object>, MessageBodyWrite
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
@Context
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Override
|
||||
public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
|
||||
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
|
||||
@@ -38,10 +44,29 @@ public class JsonProvider implements MessageBodyReader<Object>, MessageBodyWrite
|
||||
httpHeaders.putSingle(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_VALUE);
|
||||
httpHeaders.putSingle(HttpHeaders.PRAGMA, CACHE_CONTROL_VALUE);
|
||||
|
||||
getMapper().writeValue(entityStream, value);
|
||||
ObjectWriter writer = getMapper().writer();
|
||||
if (hasPrettyPrint(annotations)) {
|
||||
writer = writer.withDefaultPrettyPrinter();
|
||||
}
|
||||
writer.writeValue(entityStream, value);
|
||||
|
||||
}
|
||||
|
||||
private boolean hasPrettyPrint(Annotation[] annotations) {
|
||||
boolean prettyPrint = false;
|
||||
|
||||
for (Annotation annotation : annotations) {
|
||||
if (PrettyPrint.class.equals(annotation.annotationType())) {
|
||||
prettyPrint = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prettyPrint && request != null) {
|
||||
prettyPrint = Boolean.parseBoolean(request.getParameter("pretty"));
|
||||
}
|
||||
return prettyPrint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
|
||||
MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
|
||||
|
||||
12
src/main/java/com/commafeed/frontend/rest/PrettyPrint.java
Normal file
12
src/main/java/com/commafeed/frontend/rest/PrettyPrint.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.commafeed.frontend.rest;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PrettyPrint {
|
||||
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.apache.wicket.protocol.http.servlet.ServletWebResponse;
|
||||
import org.apache.wicket.request.cycle.RequestCycle;
|
||||
import org.apache.wicket.util.crypt.Base64;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
@@ -42,6 +43,9 @@ public abstract class AbstractREST {
|
||||
@Context
|
||||
private HttpServletResponse response;
|
||||
|
||||
@Inject
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
private UserDAO userDAO;
|
||||
|
||||
@@ -93,10 +97,13 @@ public abstract class AbstractREST {
|
||||
}
|
||||
|
||||
@AroundInvoke
|
||||
public Object checkSecurity(InvocationContext context) throws Exception {
|
||||
public Object intercept(InvocationContext context) throws Exception {
|
||||
Method method = context.getMethod();
|
||||
|
||||
// check security
|
||||
boolean allowed = true;
|
||||
User user = null;
|
||||
Method method = context.getMethod();
|
||||
|
||||
SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method.getAnnotation(SecurityCheck.class) : method
|
||||
.getDeclaringClass().getAnnotation(SecurityCheck.class);
|
||||
|
||||
@@ -118,7 +125,16 @@ public abstract class AbstractREST {
|
||||
|
||||
}
|
||||
|
||||
return context.proceed();
|
||||
Object result = null;
|
||||
com.codahale.metrics.Timer.Context timer = metrics.timer(
|
||||
MetricRegistry.name(method.getDeclaringClass(), method.getName(), "responseTime")).time();
|
||||
try {
|
||||
result = context.proceed();
|
||||
} finally {
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkRole(Role requiredRole) {
|
||||
@@ -130,4 +146,5 @@ public abstract class AbstractREST {
|
||||
boolean authorized = roles.hasAnyRole(new Roles(requiredRole.name()));
|
||||
return authorized;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
package com.commafeed.frontend.rest.resources;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.DatabaseCleaner;
|
||||
import com.commafeed.backend.MetricsBean;
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedDAO.DuplicateMode;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserRoleDAO;
|
||||
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
|
||||
import com.commafeed.backend.feeds.FeedRefreshUpdater;
|
||||
import com.commafeed.backend.feeds.FeedRefreshWorker;
|
||||
import com.commafeed.backend.model.ApplicationSettings;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.DatabaseCleaningService;
|
||||
import com.commafeed.backend.services.FeedService;
|
||||
import com.commafeed.backend.services.PasswordEncryptionService;
|
||||
import com.commafeed.backend.services.UserService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.SecurityCheck;
|
||||
import com.commafeed.frontend.model.FeedCount;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
import com.commafeed.frontend.model.request.FeedMergeRequest;
|
||||
import com.commafeed.frontend.model.request.IDRequest;
|
||||
import com.commafeed.frontend.rest.PrettyPrint;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.wordnik.swagger.annotations.Api;
|
||||
@@ -70,10 +62,10 @@ public class AdminREST extends AbstractREST {
|
||||
FeedDAO feedDAO;
|
||||
|
||||
@Inject
|
||||
MetricsBean metricsBean;
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
DatabaseCleaner cleaner;
|
||||
DatabaseCleaningService cleaner;
|
||||
|
||||
@Inject
|
||||
FeedRefreshWorker feedRefreshWorker;
|
||||
@@ -226,26 +218,24 @@ public class AdminREST extends AbstractREST {
|
||||
|
||||
@Path("/metrics")
|
||||
@GET
|
||||
@PrettyPrint
|
||||
@ApiOperation(value = "Retrieve server metrics")
|
||||
public Response getMetrics(@QueryParam("backlog") @DefaultValue("false") boolean backlog) {
|
||||
Map<String, Object> map = Maps.newLinkedHashMap();
|
||||
map.put("lastMinute", metricsBean.getLastMinute());
|
||||
map.put("lastHour", metricsBean.getLastHour());
|
||||
if (backlog) {
|
||||
map.put("backlog", taskGiver.getUpdatableCount());
|
||||
}
|
||||
map.put("http_active", feedRefreshWorker.getActiveCount());
|
||||
map.put("http_queue", feedRefreshWorker.getQueueSize());
|
||||
map.put("database_active", feedRefreshUpdater.getActiveCount());
|
||||
map.put("database_queue", feedRefreshUpdater.getQueueSize());
|
||||
map.put("cache", metricsBean.getCacheStats());
|
||||
public Response getMetrics() {
|
||||
return Response.ok(metrics).build();
|
||||
}
|
||||
|
||||
@Path("/cleanup/entries")
|
||||
@GET
|
||||
@ApiOperation(value = "Entries cleanup", notes = "Delete entries without subscriptions")
|
||||
public Response cleanupEntries() {
|
||||
Map<String, Long> map = Maps.newHashMap();
|
||||
map.put("entries_without_subscriptions", cleaner.cleanEntriesWithoutSubscriptions());
|
||||
return Response.ok(map).build();
|
||||
}
|
||||
|
||||
@Path("/cleanup/feeds")
|
||||
@GET
|
||||
@ApiOperation(value = "Feeds cleanup", notes = "Delete feeds without subscriptions and entries without feeds")
|
||||
@ApiOperation(value = "Feeds cleanup", notes = "Delete feeds without subscriptions")
|
||||
public Response cleanupFeeds() {
|
||||
Map<String, Long> map = Maps.newHashMap();
|
||||
map.put("feeds_without_subscriptions", cleaner.cleanFeedsWithoutSubscriptions());
|
||||
@@ -261,44 +251,4 @@ public class AdminREST extends AbstractREST {
|
||||
return Response.ok(map).build();
|
||||
}
|
||||
|
||||
@Path("/cleanup/entries")
|
||||
@GET
|
||||
@ApiOperation(value = "Entries cleanup", notes = "Delete entries older than given date")
|
||||
public Response cleanupEntries(@QueryParam("days") @DefaultValue("30") int days) {
|
||||
Map<String, Long> map = Maps.newHashMap();
|
||||
map.put("old_entries", cleaner.cleanEntriesOlderThan(days, TimeUnit.DAYS));
|
||||
return Response.ok(map).build();
|
||||
}
|
||||
|
||||
@Path("/cleanup/findDuplicateFeeds")
|
||||
@GET
|
||||
@ApiOperation(value = "Find duplicate feeds")
|
||||
public Response findDuplicateFeeds(@QueryParam("mode") DuplicateMode mode, @QueryParam("page") int page,
|
||||
@QueryParam("limit") int limit, @QueryParam("minCount") long minCount) {
|
||||
List<FeedCount> list = feedDAO.findDuplicates(mode, limit * page, limit, minCount);
|
||||
return Response.ok(list).build();
|
||||
}
|
||||
|
||||
@Path("/cleanup/merge")
|
||||
@POST
|
||||
@ApiOperation(value = "Merge feeds", notes = "Merge feeds together")
|
||||
public Response mergeFeeds(@ApiParam(required = true) FeedMergeRequest request) {
|
||||
Feed into = feedDAO.findById(request.getIntoFeedId());
|
||||
if (into == null) {
|
||||
return Response.status(Status.BAD_REQUEST).entity("'into feed' not found").build();
|
||||
}
|
||||
|
||||
List<Feed> feeds = Lists.newArrayList();
|
||||
for (Long feedId : request.getFeedIds()) {
|
||||
Feed feed = feedDAO.findById(feedId);
|
||||
feeds.add(feed);
|
||||
}
|
||||
|
||||
if (feeds.isEmpty()) {
|
||||
return Response.status(Status.BAD_REQUEST).entity("'from feeds' empty").build();
|
||||
}
|
||||
|
||||
cleaner.mergeFeeds(into, feeds);
|
||||
return Response.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import javax.ws.rs.core.Response.Status;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang.ObjectUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
@@ -106,7 +106,8 @@ public class CategoryREST extends AbstractREST {
|
||||
@ApiParam(
|
||||
value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
|
||||
@ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds,
|
||||
@ApiParam(value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds) {
|
||||
@ApiParam(value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds,
|
||||
@ApiParam(value = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
|
||||
|
||||
Preconditions.checkNotNull(readType);
|
||||
|
||||
@@ -135,11 +136,11 @@ public class CategoryREST extends AbstractREST {
|
||||
}
|
||||
|
||||
if (ALL.equals(id)) {
|
||||
entries.setName("All");
|
||||
entries.setName(ObjectUtils.defaultIfNull(tag, "All"));
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser());
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(subs, unreadOnly, keywords, newerThanDate, offset,
|
||||
limit + 1, order, true, onlyIds);
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), subs, unreadOnly, keywords, newerThanDate,
|
||||
offset, limit + 1, order, true, onlyIds, tag);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(
|
||||
@@ -161,8 +162,8 @@ public class CategoryREST extends AbstractREST {
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(getUser(), parent);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(getUser(), categories);
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(subs, unreadOnly, keywords, newerThanDate, offset,
|
||||
limit + 1, order, true, onlyIds);
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), subs, unreadOnly, keywords, newerThanDate,
|
||||
offset, limit + 1, order, true, onlyIds, tag);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(
|
||||
@@ -170,8 +171,9 @@ public class CategoryREST extends AbstractREST {
|
||||
.isImageProxyEnabled()));
|
||||
}
|
||||
entries.setName(parent.getName());
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).entity("<message>category not found</message>").build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean hasMore = entries.getEntries().size() > limit;
|
||||
@@ -181,6 +183,7 @@ public class CategoryREST extends AbstractREST {
|
||||
}
|
||||
|
||||
entries.setTimestamp(System.currentTimeMillis());
|
||||
entries.setIgnoredReadStatus(STARRED.equals(id) || keywords != null || tag != null);
|
||||
FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords);
|
||||
return Response.ok(entries).build();
|
||||
}
|
||||
@@ -191,16 +194,24 @@ public class CategoryREST extends AbstractREST {
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
@SecurityCheck(value = Role.USER, apiKeyAllowed = true)
|
||||
public Response getCategoryEntriesAsFeed(
|
||||
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id) {
|
||||
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
|
||||
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType,
|
||||
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
|
||||
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
|
||||
@ApiParam(
|
||||
value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
|
||||
@ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds,
|
||||
@ApiParam(value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds,
|
||||
@ApiParam(value = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
|
||||
|
||||
Preconditions.checkNotNull(id);
|
||||
|
||||
ReadingMode readType = ReadingMode.all;
|
||||
ReadingOrder order = ReadingOrder.desc;
|
||||
int offset = 0;
|
||||
int limit = 20;
|
||||
|
||||
Entries entries = (Entries) getCategoryEntries(id, readType, null, offset, limit, order, null, false, null).getEntity();
|
||||
Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, onlyIds, excludedSubscriptionIds,
|
||||
tag);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
Entries entries = (Entries) response.getEntity();
|
||||
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("rss_2.0");
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.commafeed.frontend.rest.resources;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryTagDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.FeedEntryService;
|
||||
import com.commafeed.backend.services.FeedEntryTagService;
|
||||
import com.commafeed.frontend.model.request.MarkRequest;
|
||||
import com.commafeed.frontend.model.request.MultipleMarkRequest;
|
||||
import com.commafeed.frontend.model.request.StarRequest;
|
||||
import com.commafeed.frontend.model.request.TagRequest;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.wordnik.swagger.annotations.Api;
|
||||
import com.wordnik.swagger.annotations.ApiOperation;
|
||||
@@ -30,6 +36,12 @@ public class EntryREST extends AbstractREST {
|
||||
@Inject
|
||||
FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
|
||||
@Inject
|
||||
FeedEntryTagDAO feedEntryTagDAO;
|
||||
|
||||
@Inject
|
||||
FeedEntryTagService feedEntryTagService;
|
||||
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@@ -71,4 +83,24 @@ public class EntryREST extends AbstractREST {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/tags")
|
||||
@GET
|
||||
@ApiOperation(value = "Get list of tags for the user", notes = "Get list of tags for the user")
|
||||
public Response getTags() {
|
||||
List<String> tags = feedEntryTagDAO.findByUser(getUser());
|
||||
return Response.ok(tags).build();
|
||||
}
|
||||
|
||||
@Path("/tag")
|
||||
@POST
|
||||
@ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread")
|
||||
public Response tagFeedEntry(@ApiParam(value = "Tag Request", required = true) TagRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getEntryId());
|
||||
|
||||
feedEntryTagService.updateTags(getUser(), req.getEntryId(), req.getTags());
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang.ObjectUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
@@ -47,8 +46,6 @@ import com.commafeed.backend.feeds.FeedFetcher;
|
||||
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
|
||||
import com.commafeed.backend.feeds.FeedUtils;
|
||||
import com.commafeed.backend.feeds.FetchedFeed;
|
||||
import com.commafeed.backend.feeds.OPMLExporter;
|
||||
import com.commafeed.backend.feeds.OPMLImporter;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
@@ -56,9 +53,12 @@ import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.opml.OPMLExporter;
|
||||
import com.commafeed.backend.opml.OPMLImporter;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.FeedEntryService;
|
||||
import com.commafeed.backend.services.FeedSubscriptionService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.SecurityCheck;
|
||||
import com.commafeed.frontend.model.Entries;
|
||||
import com.commafeed.frontend.model.Entry;
|
||||
@@ -71,6 +71,7 @@ import com.commafeed.frontend.model.request.IDRequest;
|
||||
import com.commafeed.frontend.model.request.MarkRequest;
|
||||
import com.commafeed.frontend.model.request.SubscribeRequest;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.sun.syndication.feed.opml.Opml;
|
||||
import com.sun.syndication.feed.synd.SyndEntry;
|
||||
@@ -167,8 +168,8 @@ public class FeedREST extends AbstractREST {
|
||||
entries.setErrorCount(subscription.getFeed().getErrorCount());
|
||||
entries.setFeedLink(subscription.getFeed().getLink());
|
||||
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(Arrays.asList(subscription), unreadOnly, keywords,
|
||||
newerThanDate, offset, limit + 1, order, true, onlyIds);
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), Arrays.asList(subscription), unreadOnly,
|
||||
keywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(
|
||||
@@ -181,9 +182,12 @@ public class FeedREST extends AbstractREST {
|
||||
entries.setHasMore(true);
|
||||
entries.getEntries().remove(entries.getEntries().size() - 1);
|
||||
}
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).entity("<message>feed not found</message>").build();
|
||||
}
|
||||
|
||||
entries.setTimestamp(System.currentTimeMillis());
|
||||
entries.setIgnoredReadStatus(keywords != null);
|
||||
FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords);
|
||||
return Response.ok(entries).build();
|
||||
}
|
||||
@@ -193,16 +197,22 @@ public class FeedREST extends AbstractREST {
|
||||
@ApiOperation(value = "Get feed entries as a feed", notes = "Get a feed of feed entries")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
@SecurityCheck(value = Role.USER, apiKeyAllowed = true)
|
||||
public Response getFeedEntriesAsFeed(@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id) {
|
||||
public Response getFeedEntriesAsFeed(
|
||||
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType,
|
||||
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
|
||||
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
|
||||
@ApiParam(
|
||||
value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
|
||||
@ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds) {
|
||||
|
||||
Preconditions.checkNotNull(id);
|
||||
|
||||
ReadingMode readType = ReadingMode.all;
|
||||
ReadingOrder order = ReadingOrder.desc;
|
||||
int offset = 0;
|
||||
int limit = 20;
|
||||
|
||||
Entries entries = (Entries) getFeedEntries(id, readType, null, offset, limit, order, null, false).getEntity();
|
||||
Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords, onlyIds);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
Entries entries = (Entries) response.getEntity();
|
||||
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("rss_2.0");
|
||||
@@ -235,7 +245,7 @@ public class FeedREST extends AbstractREST {
|
||||
try {
|
||||
FetchedFeed feed = feedFetcher.fetch(url, true, null, null, null, null);
|
||||
info = new FeedInfo();
|
||||
info.setUrl(feed.getFeed().getUrl());
|
||||
info.setUrl(feed.getUrlAfterRedirect());
|
||||
info.setTitle(feed.getTitle());
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -256,7 +266,8 @@ public class FeedREST extends AbstractREST {
|
||||
try {
|
||||
info = fetchFeedInternal(req.getUrl());
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(Throwables.getStackTraceAsString(Throwables.getRootCause(e)))
|
||||
.build();
|
||||
}
|
||||
return Response.ok(info).build();
|
||||
}
|
||||
@@ -265,11 +276,7 @@ public class FeedREST extends AbstractREST {
|
||||
@GET
|
||||
@ApiOperation(value = "Queue all feeds of the user for refresh", notes = "Manually add all feeds of the user to the refresh queue")
|
||||
public Response queueAllForRefresh() {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser());
|
||||
for (FeedSubscription sub : subs) {
|
||||
Feed feed = sub.getFeed();
|
||||
taskGiver.add(feed, true);
|
||||
}
|
||||
feedSubscriptionService.refreshAll(getUser());
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@@ -368,8 +375,10 @@ public class FeedREST extends AbstractREST {
|
||||
try {
|
||||
url = fetchFeedInternal(url).getUrl();
|
||||
|
||||
FeedCategory category = CategoryREST.ALL.equals(req.getCategoryId()) ? null : feedCategoryDAO.findById(Long.valueOf(req
|
||||
.getCategoryId()));
|
||||
FeedCategory category = null;
|
||||
if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) {
|
||||
category = feedCategoryDAO.findById(Long.valueOf(req.getCategoryId()));
|
||||
}
|
||||
FeedInfo info = fetchFeedInternal(url);
|
||||
feedSubscriptionService.subscribe(getUser(), info.getUrl(), req.getTitle(), category);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.commafeed.frontend.rest.resources;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.Consumes;
|
||||
@@ -22,7 +23,8 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.commafeed.backend.MetricsBean;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.feeds.FeedParser;
|
||||
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
|
||||
@@ -52,8 +54,13 @@ public class PubSubHubbubCallbackREST extends AbstractREST {
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
MetricsBean metricsBean;
|
||||
private Meter pushReceived;
|
||||
|
||||
@PostConstruct
|
||||
public void initMetrics() {
|
||||
pushReceived = metrics.meter(MetricRegistry.name(getClass(), "pushReceived"));
|
||||
|
||||
}
|
||||
|
||||
@Path("/callback")
|
||||
@GET
|
||||
@@ -119,7 +126,7 @@ public class PubSubHubbubCallbackREST extends AbstractREST {
|
||||
log.debug("pushing content to queue for {}", feed.getUrl());
|
||||
taskGiver.add(feed, false);
|
||||
}
|
||||
metricsBean.pushReceived(feeds.size());
|
||||
pushReceived.mark();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Could not parse pubsub callback: " + e.getMessage());
|
||||
|
||||
@@ -10,10 +10,10 @@ import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.feeds.FeedUtils;
|
||||
import com.commafeed.backend.services.ApplicationPropertiesService;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.model.ServerInfo;
|
||||
import com.wordnik.swagger.annotations.Api;
|
||||
import com.wordnik.swagger.annotations.ApiOperation;
|
||||
|
||||
@@ -11,7 +11,6 @@ import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserRoleDAO;
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
@@ -25,6 +24,7 @@ import com.commafeed.backend.model.UserSettings.ViewMode;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.PasswordEncryptionService;
|
||||
import com.commafeed.backend.services.UserService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.SecurityCheck;
|
||||
import com.commafeed.frontend.model.Settings;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
@@ -74,20 +74,44 @@ public class UserREST extends AbstractREST {
|
||||
s.setReadingOrder(settings.getReadingOrder().name());
|
||||
s.setViewMode(settings.getViewMode().name());
|
||||
s.setShowRead(settings.isShowRead());
|
||||
s.setSocialButtons(settings.isSocialButtons());
|
||||
|
||||
s.setEmail(settings.isEmail());
|
||||
s.setGmail(settings.isGmail());
|
||||
s.setFacebook(settings.isFacebook());
|
||||
s.setTwitter(settings.isTwitter());
|
||||
s.setGoogleplus(settings.isGoogleplus());
|
||||
s.setTumblr(settings.isTumblr());
|
||||
s.setPocket(settings.isPocket());
|
||||
s.setInstapaper(settings.isInstapaper());
|
||||
s.setBuffer(settings.isBuffer());
|
||||
s.setReadability(settings.isReadability());
|
||||
|
||||
s.setScrollMarks(settings.isScrollMarks());
|
||||
s.setTheme(settings.getTheme());
|
||||
s.setCustomCss(settings.getCustomCss());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
} else {
|
||||
s.setReadingMode(ReadingMode.unread.name());
|
||||
s.setReadingOrder(ReadingOrder.desc.name());
|
||||
s.setViewMode(ViewMode.title.name());
|
||||
s.setShowRead(true);
|
||||
s.setTheme("default");
|
||||
s.setSocialButtons(true);
|
||||
|
||||
s.setEmail(true);
|
||||
s.setGmail(true);
|
||||
s.setFacebook(true);
|
||||
s.setTwitter(true);
|
||||
s.setGoogleplus(true);
|
||||
s.setTumblr(true);
|
||||
s.setPocket(true);
|
||||
s.setInstapaper(true);
|
||||
s.setBuffer(true);
|
||||
s.setReadability(true);
|
||||
|
||||
s.setScrollMarks(true);
|
||||
s.setLanguage("en");
|
||||
s.setScrollSpeed(400);
|
||||
}
|
||||
return Response.ok(s).build();
|
||||
}
|
||||
@@ -114,8 +138,20 @@ public class UserREST extends AbstractREST {
|
||||
s.setScrollMarks(settings.isScrollMarks());
|
||||
s.setTheme(settings.getTheme());
|
||||
s.setCustomCss(settings.getCustomCss());
|
||||
s.setSocialButtons(settings.isSocialButtons());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
|
||||
s.setEmail(settings.isEmail());
|
||||
s.setGmail(settings.isGmail());
|
||||
s.setFacebook(settings.isFacebook());
|
||||
s.setTwitter(settings.isTwitter());
|
||||
s.setGoogleplus(settings.isGoogleplus());
|
||||
s.setTumblr(settings.isTumblr());
|
||||
s.setPocket(settings.isPocket());
|
||||
s.setInstapaper(settings.isInstapaper());
|
||||
s.setBuffer(settings.isBuffer());
|
||||
s.setReadability(settings.isReadability());
|
||||
|
||||
userSettingsDAO.saveOrUpdate(s);
|
||||
return Response.ok().build();
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
/**
|
||||
* See http://en.wikipedia.org/wiki/URL_normalization for a reference Note: some
|
||||
* parts of the code are adapted from: http://stackoverflow.com/a/4057470/405418
|
||||
@@ -46,7 +48,7 @@ public class URLCanonicalizer {
|
||||
URL canonicalURL = new URL(UrlResolver.resolveUrl(context == null ? "" : context, href));
|
||||
|
||||
String host = canonicalURL.getHost().toLowerCase();
|
||||
if (host == "") {
|
||||
if (StringUtils.isBlank(host)) {
|
||||
// This is an invalid Url.
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package liquibase.integration.cdi;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import javax.enterprise.event.Observes;
|
||||
import javax.enterprise.inject.Produces;
|
||||
import javax.enterprise.inject.spi.AfterBeanDiscovery;
|
||||
import javax.enterprise.inject.spi.AfterDeploymentValidation;
|
||||
import javax.enterprise.inject.spi.BeanManager;
|
||||
import javax.enterprise.inject.spi.Extension;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import liquibase.integration.cdi.annotations.LiquibaseType;
|
||||
import liquibase.resource.ResourceAccessor;
|
||||
|
||||
/**
|
||||
* temporary fix until https://liquibase.jira.com/browse/CORE-1325 is fixed
|
||||
*/
|
||||
public class CDIBootstrap implements Extension {
|
||||
|
||||
void afterBeanDiscovery(@Observes AfterBeanDiscovery abd, BeanManager bm) {
|
||||
}
|
||||
|
||||
void afterDeploymentValidation(@Observes AfterDeploymentValidation event, BeanManager manager) {
|
||||
}
|
||||
|
||||
@Produces
|
||||
@LiquibaseType
|
||||
public CDILiquibaseConfig createConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Produces
|
||||
@LiquibaseType
|
||||
public DataSource createDataSource() throws SQLException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Produces
|
||||
@LiquibaseType
|
||||
public ResourceAccessor create() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<entity-mappings version="2.0"
|
||||
xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="
|
||||
http://java.sun.com/xml/ns/persistence/orm
|
||||
http://java.sun.com/xml/ns/persistence/orm_2_0.xsd">
|
||||
|
||||
<named-query name="Statuses.deleteOld">
|
||||
<query>delete from FeedEntryStatus s where s.entryInserted < :date and s.starred = false</query>
|
||||
</named-query>
|
||||
|
||||
</entity-mappings>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<persistence version="2.0"
|
||||
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="
|
||||
http://java.sun.com/xml/ns/persistence
|
||||
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
|
||||
@@ -9,6 +8,7 @@
|
||||
<jta-data-source>${jpa.datasource.name}</jta-data-source>
|
||||
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
|
||||
<properties>
|
||||
<property name="openejb.jpa.auto-scan" value="true" />
|
||||
|
||||
<property name="format_sql" value="true" />
|
||||
<property name="use_sql_comments" value="true" />
|
||||
@@ -22,26 +22,17 @@
|
||||
|
||||
<property name="hibernate.generate_statistics" value="true" />
|
||||
|
||||
<property name="hibernate.cache.use_second_level_cache"
|
||||
value="${jpa.cache}" />
|
||||
<property name="hibernate.cache.use_second_level_cache" value="${jpa.cache}" />
|
||||
<property name="hibernate.cache.use_query_cache" value="${jpa.cache}" />
|
||||
|
||||
<property name="hibernate.cache.region.factory_class"
|
||||
value="org.hibernate.cache.infinispan.InfinispanRegionFactory" />
|
||||
<property name="hibernate.cache.infinispan.statistics"
|
||||
value="true" />
|
||||
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.infinispan.InfinispanRegionFactory" />
|
||||
<property name="hibernate.cache.infinispan.statistics" value="true" />
|
||||
|
||||
<property name="hibernate.cache.infinispan.entity.eviction.strategy"
|
||||
value="LRU" />
|
||||
<property
|
||||
name="hibernate.cache.infinispan.entity.eviction.wake_up_interval"
|
||||
value="2000" />
|
||||
<property name="hibernate.cache.infinispan.entity.eviction.max_entries"
|
||||
value="10000" />
|
||||
<property name="hibernate.cache.infinispan.entity.expiration.lifespan"
|
||||
value="60000" />
|
||||
<property name="hibernate.cache.infinispan.entity.expiration.max_idle"
|
||||
value="30000" />
|
||||
<property name="hibernate.cache.infinispan.entity.eviction.strategy" value="LRU" />
|
||||
<property name="hibernate.cache.infinispan.entity.eviction.wake_up_interval" value="2000" />
|
||||
<property name="hibernate.cache.infinispan.entity.eviction.max_entries" value="10000" />
|
||||
<property name="hibernate.cache.infinispan.entity.expiration.lifespan" value="60000" />
|
||||
<property name="hibernate.cache.infinispan.entity.expiration.max_idle" value="30000" />
|
||||
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
|
||||
@@ -357,6 +357,7 @@
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="athou" id="status-cleanup">
|
||||
<validCheckSum>7:cf40ae235c2d4086c5fa6ac64102c6a9</validCheckSum>
|
||||
<delete tableName="FEEDENTRYSTATUSES">
|
||||
<where>read_status = false and starred = false</where>
|
||||
</delete>
|
||||
|
||||
11
src/main/resources/changelogs/db.changelog-1.3.xml
Normal file
11
src/main/resources/changelogs/db.changelog-1.3.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">
|
||||
|
||||
<changeSet author="athou" id="add-url-after-redirect">
|
||||
<addColumn tableName="FEEDS">
|
||||
<column name="url_after_redirect" type="VARCHAR(2048)" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
51
src/main/resources/changelogs/db.changelog-1.4.xml
Normal file
51
src/main/resources/changelogs/db.changelog-1.4.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">
|
||||
|
||||
<changeSet author="athou" id="scroll-speed">
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="scroll_speed" type="BIGINT" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="athou" id="set-default-scroll-speed">
|
||||
<update tableName="USERSETTINGS">
|
||||
<column name="scroll_speed" valueNumeric="400" />
|
||||
</update>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="athou" id="create-tags-table">
|
||||
<validCheckSum>7:fdd37bdee09c8fbbcbcd867b05decaae</validCheckSum>
|
||||
<createTable tableName="FEEDENTRYTAGS">
|
||||
<column name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true" />
|
||||
</column>
|
||||
<column name="entry_id" type="BIGINT">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="user_id" type="BIGINT">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="name" type="VARCHAR(40)">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<addForeignKeyConstraint constraintName="fk_entry_id" baseTableName="FEEDENTRYTAGS" baseColumnNames="entry_id"
|
||||
referencedTableName="FEEDENTRIES" referencedColumnNames="id" />
|
||||
<addForeignKeyConstraint constraintName="fk_user_id" baseTableName="FEEDENTRYTAGS" baseColumnNames="user_id"
|
||||
referencedTableName="USERS" referencedColumnNames="id" />
|
||||
|
||||
<createIndex tableName="FEEDENTRYTAGS" indexName="user_entry_name_index">
|
||||
<column name="user_id" />
|
||||
<column name="entry_id" />
|
||||
<column name="name" />
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="athou" id="add-full-refresh-timestamp">
|
||||
<addColumn tableName="USERS">
|
||||
<column name="last_full_refresh" type="DATETIME" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
53
src/main/resources/changelogs/db.changelog-1.5.xml
Normal file
53
src/main/resources/changelogs/db.changelog-1.5.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
|
||||
<changeSet id="add-detailed-social-options" author="athou">
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="email" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="gmail" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="facebook" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="twitter" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="googleplus" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="tumblr" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="pocket" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="instapaper" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="buffer" type="BIT"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="readability" type="BIT"></column>
|
||||
</addColumn>
|
||||
|
||||
<dropColumn tableName="USERSETTINGS" columnName="socialButtons" />
|
||||
|
||||
<update tableName="USERSETTINGS">
|
||||
<column name="email" valueBoolean="true"></column>
|
||||
<column name="gmail" valueBoolean="true"></column>
|
||||
<column name="facebook" valueBoolean="true"></column>
|
||||
<column name="twitter" valueBoolean="true"></column>
|
||||
<column name="googleplus" valueBoolean="true"></column>
|
||||
<column name="tumblr" valueBoolean="true"></column>
|
||||
<column name="pocket" valueBoolean="true"></column>
|
||||
<column name="instapaper" valueBoolean="true"></column>
|
||||
<column name="buffer" valueBoolean="true"></column>
|
||||
<column name="readability" valueBoolean="true"></column>
|
||||
</update>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -6,5 +6,8 @@
|
||||
<include file="changelogs/db.changelog-1.0.xml" />
|
||||
<include file="changelogs/db.changelog-1.1.xml" />
|
||||
<include file="changelogs/db.changelog-1.2.xml" />
|
||||
<include file="changelogs/db.changelog-1.3.xml" />
|
||||
<include file="changelogs/db.changelog-1.4.xml" />
|
||||
<include file="changelogs/db.changelog-1.5.xml" />
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -6,6 +6,7 @@ global.download=تحميل
|
||||
global.link=رابط
|
||||
global.bookmark=مرجعية
|
||||
global.close=أغلق
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=اشترك
|
||||
tree.import=استورد
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=الترتيب حسب التاريخ تصاعدي / ت
|
||||
toolbar.titles_only=العناوين فقط
|
||||
toolbar.expanded_view=عرض موسع
|
||||
toolbar.mark_all_as_read=اعتبر الكل مقروء
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=العناصر الأقدم من يوم
|
||||
toolbar.mark_all_older_week=العناصر الأقدم من أسبوع
|
||||
toolbar.mark_all_older_two_weeks=العناصر الأقدم من أسبوعين
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Show feeds and categories with no unread entries
|
||||
settings.general.social_buttons=Show social sharing buttons
|
||||
settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read
|
||||
settings.appearance=Appearance
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Theme
|
||||
settings.submit_your_theme=Submit your theme
|
||||
settings.custom_css=Custom CSS
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh
|
||||
details.feed_url=Feed URL
|
||||
details.generate_api_key_first=Generate an API key in your profile first.
|
||||
details.unsubscribe=Unsubscribe
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=Category details
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=Parent category
|
||||
|
||||
profile.user_name=User name
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Generate new API key
|
||||
profile.generate_new_api_key_info=Changing password will generate a new API key
|
||||
profile.opml_export=OPML export
|
||||
profile.delete_account=Delete account
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Keyboard shortcuts
|
||||
|
||||
156
src/main/resources/i18n/ca.properties
Normal file
156
src/main/resources/i18n/ca.properties
Normal file
@@ -0,0 +1,156 @@
|
||||
global.save=Desa
|
||||
global.cancel=Cancel·la
|
||||
global.delete=Esborra
|
||||
global.required=Requerit
|
||||
global.download=Descarrega
|
||||
global.link=Enllaç
|
||||
global.bookmark=Adreça d'interès
|
||||
global.close=Tancar
|
||||
global.tags=Etiquetes
|
||||
|
||||
tree.subscribe=Subscriure
|
||||
tree.import=Importa
|
||||
tree.new_category=Nova categoria
|
||||
tree.all=Tot
|
||||
tree.starred=Destacats
|
||||
|
||||
subscribe.feed_url=URL del canal
|
||||
subscribe.feed_name=Nom del canal
|
||||
subscribe.category=Categoria
|
||||
|
||||
import.google_reader_prefix=Importaré els canals del teu
|
||||
import.google_reader_suffix= compte.
|
||||
import.google_download=O be, carrega el teu fitxer subscriptions.xml.
|
||||
import.google_download_link=Descarrega'l d'aquí.
|
||||
import.xml_file=Fitxer OPML
|
||||
|
||||
new_category.name=Nom
|
||||
new_category.parent=Arrel
|
||||
|
||||
toolbar.unread=Per llegir
|
||||
toolbar.all=Tots
|
||||
toolbar.previous_entry=Entrada prèvia
|
||||
toolbar.next_entry=Entrada següent
|
||||
toolbar.refresh=Actualitzar
|
||||
toolbar.refresh_all=Força l'actualització de tots els canals
|
||||
toolbar.sort_by_asc_desc=Ordenar per data asc/desc
|
||||
toolbar.titles_only=Només títols
|
||||
toolbar.expanded_view=Vista ampliada
|
||||
toolbar.mark_all_as_read=Marcar tots llegits
|
||||
toolbar.mark_all_older_12_hours=Ítems més vells de 12 hores
|
||||
toolbar.mark_all_older_day=Ítems més vells d'un dia
|
||||
toolbar.mark_all_older_week=Ítems més vells d'una setmana
|
||||
toolbar.mark_all_older_two_weeks=Ítems més vells de dues setmanes
|
||||
toolbar.settings=Configuració
|
||||
toolbar.profile=Perfil
|
||||
toolbar.admin=Admin
|
||||
toolbar.about=Quant a
|
||||
toolbar.logout=Desconnecta't
|
||||
toolbar.donate=Donació
|
||||
|
||||
view.entry_source=de
|
||||
view.entry_author=per
|
||||
view.error_while_loading_feed=Error carregant el canal
|
||||
view.keep_unread=Conserva com a no llegit
|
||||
view.no_unread_items=no té ítems sense llegir.
|
||||
view.mark_up_to_here=Marcar com a llegit fins aquí
|
||||
view.search_for=cercant:
|
||||
view.no_search_results=No hi ha coincidències per les paraules clau sol·licitades
|
||||
|
||||
feedsearch.hint=Introdueix una subscripció...
|
||||
feedsearch.help=Utilitza la tecla de retorn per seleccionar i les tecles de cursor per navegar.
|
||||
feedsearch.result_prefix=Les teves subscripcions:
|
||||
|
||||
settings.general=General
|
||||
settings.general.language=Idioma
|
||||
settings.general.language.contribute=Contribueix amb traduccions
|
||||
settings.general.show_unread=Mostrar canals i categories amb entrades sense llegir
|
||||
settings.general.social_buttons=Mostrar botons per compartir en xarxes socials
|
||||
settings.general.scroll_marks=A la vista ampliada si et desplaces per les entrades les marques com a llegides
|
||||
settings.appearance=Aparença
|
||||
settings.scroll_speed=Velocitat de desplaçament quan navegues entre entrades (en mil·lisegons)
|
||||
settings.scroll_speed.help=Fixa a 0 per desactivar
|
||||
settings.theme=Tema
|
||||
settings.submit_your_theme=Envia un tema
|
||||
settings.custom_css=CSS personalitzat
|
||||
|
||||
details.feed_details=Detalls del canal
|
||||
details.url=URL
|
||||
details.website=Lloc web
|
||||
details.name=Nom
|
||||
details.category=Categoria
|
||||
details.position=Posició
|
||||
details.last_refresh=Darrera actualització
|
||||
details.message=Darrer missatge d'actualització
|
||||
details.next_refresh=Propera actualització
|
||||
details.queued_for_refresh=A la cua d'actualització
|
||||
details.feed_url=URL del canal
|
||||
details.generate_api_key_first=Abans cal que generis una clau API en el teu perfil.
|
||||
details.unsubscribe=Cancel·la la subscripció
|
||||
details.unsubscribe_confirmation=Segur que vols cancel·lar la subscripció del canal?
|
||||
details.delete_category_confirmation=Segur que vols esborrar la categoria?
|
||||
details.category_details=Detalls de la categoria
|
||||
details.tag_details=Detalls de l'etiqueta
|
||||
details.parent_category=Categoria arrel
|
||||
|
||||
profile.user_name=Nom d'usuari
|
||||
profile.email=Adreça electrònica
|
||||
profile.change_password=Canvia la contrasenya
|
||||
profile.confirm_password=Confirma la contrasenya
|
||||
profile.minimum_6_chars=Mínim de 6 caracters
|
||||
profile.passwords_do_not_match=Les contrasenyes no coincideixen
|
||||
profile.api_key=Clau API
|
||||
profile.api_key_not_generated=Encara no s'ha generat
|
||||
profile.generate_new_api_key=Genera una nova clau API
|
||||
profile.generate_new_api_key_info=El canvi de contrasenya generarà una nova clau API
|
||||
profile.opml_export=Exporta OPML
|
||||
profile.delete_account=Esborra el compte
|
||||
profile.delete_account_confirmation=Vols esborrar el teu compte? No ho podràs desfer!
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Dreceres de teclat
|
||||
about.version=Versió de CommaFeed
|
||||
about.line1_prefix=CommaFeed és un projecte de codi font obert. El codi font és hostatjat a
|
||||
about.line1_suffix=.
|
||||
about.line2_prefix=Si trobes un problema, si us plau informa'n a la pàgina de problemes del
|
||||
about.line2_suffix=\ projecte.
|
||||
about.line3=Si t'agrada el projecte, pensa en fer un donatiu per recolzar el desenvolupador i per ajudar amb les despeses de l'hostatge del lloc web.
|
||||
about.line4=I pels que preferiu bitcoin, aquí teniu l'adreça
|
||||
about.goodies=Afegitons
|
||||
about.goodies.android_app=App Android
|
||||
about.goodies.subscribe_url=URL de subscripció
|
||||
about.goodies.chrome_extension=Extensió del Chrome
|
||||
about.goodies.firefox_extension=Extensió del Firefox
|
||||
about.goodies.opera_extension=Extensió de l'Opera
|
||||
about.goodies.subscribe_bookmarklet=Afegeix bookmarklet de subscripció (clica)
|
||||
about.goodies.subscribe_bookmarklet_asc=Primer els vells
|
||||
about.goodies.subscribe_bookmarklet_desc=Primer els nous
|
||||
about.goodies.next_unread_bookmarklet=Bookmarklet del proper ítem sense llegir (arrosega a la barra d'adreces d'interès)
|
||||
about.translation=Traducció
|
||||
about.translation.message=Necessitem la teva ajuda per traduir CommaFeed.
|
||||
about.translation.link=Informació per contribuir amb traduccions.
|
||||
about.announcements=Anuncis
|
||||
about.rest_api.line1=CommaFeed funciona amb JAX-RS i AngularJS. Per tant, té disponible una API REST.
|
||||
about.rest_api.link_to_documentation=Enllaç a la documentació.
|
||||
|
||||
about.shortcuts.mouse_middleclick=Clic amb el botó del mig
|
||||
about.shortcuts.open_next_entry=obrir entrada següent
|
||||
about.shortcuts.open_previous_entry=obrir entrada prèvia
|
||||
about.shortcuts.spacebar=espai/majúscula+espai
|
||||
about.shortcuts.move_page_down_up=mou la pàgina avall/amunt
|
||||
about.shortcuts.focus_next_entry=fixa el focus en l'entrada següent entrada sense obrir-la
|
||||
about.shortcuts.focus_previous_entry=fixa el focus en l'entrada prèvia sense obrir-la
|
||||
about.shortcuts.open_next_feed=obrir canal o categoria següent
|
||||
about.shortcuts.open_previous_feed=obrir canal o categoria prèvia
|
||||
about.shortcuts.open_close_current_entry=obre/tanca entrada actual
|
||||
about.shortcuts.open_current_entry_in_new_window=obrir entrada actual en una finestra nova
|
||||
about.shortcuts.open_current_entry_in_new_window_background=obrir entrada actual en una finestra nova en segon pla
|
||||
about.shortcuts.star_unstar=destacar/treure destacat a l'entrada actual
|
||||
about.shortcuts.mark_current_entry=marcar com a llegida/no llegida l'entrada actual
|
||||
about.shortcuts.mark_all_as_read=marcar totes les entrades com a llegides
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=obrir entrada en una pestanya nova i marcar com a llegida
|
||||
about.shortcuts.fullscreen=commutar el mode de pantalla completa
|
||||
about.shortcuts.font_size=incrementar/reduir la mida de la font de l'entrada actual
|
||||
about.shortcuts.go_to_all=anar a la vista de Tot
|
||||
about.shortcuts.go_to_starred=anar a la vista de Destacats
|
||||
about.shortcuts.feed_search=navegar a una subscripció introduint-ne el nom
|
||||
@@ -6,6 +6,7 @@ global.download = Stáhnout
|
||||
global.link = Odkaz
|
||||
global.bookmark = Záložky
|
||||
global.close = Zavřít
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe = Nový odběr
|
||||
tree.import = Importovat
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc = Seřadit podle nejnovějšího/nejstaršího
|
||||
toolbar.titles_only = Zobrazit jenom titulky
|
||||
toolbar.expanded_view = Rozšířený náhled
|
||||
toolbar.mark_all_as_read = Označit vše jako přečtené
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day = Položky starší než den
|
||||
toolbar.mark_all_older_week = Položky starší než týden
|
||||
toolbar.mark_all_older_two_weeks = Položky starší než dva týdny
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread = Zobrazit položky a kategorie z přečtenými pol
|
||||
settings.general.social_buttons = Zobrazit možnosti sdílení
|
||||
settings.general.scroll_marks = Skrolování v rozšířeném náhledu označí položky jako přečtené
|
||||
settings.appearance = Vzhled
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme = Motiv
|
||||
settings.submit_your_theme = Nahrát vlastní motiv
|
||||
settings.custom_css = Vlastní motiv (CSS)
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh = Ve frontě na obnovu
|
||||
details.feed_url = URL RSS zdroje
|
||||
details.generate_api_key_first = Vygenerujte si API klíč na stránce vašeho profilu.
|
||||
details.unsubscribe = Odhlásit odběr.
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details = Detail kategorie
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category = Hlavní kategorie
|
||||
|
||||
profile.user_name = Uživatelské jméno
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key = Vygenerovat nový API klíč
|
||||
profile.generate_new_api_key_info = Změnou hesla vygenerujete nový API klíč
|
||||
profile.opml_export = exportovat do formátu OPML
|
||||
profile.delete_account = Odstranit účet
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api = REST API
|
||||
about.keyboard_shortcuts = Klávesové zkratky
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Lawrlwytho
|
||||
global.link=Dolen
|
||||
global.bookmark=Nod tudalen
|
||||
global.close=Cau
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=Tanysgrifio
|
||||
tree.import=Mewnforio
|
||||
@@ -17,10 +18,10 @@ subscribe.feed_url=URL Ffrwd
|
||||
subscribe.feed_name=Enw Ffrwd
|
||||
subscribe.category=Categori
|
||||
|
||||
import.google_reader_prefix=Gadawa i mi fewnforio dy ffrydiau o dy
|
||||
import.google_reader_prefix=Gad i mi fewnforio dy ffrydiau o dy
|
||||
import.google_reader_suffix= gyfrif.
|
||||
import.google_download=Fel arall, lanlwytho dy ffeil tanysgrifiadau.xml
|
||||
import.google_download_link=Lawrlwytho fe yma.
|
||||
import.google_download=Fel arall, lanlwytha dy ffeil tanysgrifiadau.xml
|
||||
import.google_download_link=Lawrlwytha fe yma.
|
||||
import.xml_file=Ffeil OPML
|
||||
|
||||
new_category.name=Enw
|
||||
@@ -31,14 +32,15 @@ toolbar.all=Popeth
|
||||
toolbar.previous_entry=Eitem blaenorol
|
||||
toolbar.next_entry=Eitem nesaf
|
||||
toolbar.refresh=Adnewyddu
|
||||
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
|
||||
toolbar.refresh_all=Gorfodi ail-lwytho pob ffrwd
|
||||
toolbar.sort_by_asc_desc=Trefnu yn ôl dyddiad
|
||||
toolbar.titles_only=Teitlau yn unig
|
||||
toolbar.expanded_view=Golygfa estynedig
|
||||
toolbar.mark_all_as_read=Marcio popeth fel darllenwyd
|
||||
toolbar.mark_all_older_day=Eitemau sy'n hyn na diwrnod
|
||||
toolbar.mark_all_older_week=Eitemau sy'n hyn nag wythnos
|
||||
toolbar.mark_all_older_two_weeks=Eitemau sy'n hyn na phythefnos
|
||||
toolbar.expanded_view=Golwg estynedig
|
||||
toolbar.mark_all_as_read=Nodi'r cyfan fel wedi ei ddarllen
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=Eitemau hyn na diwrnod
|
||||
toolbar.mark_all_older_week=Eitemau hyn nag wythnos
|
||||
toolbar.mark_all_older_two_weeks=Eitemau hyn na phythefnos
|
||||
toolbar.settings=Gosodiadau
|
||||
toolbar.profile=Proffil
|
||||
toolbar.admin=Gweinyddwr
|
||||
@@ -46,44 +48,49 @@ toolbar.about=Ynghylch
|
||||
toolbar.logout=Allgofnodi
|
||||
toolbar.donate=Rhoddi
|
||||
|
||||
view.entry_source=from ####### Needs translation
|
||||
view.entry_author=by ####### Needs translation
|
||||
view.error_while_loading_feed=Gwall tra'n llwytho'r ffrwd
|
||||
view.keep_unread=Cadw fel heb ei darllen
|
||||
view.no_unread_items=dim eitemau heb eu darllen
|
||||
view.mark_up_to_here=Mark as read up to here ####### Needs translation
|
||||
view.search_for=searching for: ####### Needs translation
|
||||
view.no_search_results=No match found for the requested keywords ####### Needs translation
|
||||
view.entry_source=o
|
||||
view.entry_author=gan
|
||||
view.error_while_loading_feed=Gwall wrth lwytho'r ffrwd
|
||||
view.keep_unread=Parhau i'w nodi fel heb ei ddarllen
|
||||
view.no_unread_items=: Dim eitemau heb eu darllen ###### Cynnwys y colon oherwydd gystrawen y cyd-destyn
|
||||
view.mark_up_to_here=Nodi'r rhai hyd yma fel wedi eu darllen
|
||||
view.search_for=yn chwilio am:
|
||||
view.no_search_results=Ni chanfuwyd unrhyw beth gyda'r geiriau hynny
|
||||
|
||||
feedsearch.hint=Teipio tanysgrifiad...
|
||||
feedsearch.hint=Rho'r tanysgrifiad...
|
||||
feedsearch.help=Defnyddia'r dychwelwr i ddethol a saethau i lywio
|
||||
feedsearch.result_prefix=Dy danysgrifiadau:
|
||||
|
||||
settings.general=Cyffredinol
|
||||
settings.general.language=Iaith
|
||||
settings.general.language.contribute=Cyfrannu gyda chyfieithiadau
|
||||
settings.general.language.contribute=Cyfrannu drwy gyfieithu
|
||||
settings.general.show_unread=Dangos ffrydiau a chategoriau gyda dim eitemau heb eu darllen
|
||||
settings.general.social_buttons=Dangos botymau rhannu
|
||||
settings.general.scroll_marks=Mewn golygfa estynedig, sgrolio trwy eitemau yn marcio fel darllenwyd
|
||||
settings.appearance=Golygfa
|
||||
settings.general.scroll_marks=Marcio eitemau fel wedi eu darllen wrth sgrolio drwyddynt yn y golwg estynedig ###### Defnyddio gystrawen debyg i'r ddau uwch.
|
||||
settings.appearance=Golwg
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Thema
|
||||
settings.submit_your_theme=Cyflwyno dy thema
|
||||
settings.submit_your_theme=Cyflwyna dy thema
|
||||
settings.custom_css=CSS wedi'i addasu
|
||||
|
||||
details.feed_details=Manylion ffrwd
|
||||
details.url=URL
|
||||
details.website=Website ####### Needs translation
|
||||
details.website=Gwefan
|
||||
details.name=Enw
|
||||
details.category=Categori
|
||||
details.position=Safle
|
||||
details.last_refresh=Adnewyddiad diwethaf
|
||||
details.message=Last refresh message ####### Needs translation
|
||||
details.message=Neges adnewyddiad diwethaf
|
||||
details.next_refresh=Adnewyddiad nesaf
|
||||
details.queued_for_refresh=Ciwiwyd am adnewyddu
|
||||
details.queued_for_refresh=Ciwiwyd i'w adnewyddu
|
||||
details.feed_url=URL Ffrwd
|
||||
details.generate_api_key_first=Cynhyrchu allwedd API yn dy broffil yn gyntaf.
|
||||
details.generate_api_key_first=Rhaid creu allwedd API yn dy broffil yn gyntaf.
|
||||
details.unsubscribe=Dad-danysgrifio
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=Manylion categori
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=Categori rhiant
|
||||
|
||||
profile.user_name=Enw defnyddiwr
|
||||
@@ -91,59 +98,60 @@ profile.email=E-bost
|
||||
profile.change_password=Newid cyfrinair
|
||||
profile.confirm_password=Cadarnhau cyfrinair
|
||||
profile.minimum_6_chars=Isafswm 6 nod
|
||||
profile.passwords_do_not_match=Cyfrineiriau yn wahanol
|
||||
profile.api_key=allwedd API
|
||||
profile.api_key_not_generated=Heb gynhyrchu eto
|
||||
profile.generate_new_api_key=Cynhyrchu allwedd API newydd
|
||||
profile.generate_new_api_key_info=Newid cyfrinair yn cynhyrchu allwedd API newydd
|
||||
profile.passwords_do_not_match=Mae'r cyfrineiriau yn wahanol
|
||||
profile.api_key=Allwedd API
|
||||
profile.api_key_not_generated=Heb ei gynhyrchu eto
|
||||
profile.generate_new_api_key=Creu allwedd API newydd
|
||||
profile.generate_new_api_key_info=Mae newid cyfrinair yn creu allwedd API newydd
|
||||
profile.opml_export=Allforio OPML
|
||||
profile.delete_account=Dileu cyfrif
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Llwybr byr bysellfwrdd
|
||||
about.version=CommaFeed version ####### Needs translation
|
||||
about.line1_prefix=CommaFeed yn prosiect cod agored. Mae'r cod ar
|
||||
about.version=Fersiwn CommaFeed: ###### Cynnwys y colon oherwydd gystrawen y cyd-destun
|
||||
about.line1_prefix=Mae CommaFeed yn prosiect cod agored. Mae'r cod ar
|
||||
about.line1_suffix=.
|
||||
about.line2_prefix=Os wyt ti'n ffeindio problem, plis adrodda fe ar dudalen problemau o'r
|
||||
about.line2_prefix=Os wyt ti'n ffeindio problem, plîs gad wybod amdano ar dudalen problemau o'r
|
||||
about.line2_suffix=\ prosiect.
|
||||
about.line3=Os wyt ti'n hoffi'r prosiect, plis ystyried cyfraniad er mwyn cefnogi'r datblygwr a helpu gyda chynnal a chadw o'r wefan hon.
|
||||
about.line4=I'r rhai sy'n hoff o bitcoin, dyma'r gyfeiriad
|
||||
about.line3=Os wyt ti'n hoffi'r prosiect, plîs ystyria cyfrannu i gefnogi'r datblygwr a helpu gyda chynnal a chadw'r wefan hon.
|
||||
about.line4=I'r rhai sy'n hoff o Bitcoin, dyma'r cyfeiriad
|
||||
about.goodies=Goodies
|
||||
about.goodies.android_app=Android app ####### Needs translation
|
||||
about.goodies.android_app=Ap Android
|
||||
about.goodies.subscribe_url=URL Tanysgrifio
|
||||
about.goodies.chrome_extension=estyniad Chrome
|
||||
about.goodies.firefox_extension=estyniad Firefox
|
||||
about.goodies.opera_extension=estyniad Opera
|
||||
about.goodies.subscribe_bookmarklet=Ychwanegu botwm tanysgrifio (clicio)
|
||||
about.goodies.subscribe_bookmarklet_asc=Oldest first ####### Needs translation
|
||||
about.goodies.subscribe_bookmarklet_desc=Newest first ####### Needs translation
|
||||
about.goodies.subscribe_bookmarklet=Ychwanegu botwm tanysgrifio ###### Dim angen 'Click' - digon amlwg o'r cyd-destyn
|
||||
about.goodies.subscribe_bookmarklet_asc=Hynaf yn gyntaf
|
||||
about.goodies.subscribe_bookmarklet_desc=Diweddaraf yn gyntaf
|
||||
about.goodies.next_unread_bookmarklet=Botwm eitem nesaf heb ei ddarllen (llusgo i far nodau)
|
||||
about.translation=Translation
|
||||
about.translation.message=Rydym ni angen dy help i gyfieithu CommaFeed.
|
||||
about.translation=Cyfieithiad
|
||||
about.translation.message=Rydym angen dy help i gyfieithu CommaFeed.
|
||||
about.translation.link=Gweler sut i gyfrannu i gyfieithiadau.
|
||||
about.announcements=Datganiadau
|
||||
about.rest_api.line1=Mae CommaFeed wedi cael ei adeiladu ar JAX-RS ac AngularJS. Mae REST API ar gael.
|
||||
about.rest_api.line1=Adeiladir CommaFeed ar JAX-RS ac AngularJS. Mae REST API ar gael.
|
||||
about.rest_api.link_to_documentation=Dolen i'r ddogfennaeth.
|
||||
|
||||
about.shortcuts.mouse_middleclick=llygoden clic-canol
|
||||
about.shortcuts.open_next_entry=agor eitem nesaf
|
||||
about.shortcuts.open_previous_entry=agor eitem flaenorol
|
||||
about.shortcuts.spacebar=space/shift+space ####### Needs translation
|
||||
about.shortcuts.move_page_down_up=moves the page down/up ####### Needs translation
|
||||
about.shortcuts.focus_next_entry=gosod ffocws ar eitem nesaf heb ei hagor
|
||||
about.shortcuts.focus_previous_entry=gosod ffocws ar eitem flaenorol heb ei hagor
|
||||
about.shortcuts.open_next_feed=agor ffrwd neu gategori nesaf
|
||||
about.shortcuts.open_previous_feed=agor ffrwd neu gategori blaenorol
|
||||
about.shortcuts.open_close_current_entry=agor/cau eitem gyfredol
|
||||
about.shortcuts.open_current_entry_in_new_window=agor eitem gyfredol mewn ffenestr newydd
|
||||
about.shortcuts.open_current_entry_in_new_window_background=agor eitem gyfredol mewn ffenestr newydd yn y cefndir
|
||||
about.shortcuts.star_unstar=serennu/dadserennu eitem gyfredol
|
||||
about.shortcuts.mark_current_entry=marcio eitem gyfredol fel darllenwyd/heb ddarllen
|
||||
about.shortcuts.mark_all_as_read=marcio popeth fel darllenwyd
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=agor eitem mewn tab newydd a marcio fel darllenwyd
|
||||
about.shortcuts.fullscreen=toggle full screen mode ####### Needs translation
|
||||
about.shortcuts.font_size=increase/decrease font size of the current entry ####### Needs translation
|
||||
about.shortcuts.go_to_all=go to the All view ####### Needs translation
|
||||
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation
|
||||
about.shortcuts.feed_search=llywio i danysgrifiad trwy rhoi ei enw mewn
|
||||
about.shortcuts.mouse_middleclick=clic botwm canol llygoden
|
||||
about.shortcuts.open_next_entry=agor yr eitem nesaf
|
||||
about.shortcuts.open_previous_entry=agor yr eitem flaenorol
|
||||
about.shortcuts.spacebar=space/shift+space
|
||||
about.shortcuts.move_page_down_up=symud y tudalen i lawr/fyny
|
||||
about.shortcuts.focus_next_entry=newid ffocws i'r eitem nesaf heb ei hagor
|
||||
about.shortcuts.focus_previous_entry=newid ffocws i'r eitem flaenorol heb ei hagor
|
||||
about.shortcuts.open_next_feed=agor y ffrwd neu gategori nesaf
|
||||
about.shortcuts.open_previous_feed=agor y ffrwd neu gategori blaenorol
|
||||
about.shortcuts.open_close_current_entry=agor/cau yr eitem gyfredol
|
||||
about.shortcuts.open_current_entry_in_new_window=agor yr eitem gyfredol mewn ffenestr newydd
|
||||
about.shortcuts.open_current_entry_in_new_window_background=agor yr eitem gyfredol mewn ffenestr newydd yn y cefndir
|
||||
about.shortcuts.star_unstar=serennu/dadserennu'r eitem gyfredol
|
||||
about.shortcuts.mark_current_entry=marcio'r eitem gyfredol fel wedi/heb ei ddarllen
|
||||
about.shortcuts.mark_all_as_read=marcio popeth fel wedi ei ddarllen
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=agor yr eitem mewn tab newydd a'i farcio fel wedi ei ddarllen
|
||||
about.shortcuts.fullscreen=toglo'r golwg sgrin lawn
|
||||
about.shortcuts.font_size=cynyddu/lleihau maint ffont yr eitem gyfredol
|
||||
about.shortcuts.go_to_all=newid i olwg 'Popeth'
|
||||
about.shortcuts.go_to_starred=newid i olwg 'Serennwyd'
|
||||
about.shortcuts.feed_search=llywio i danysgrifiad gan roi ei enw mewn
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Hent
|
||||
global.link=Link
|
||||
global.bookmark=Bogmærke
|
||||
global.close=Luk
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=Abonner
|
||||
tree.import=Importer
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sorter efter dato ny/gammel
|
||||
toolbar.titles_only=Kun titler
|
||||
toolbar.expanded_view=Udvidet visning
|
||||
toolbar.mark_all_as_read=Marker alle som læst
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=Artikler ældere end én dag
|
||||
toolbar.mark_all_older_week=Artikler ældere end én uge
|
||||
toolbar.mark_all_older_two_weeks=Artikler ældere end to uger
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Vis abonnomenter og kategorier med læste artikler
|
||||
settings.general.social_buttons=Vis delingsknapper
|
||||
settings.general.scroll_marks=I udvidet visning, marker artikler som læste når der rulles forbi dem
|
||||
settings.appearance=Udseende
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Tema
|
||||
settings.submit_your_theme=Indsend dit tema
|
||||
settings.custom_css=Brugerdefineret CSS
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=I kø til opdatering
|
||||
details.feed_url=URL for abonnement
|
||||
details.generate_api_key_first=Generer en API nøgle i din profil først.
|
||||
details.unsubscribe=Afmeld abonnement
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=Kategori detaljer
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=Overordnet kategori
|
||||
|
||||
profile.user_name=Brugernavn
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Generer ny API nøgle
|
||||
profile.generate_new_api_key_info=Ændring af adgangskode vil generere en ny API nøgle
|
||||
profile.opml_export=OPML eksport
|
||||
profile.delete_account=Slet konto
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Tastaturgenveje
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Herunterladen
|
||||
global.link=Link
|
||||
global.bookmark=Lesezeichen
|
||||
global.close=Schließen
|
||||
global.tags=Tags
|
||||
|
||||
tree.subscribe=Abonnieren
|
||||
tree.import=Importieren
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Nach Datum sortieren (auf-/absteigend)
|
||||
toolbar.titles_only=Nur Überschriften
|
||||
toolbar.expanded_view=Ausgedehnte Ansicht
|
||||
toolbar.mark_all_as_read=Alle Artikel als gelesen markieren
|
||||
toolbar.mark_all_older_12_hours=Artikel älter als 12 Stunden
|
||||
toolbar.mark_all_older_day=Artikel älter als ein Tag
|
||||
toolbar.mark_all_older_week=Artikel älter als eine Woche
|
||||
toolbar.mark_all_older_two_weeks=Artikel älter als zwei Wochen
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Zeige Feeds und Kategorien mit ungelesenen Einträg
|
||||
settings.general.social_buttons=Zeige Buttons zum Teilen von Inhalten über soziale Netzwerke
|
||||
settings.general.scroll_marks=In der ausgedehnten Ansicht werden Artikel beim Scrollen als gelesen markiert
|
||||
settings.appearance=Aussehen
|
||||
settings.scroll_speed=Geschwindigkeit beim scrollen zwischen Einträgen (in Millisekunden)
|
||||
settings.scroll_speed.help=setze auf 0 zum deaktivieren
|
||||
settings.theme=Theme
|
||||
settings.submit_your_theme=Füg dein Theme hinzu
|
||||
settings.custom_css=Eigenes CSS
|
||||
@@ -77,13 +81,16 @@ details.name=Name
|
||||
details.category=Kategorie
|
||||
details.position=Position
|
||||
details.last_refresh=Letzte Aktualisierung
|
||||
details.message=Last refresh message ####### Needs translation
|
||||
details.message=Nachricht der letzten Aktualisierung
|
||||
details.next_refresh=Nächste Aktualisierung
|
||||
details.queued_for_refresh=Wartet auf Aktualisierung
|
||||
details.feed_url=Feed Adresse
|
||||
details.generate_api_key_first=Generiere zuerst einen API Schlüssel in deinem Profil.
|
||||
details.unsubscribe=Kündigen
|
||||
details.unsubscribe_confirmation=Bist du sicher das du diesen Feed kündigen möchtest?
|
||||
details.delete_category_confirmation=Bist du sicher das du diese Kategorie löschen möchtest?
|
||||
details.category_details=Kategoriedetails
|
||||
details.tag_details=Tag Details
|
||||
details.parent_category=Übergeordnete Kategorie
|
||||
|
||||
profile.user_name=Benutzername
|
||||
@@ -97,7 +104,8 @@ profile.api_key_not_generated=Noch nicht generiert
|
||||
profile.generate_new_api_key=Generiere einen neuen API key
|
||||
profile.generate_new_api_key_info=Das Ändern des Passwortes erzeugt einen neuen API Schlüssel
|
||||
profile.opml_export=OPML exportieren
|
||||
profile.delete_account=Lösche den Account
|
||||
profile.delete_account=Account löschen
|
||||
profile.delete_account_confirmation=Deinen Account löschen? Es gibt kein Zurück!
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Tastatur Kurzbefehle
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Download
|
||||
global.link=Link
|
||||
global.bookmark=Bookmark
|
||||
global.close=Close
|
||||
global.tags=Tags
|
||||
|
||||
tree.subscribe=Subscribe
|
||||
tree.import=Import
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sort by date asc/desc
|
||||
toolbar.titles_only=Titles only
|
||||
toolbar.expanded_view=Expanded view
|
||||
toolbar.mark_all_as_read=Mark all as read
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours
|
||||
toolbar.mark_all_older_day=Items older than a day
|
||||
toolbar.mark_all_older_week=Items older than a week
|
||||
toolbar.mark_all_older_two_weeks=Items older than two weeks
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Show feeds and categories with no unread entries
|
||||
settings.general.social_buttons=Show social sharing buttons
|
||||
settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read
|
||||
settings.appearance=Appearance
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds)
|
||||
settings.scroll_speed.help=set to 0 to disable
|
||||
settings.theme=Theme
|
||||
settings.submit_your_theme=Submit your theme
|
||||
settings.custom_css=Custom CSS
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh
|
||||
details.feed_url=Feed URL
|
||||
details.generate_api_key_first=Generate an API key in your profile first.
|
||||
details.unsubscribe=Unsubscribe
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed?
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category?
|
||||
details.category_details=Category details
|
||||
details.tag_details=Tag details
|
||||
details.parent_category=Parent category
|
||||
|
||||
profile.user_name=User name
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Generate new API key
|
||||
profile.generate_new_api_key_info=Changing password will generate a new API key
|
||||
profile.opml_export=OPML export
|
||||
profile.delete_account=Delete account
|
||||
profile.delete_account_confirmation=Delete your account? There's no turning back!
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Keyboard shortcuts
|
||||
@@ -145,4 +153,4 @@ about.shortcuts.fullscreen=toggle full screen mode
|
||||
about.shortcuts.font_size=increase/decrease font size of the current entry
|
||||
about.shortcuts.go_to_all=go to the All view
|
||||
about.shortcuts.go_to_starred=go to the Starred view
|
||||
about.shortcuts.feed_search=navigate to a subscription by entering the subscription name
|
||||
about.shortcuts.feed_search=navigate to a subscription by entering the subscription name
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Descargar
|
||||
global.link=Enlace
|
||||
global.bookmark=Marcador
|
||||
global.close=Close ####### Needs translation
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=Subscribir
|
||||
tree.import=Importar
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Ordenar por fecha asc/desc
|
||||
toolbar.titles_only=Sólo Títulos
|
||||
toolbar.expanded_view=Vista Expandida
|
||||
toolbar.mark_all_as_read=Marcar todos como leído
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=Artículos anteriores a un día
|
||||
toolbar.mark_all_older_week=Artículos más de una semana
|
||||
toolbar.mark_all_older_two_weeks=Artículos más de does semanas
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Mostrar canales y categorías sin entradas no leíd
|
||||
settings.general.social_buttons=Mostrar botones de compartir de redes sociales.
|
||||
settings.general.scroll_marks=En vista expandida, el desplazamiento por las entradas las marca como leídas
|
||||
settings.appearance=Appearance ####### Needs translation
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Theme ####### Needs translation
|
||||
settings.submit_your_theme=Submit your theme ####### Needs translation
|
||||
settings.custom_css=CSS Personalizado
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh ####### Needs translation
|
||||
details.feed_url=URL del Canal
|
||||
details.generate_api_key_first=Genera una llave API en tu perfil primero.
|
||||
details.unsubscribe=Terminar subscripción
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=Detalles de la categoría
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=Categoría principal
|
||||
|
||||
profile.user_name=Nombre de usuario
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Generar nueva llave API
|
||||
profile.generate_new_api_key_info=Al cambiar la contraseña se generará una nueva llave API
|
||||
profile.opml_export=Exportación de OPML
|
||||
profile.delete_account=Eliminar cuenta
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Atajos de teclado
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=بارگیری
|
||||
global.link=پیوند
|
||||
global.bookmark=بوکمارک
|
||||
global.close=بستن
|
||||
global.tags=برجسپها
|
||||
|
||||
tree.subscribe=مشترک شوید
|
||||
tree.import=درونریزی
|
||||
@@ -31,11 +32,12 @@ toolbar.all=همه
|
||||
toolbar.previous_entry=مطلب قبلی
|
||||
toolbar.next_entry=مطلب بعدی
|
||||
toolbar.refresh=تازهسازی
|
||||
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
|
||||
toolbar.refresh_all=مجبورکردن تازهسازی همهٔ خوراکها
|
||||
toolbar.sort_by_asc_desc=مرتبکردن بر اساس تاریخ بهصورت صعودی/نزولی
|
||||
toolbar.titles_only=فقط عنوانها
|
||||
toolbar.expanded_view=نمای گسترشیافته
|
||||
toolbar.mark_all_as_read=علامتگذاری تمامی مطالب بهعنوان خواندهشده
|
||||
toolbar.mark_all_older_12_hours=مطالب قدیمیتر از ۱۲ ساعت
|
||||
toolbar.mark_all_older_day=مطالب قدیمیتر از یک روز
|
||||
toolbar.mark_all_older_week=مطالب قدیمیتر از یک هفته
|
||||
toolbar.mark_all_older_two_weeks=مطالب قدیمی تر از چند هفته قیل
|
||||
@@ -52,8 +54,8 @@ view.error_while_loading_feed=متأسفانه، هنگام بارگیری ای
|
||||
view.keep_unread=خواندهنشده نگهدار
|
||||
view.no_unread_items=هیچ مطلب خواندهنشدهای ندارد.
|
||||
view.mark_up_to_here=تا اینجا را خواندهشده در نظر بگیر
|
||||
view.search_for=searching for: ####### Needs translation
|
||||
view.no_search_results=No match found for the requested keywords ####### Needs translation
|
||||
view.search_for=جستجو برای:
|
||||
view.no_search_results=هیج نتیجهای برای کلیدواژههای درخواستی یافت نشد
|
||||
|
||||
feedsearch.hint=نوشتن بر روی یک اشتراک...
|
||||
feedsearch.help=دکمهٔ بازگشت برای انتخاب و دکمههای جهتدار را برای ناوبری استفاده کن.
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=تنها خوراکها و دستههای ر
|
||||
settings.general.social_buttons=نشاندادن دکمههای اشتراکگذاری در شبکههای اجتماعی
|
||||
settings.general.scroll_marks=در نمای گسترشیافته، لغزیدن بر روی مطالب بهعنوان نشانهگذاری بهعنوان خواندهشده در نظر گرفتهشوند.
|
||||
settings.appearance=ظاهر
|
||||
settings.scroll_speed=سرعت لغزش هنگام گشتن بین مدخلها (به میلیثانیه)
|
||||
settings.scroll_speed.help=قراردادن به ۰ برای غیرفعالکردن
|
||||
settings.theme=پوسته
|
||||
settings.submit_your_theme=پوستهٔ خود را ارسالکنید
|
||||
settings.custom_css=سیاساس شخصیسازیشده
|
||||
@@ -77,13 +81,16 @@ details.name=نام
|
||||
details.category=دسته
|
||||
details.position=موقعیت
|
||||
details.last_refresh=آخرین بروزرسانی
|
||||
details.message=Last refresh message ####### Needs translation
|
||||
details.message=پیام آخرین تازهسازی
|
||||
details.next_refresh=بروزرسانی بعدی
|
||||
details.queued_for_refresh=منتظر برای بروزرسانی
|
||||
details.feed_url=نشانی خوراک
|
||||
details.generate_api_key_first=ابتدا یک کلید API در نمایهٔ خود ایجاد کنید.
|
||||
details.unsubscribe=لغو اشتراک
|
||||
details.unsubscribe_confirmation=مطمئنید میخواهید از این این لغو اشتراک کنید؟
|
||||
details.delete_category_confirmation=مطمئنید میخواهید این رده را حذف کنید؟
|
||||
details.category_details=جزئیات دسته
|
||||
details.tag_details=جزئیات برچسپ
|
||||
details.parent_category=ردهٔ پدر
|
||||
|
||||
profile.user_name=نام کاربری
|
||||
@@ -98,10 +105,11 @@ profile.generate_new_api_key=ایجاد کلید جدید API
|
||||
profile.generate_new_api_key_info=تغییر گذرواژه کلید API بهوجود خواهد آورد.
|
||||
profile.opml_export=خارجسازی OPML
|
||||
profile.delete_account=حذف حساب کاربری
|
||||
profile.delete_account_confirmation=حذف حسابتان؟ بازگشتی وجود ندارد!
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=کلیدهای میانبر
|
||||
about.version=CommaFeed version ####### Needs translation
|
||||
about.version=نسخهٔ کامافید
|
||||
about.line1_prefix=کامافید یک پروژه متنباز است. مخازن آن در
|
||||
about.line1_suffix=میزبانی میشود.
|
||||
about.line2_prefix=اگر شما به مسئلهای برخورده اید، لطفاً آن را در صفحه مسائل گزارش دهید
|
||||
@@ -143,7 +151,7 @@ about.shortcuts.mark_all_as_read=علامتگذاری تمامی مطالب
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=بازکردن مطلب در سربرگ جدید و علامتگذاری آن بهعنوان خواندهشده
|
||||
about.shortcuts.fullscreen=فعال/غیرفعالکردن حالت تمام صفحه
|
||||
about.shortcuts.font_size=افزایش/کاهش اندازهٔ قلم مدخل فعلی
|
||||
about.shortcuts.go_to_all=go to the All view ####### Needs translation
|
||||
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation
|
||||
about.shortcuts.go_to_all=رفتن به نمای همه
|
||||
about.shortcuts.go_to_starred=رفتن به نمای ستاره دادهشدهها
|
||||
about.shortcuts.feed_search=ناوبری به یک اشتراک با نوشتن نام اشتراک
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Lataa
|
||||
global.link=Linkki
|
||||
global.bookmark=Kirjanmerkki
|
||||
global.close=Sulje
|
||||
global.tags=Tagit
|
||||
|
||||
tree.subscribe=Tilaa syöte
|
||||
tree.import=Tuo
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Järjestä päivämäärän mukaan nousevasti/laskevast
|
||||
toolbar.titles_only=Näytä vain otsikot
|
||||
toolbar.expanded_view=Laajennettu näkymä
|
||||
toolbar.mark_all_as_read=Merkitse kaikki luetuiksi
|
||||
toolbar.mark_all_older_12_hours=12 tuntia vanhemmat otsikot
|
||||
toolbar.mark_all_older_day=Päivää vanhemmat otsikot
|
||||
toolbar.mark_all_older_week=Viikkoa vanhemmat otsikot
|
||||
toolbar.mark_all_older_two_weeks=Kahta viikkoa vanhemmat otsikot
|
||||
@@ -52,8 +54,8 @@ view.error_while_loading_feed=Virhe tilausta ladattaessa
|
||||
view.keep_unread=Pidä lukemattomana
|
||||
view.no_unread_items=ei sisällä lukemattomia otsikoita.
|
||||
view.mark_up_to_here=Merkitse luetuksi tähän asti
|
||||
view.search_for=searching for: ####### Needs translation
|
||||
view.no_search_results=No match found for the requested keywords ####### Needs translation
|
||||
view.search_for=Etsi sanoilla:
|
||||
view.no_search_results=Ei tuloksia annetuilla hakusanoilla.
|
||||
|
||||
feedsearch.hint=Kirjoita syötteen nimi...
|
||||
feedsearch.help=Siirry syötteiden välillä nuolinäppäimillä ja valitse syöte enterillä.
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Näytä syötteet ja kansiot, joissa ei ole lukemat
|
||||
settings.general.social_buttons=Näytä jakonapit
|
||||
settings.general.scroll_marks=Laajennetussa näkymässä otsikoiden selaaminen merkitsee ne luetuiksi
|
||||
settings.appearance=Ulkonäkö
|
||||
settings.scroll_speed=Vieritysnopeus otsikoiden välillä navigoidessa (millisekunneissa)
|
||||
settings.scroll_speed.help=Aseta 0 poistaaksesi vieritys käytöstä.
|
||||
settings.theme=Teema
|
||||
settings.submit_your_theme=Lähetä oma teemasi
|
||||
settings.custom_css=Oma CSS
|
||||
@@ -77,13 +81,16 @@ details.name=Nimi
|
||||
details.category=Kansio
|
||||
details.position=Paikka
|
||||
details.last_refresh=Viimeisin päivitys
|
||||
details.message=Last refresh message ####### Needs translation
|
||||
details.message=Viimeisimmän päivityksen viesti
|
||||
details.next_refresh=Seuraava päivitys
|
||||
details.queued_for_refresh=Jonossa päivitettäväksi
|
||||
details.feed_url=Syötteen osoite
|
||||
details.generate_api_key_first=Luo API-avain profiilissasi.
|
||||
details.unsubscribe=Peruuta tilaus
|
||||
details.unsubscribe_confirmation=Haluatko varmasti lopettaa tämän syötteen tilauksen?
|
||||
details.delete_category_confirmation=Haluatko varmasti poistaa tämän kansion?
|
||||
details.category_details=Kansion tiedot
|
||||
details.tag_details=Tagin tiedot
|
||||
details.parent_category=Yläkansio
|
||||
|
||||
profile.user_name=Käyttäjänimi
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Luo uusi API-avain
|
||||
profile.generate_new_api_key_info=Salasanan vaihtaminen luo uuden API-avaimen
|
||||
profile.opml_export=OPML vienti
|
||||
profile.delete_account=Poista tunnus
|
||||
profile.delete_account_confirmation=Haluatko varmasti poistaa tunnuksesi? Tätä ei voi perua!
|
||||
|
||||
about.rest_api=REST-API
|
||||
about.keyboard_shortcuts=Näppäinoikotiet
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Télécharger
|
||||
global.link=Lien
|
||||
global.bookmark=Favoris
|
||||
global.close=Fermer
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=S'abonner
|
||||
tree.import=Importer
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Trier par date croissante/décroissante
|
||||
toolbar.titles_only=Titres uniquement
|
||||
toolbar.expanded_view=Vue étendue
|
||||
toolbar.mark_all_as_read=Tout marquer comme lu
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=Articles de plus d'un jour
|
||||
toolbar.mark_all_older_week=Articles de plus d'une semaine
|
||||
toolbar.mark_all_older_two_weeks=Articles de plus d'un mois
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Afficher les flux et les catégories pour lesquels
|
||||
settings.general.social_buttons=Afficher les boutons de partage sur réseaux sociaux
|
||||
settings.general.scroll_marks=En mode de lecture étendu, marquer comme lu les éléments lorsque la fenêtre descend.
|
||||
settings.appearance=Apparence
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Thème
|
||||
settings.submit_your_theme=Soumettez votre thème.
|
||||
settings.custom_css=CSS personnelle
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=En file d'attente
|
||||
details.feed_url=URL du flux
|
||||
details.generate_api_key_first=Générez une clé API dans votre profil d'abord.
|
||||
details.unsubscribe=Se désabonner
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=Détails de la catégorie
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=Catégorie parente
|
||||
|
||||
profile.user_name=Nom
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Générer une nouvelle clé API
|
||||
profile.generate_new_api_key_info=Changer de mot de passe va générer une nouvelle clé API
|
||||
profile.opml_export=Export du fichier OPML
|
||||
profile.delete_account=Effacer le compte
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=API REST
|
||||
about.keyboard_shortcuts=Raccourcis clavier
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Descargar
|
||||
global.link=Ligazón
|
||||
global.bookmark=Marcador
|
||||
global.close=Pechar
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=Subscribir
|
||||
tree.import=Importar
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Ordenar por data asc/desc
|
||||
toolbar.titles_only=Só títulos
|
||||
toolbar.expanded_view=Vista expandida
|
||||
toolbar.mark_all_as_read=Marcar todos como lidos
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=Artigos anteriores a un día
|
||||
toolbar.mark_all_older_week=Artigos de máis de unha semana
|
||||
toolbar.mark_all_older_two_weeks=Artigos de máis de dúas semanas
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Mostrar fontes e categorías sen entradas non lidas
|
||||
settings.general.social_buttons=Mostrar botóns de compartir en redes sociais.
|
||||
settings.general.scroll_marks=En vista expandida, o desplazamento polas entradas márcaas como lidas.
|
||||
settings.appearance=Aspecto
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Decorado
|
||||
settings.submit_your_theme=Envíe o seu decorado
|
||||
settings.custom_css=CSS Personalizado
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=En cola para actualizar
|
||||
details.feed_url=URL da fonte
|
||||
details.generate_api_key_first=Antes debes xerar unha chave API no teu perfil.
|
||||
details.unsubscribe=Rematar suscripción
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=Detalles da categoría
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=Categoría principal
|
||||
|
||||
profile.user_name=Nome de usuario
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Xerar nova chave da API
|
||||
profile.generate_new_api_key_info=Ao cambiar o contrasinal xerarase unha nova chave API
|
||||
profile.opml_export=Exportación de OPML
|
||||
profile.delete_account=Eliminar conta
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Atallos de teclado
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=جیرأکش
|
||||
global.link=خال
|
||||
global.bookmark=بوکمارک
|
||||
global.close=دَوَستن
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=مشترک ببید
|
||||
tree.import=درینأدأن
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=تاریخˇ سر دچئن
|
||||
toolbar.titles_only=خالی تیتران
|
||||
toolbar.expanded_view=واشاده نما
|
||||
toolbar.mark_all_as_read=همهته مطالبه چاکون بخانده
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=یک روز پیشترˇ مطالب
|
||||
toolbar.mark_all_older_week=یک هفته پیشترˇ مطالب
|
||||
toolbar.mark_all_older_two_weeks=چن هفته پیشترˇ مطالب
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=تنها خوراکها و دستههای ر
|
||||
settings.general.social_buttons=نشاندادن دکمههای اشتراکگذاری در شبکههای اجتماعی
|
||||
settings.general.scroll_marks=در نمای گسترشیافته، لغزیدن بر روی مطالب بهعنوان نشانهگذاری بهعنوان خواندهشده در نظر گرفتهشوند.
|
||||
settings.appearance=ظاهر
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=پوسته
|
||||
settings.submit_your_theme=شیمه پوستهٰ اوسه کونید
|
||||
settings.custom_css=سیاساس شخصیسازیشده
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=منتظر برای بروزرسانی
|
||||
details.feed_url=نشانی خوراک
|
||||
details.generate_api_key_first=ابتدا یک کلید API در نمایهٔ خود ایجاد کنید.
|
||||
details.unsubscribe=لغو اشتراک
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=جرگه جزئیات
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=پئرˇ جرگه
|
||||
|
||||
profile.user_name=کاربری نام
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=تازه کلید چاگودن API
|
||||
profile.generate_new_api_key_info=رمزه عوضأگودن API کلیده چاکونه.
|
||||
profile.opml_export=برینأدأن OPML
|
||||
profile.delete_account=کاربری حسابه پاکودن
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=وئر زئنˇ کلیدان
|
||||
|
||||
@@ -1,149 +1,157 @@
|
||||
global.save=Mentés
|
||||
global.cancel=Mégsem
|
||||
global.delete=Törlés
|
||||
global.required=Szükséges
|
||||
global.download=Letöltés
|
||||
global.link=Link
|
||||
global.bookmark=Könyvjelző
|
||||
global.close=Bezár
|
||||
|
||||
tree.subscribe=Feliratkozás
|
||||
tree.import=Importálás
|
||||
tree.new_category=Új kategória
|
||||
tree.all=Összes
|
||||
tree.starred=Csillagozott
|
||||
|
||||
subscribe.feed_url=Hírcsatorna URL
|
||||
subscribe.feed_name=Hírcsatorna neve
|
||||
subscribe.category=Kategória
|
||||
|
||||
import.google_reader_prefix=Engedd meg, hogy importáljuk a hírcsatornáidat a
|
||||
import.google_reader_suffix= fiókjából.
|
||||
import.google_download=Alternatívaként, feltöltheti a subscriptions.xml fájlt.
|
||||
import.google_download_link=Letöltheti innen.
|
||||
import.xml_file=OPML Fájl
|
||||
|
||||
new_category.name=Név
|
||||
new_category.parent=Szülő
|
||||
|
||||
toolbar.unread=Olvasatlan
|
||||
toolbar.all=Összes
|
||||
toolbar.previous_entry=Előző elem
|
||||
toolbar.next_entry=Következő elem
|
||||
toolbar.refresh=Frissítés
|
||||
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
|
||||
toolbar.sort_by_asc_desc=Rendezés időrend szerint
|
||||
toolbar.titles_only=Csak cím
|
||||
toolbar.expanded_view=Részletes nézet
|
||||
toolbar.mark_all_as_read=Az összes megjelölése olvasottként
|
||||
toolbar.mark_all_older_day=Régebbiek, mint egy nap
|
||||
toolbar.mark_all_older_week=Régebbiek, mint egy hét
|
||||
toolbar.mark_all_older_two_weeks=Régebbiek, mint két hét
|
||||
toolbar.settings=Beállítások
|
||||
toolbar.profile=Profil
|
||||
toolbar.admin=Admin
|
||||
toolbar.about=Névjegy
|
||||
toolbar.logout=Kilépés
|
||||
toolbar.donate=Anyagi támogatás
|
||||
|
||||
view.entry_source=from ####### Needs translation
|
||||
view.entry_author=by ####### Needs translation
|
||||
view.error_while_loading_feed=Hiba történt ennek a hírcsatornának a betöltésekor
|
||||
view.keep_unread=Megtartása olvasatlanként
|
||||
view.no_unread_items=nincsen olvasatlan eleme.
|
||||
view.mark_up_to_here=Mark as read up to here ####### Needs translation
|
||||
view.search_for=searching for: ####### Needs translation
|
||||
view.no_search_results=No match found for the requested keywords ####### Needs translation
|
||||
|
||||
feedsearch.hint=Keressen a hírcsatornák között...
|
||||
feedsearch.help=Használja a nyíl billentyűket a navigáláshoz, az enter-t a kiválasztáshoz.
|
||||
feedsearch.result_prefix=Az ön feliratkozásai:
|
||||
|
||||
settings.general=Általános
|
||||
settings.general.language=Nyelv
|
||||
settings.general.language.contribute=Segítsen a fordításban
|
||||
settings.general.show_unread=Mutassa azokat a hírcsatornákat és kategóriákat amelyekben nincsen olvasatlan bejegyzés
|
||||
settings.general.social_buttons=Mutassa a közösségi oldalak megosztás gombjait
|
||||
settings.general.scroll_marks=Kiterjesztett nézetben, görgetéssel olvasottként jelöli meg a bejegyzést
|
||||
settings.appearance=Megjelenés
|
||||
settings.theme=Téma
|
||||
settings.submit_your_theme=Küldje el a témáját
|
||||
settings.custom_css=Saját CSS
|
||||
|
||||
details.feed_details=Hírcsatorna részletei
|
||||
details.url=URL
|
||||
details.website=Website ####### Needs translation
|
||||
details.name=Név
|
||||
details.category=Kategória
|
||||
details.position=Position ####### Needs translation
|
||||
details.last_refresh=Utolsó frissítés
|
||||
details.message=Last refresh message ####### Needs translation
|
||||
details.next_refresh=Következő frissítés
|
||||
details.queued_for_refresh=Frissítésre vár
|
||||
details.feed_url=Hírcsatorna URL
|
||||
details.generate_api_key_first=A profiljában először egy API kulcsot kell generálnia.
|
||||
details.unsubscribe=Leiratkozás
|
||||
details.category_details=Kategória részletei
|
||||
details.parent_category=Szülő kategória
|
||||
|
||||
profile.user_name=Felhasználói név
|
||||
profile.email=E-mail
|
||||
profile.change_password=Jelszó megváltoztatás
|
||||
profile.confirm_password=Jelszó megerősítése
|
||||
profile.minimum_6_chars=Legalább 8 karakter
|
||||
profile.passwords_do_not_match=A jelszavak nem egyeznek
|
||||
profile.api_key=API kulcs
|
||||
profile.api_key_not_generated=Még nincsen generálva
|
||||
profile.generate_new_api_key=Új API kulcs generálása
|
||||
profile.generate_new_api_key_info=A jelszó megváltoztatása új API kulcsot generál
|
||||
profile.opml_export=OPML exportálása
|
||||
profile.delete_account=Fiók törlése
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Gyorsbillentyűk
|
||||
about.version=CommaFeed version ####### Needs translation
|
||||
about.line1_prefix=A CommaFeed egy nyílt forrású projekt. A forrás megtalálható a
|
||||
about.line1_suffix=oldalán.
|
||||
about.line2_prefix=Ha hibába ütközik, kérjük jelentse azt a
|
||||
about.line2_suffix=projekt oldalán.
|
||||
about.line3=Ha tetszik önnek ez a szolgáltatás, akkor kérjük támogassa a fejlesztőket és, hogy fentarthassák a weboldalt.
|
||||
about.line4=Akik jobban szeretnék az oldalt bitcon-nal támogatni, itt a cím
|
||||
about.goodies=Hasznos dolgok
|
||||
about.goodies.android_app=Android app ####### Needs translation
|
||||
about.goodies.subscribe_url=Feliratkozás az URL-re
|
||||
about.goodies.chrome_extension=Chrome bővítmény
|
||||
about.goodies.firefox_extension=Firefox kiterjesztés
|
||||
about.goodies.opera_extension=Opera kiterjesztés
|
||||
about.goodies.subscribe_bookmarklet=Feliratkozás bookmarklet hozzáadása (klikkeléssel)
|
||||
about.goodies.subscribe_bookmarklet_asc=Oldest first ####### Needs translation
|
||||
about.goodies.subscribe_bookmarklet_desc=Newest first ####### Needs translation
|
||||
about.goodies.next_unread_bookmarklet=Következő olvasatlan elem bookmarklet (húzza fel a könyvjelzősávba)
|
||||
about.translation=Fordítás
|
||||
about.translation.message=Segítségét kérjük a CommaFeed fordításához.
|
||||
about.translation.link=Nézze meg, hogyan tud segíteni ebben.
|
||||
about.announcements=Bejelentések
|
||||
about.rest_api.line1=A CommaFeed a JAX-RS-re és az AngularJS-re épül. Ezért a RESTA API elérhető.
|
||||
about.rest_api.link_to_documentation=Link a dokumentációhoz.
|
||||
|
||||
about.shortcuts.mouse_middleclick=középső egérgomb
|
||||
about.shortcuts.open_next_entry=következő hír megnyitása
|
||||
about.shortcuts.open_previous_entry=előző hír megnyitása
|
||||
about.shortcuts.spacebar=space/shift+space ####### Needs translation
|
||||
about.shortcuts.move_page_down_up=moves the page down/up ####### Needs translation
|
||||
about.shortcuts.focus_next_entry=megnyitás nélkül fókuszál a övetkező elemre
|
||||
about.shortcuts.focus_previous_entry=megnyitás nélkül fókuszál az előző elemre
|
||||
about.shortcuts.open_next_feed=a következő hírcsatorna vagy kategória megnyitása
|
||||
about.shortcuts.open_previous_feed=az előző hírcsatorna vagy kategória megnyitása
|
||||
about.shortcuts.open_close_current_entry=a jelenlegi elem megnyitása/bezárása
|
||||
about.shortcuts.open_current_entry_in_new_window=a jelenlegi elem megnyitása új ablakban
|
||||
about.shortcuts.open_current_entry_in_new_window_background=a jelenlegi elem megnyitása a háttérben, új ablakban
|
||||
about.shortcuts.star_unstar=hírelem csillagozása
|
||||
about.shortcuts.mark_current_entry=elem megjelölése olvasottként
|
||||
about.shortcuts.mark_all_as_read=az összes elem megjelölése olvasottként
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=elem megnyitása új fülön és megjelölése olvasottként
|
||||
about.shortcuts.fullscreen=toggle full screen mode ####### Needs translation
|
||||
about.shortcuts.font_size=increase/decrease font size of the current entry ####### Needs translation
|
||||
about.shortcuts.go_to_all=go to the All view ####### Needs translation
|
||||
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation
|
||||
about.shortcuts.feed_search=név szerinti keresés a hírcsatornák között
|
||||
|
||||
global.save=Mentés
|
||||
global.cancel=Mégsem
|
||||
global.delete=Törlés
|
||||
global.required=Szükséges
|
||||
global.download=Letöltés
|
||||
global.link=Link
|
||||
global.bookmark=Könyvjelző
|
||||
global.close=Bezár
|
||||
global.tags=Címkék
|
||||
|
||||
tree.subscribe=Feliratkozás
|
||||
tree.import=Importálás
|
||||
tree.new_category=Új kategória
|
||||
tree.all=Összes
|
||||
tree.starred=Csillagozott
|
||||
|
||||
subscribe.feed_url=Hírcsatorna URL
|
||||
subscribe.feed_name=Hírcsatorna neve
|
||||
subscribe.category=Kategória
|
||||
|
||||
import.google_reader_prefix=Engedd meg, hogy importáljuk a hírcsatornáidat a
|
||||
import.google_reader_suffix= fiókjából.
|
||||
import.google_download=Alternatívaként, feltöltheti a subscriptions.xml fájlt.
|
||||
import.google_download_link=Letöltheti innen.
|
||||
import.xml_file=OPML Fájl
|
||||
|
||||
new_category.name=Név
|
||||
new_category.parent=Szülő
|
||||
|
||||
toolbar.unread=Olvasatlan
|
||||
toolbar.all=Összes
|
||||
toolbar.previous_entry=Előző elem
|
||||
toolbar.next_entry=Következő elem
|
||||
toolbar.refresh=Frissítés
|
||||
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
|
||||
toolbar.sort_by_asc_desc=Rendezés időrend szerint
|
||||
toolbar.titles_only=Csak cím
|
||||
toolbar.expanded_view=Részletes nézet
|
||||
toolbar.mark_all_as_read=Az összes megjelölése olvasottként
|
||||
toolbar.mark_all_older_12_hours=Régebbiek 12 óránál
|
||||
toolbar.mark_all_older_day=Régebbiek, mint egy nap
|
||||
toolbar.mark_all_older_week=Régebbiek, mint egy hét
|
||||
toolbar.mark_all_older_two_weeks=Régebbiek, mint két hét
|
||||
toolbar.settings=Beállítások
|
||||
toolbar.profile=Profil
|
||||
toolbar.admin=Admin
|
||||
toolbar.about=Névjegy
|
||||
toolbar.logout=Kilépés
|
||||
toolbar.donate=Anyagi támogatás
|
||||
|
||||
view.entry_source=forrás
|
||||
view.entry_author=szerző
|
||||
view.error_while_loading_feed=Hiba történt ennek a hírcsatornának a betöltésekor
|
||||
view.keep_unread=Megtartása olvasatlanként
|
||||
view.no_unread_items=nincsen olvasatlan eleme.
|
||||
view.mark_up_to_here=Megjelölés olvasottnak eddig
|
||||
view.search_for=keresés erre:
|
||||
view.no_search_results=Nem található semmi erre a keresett szóra
|
||||
|
||||
feedsearch.hint=Keressen a hírcsatornák között...
|
||||
feedsearch.help=Használja a nyíl billentyűket a navigáláshoz, az enter-t a kiválasztáshoz.
|
||||
feedsearch.result_prefix=Az ön feliratkozásai:
|
||||
|
||||
settings.general=Általános
|
||||
settings.general.language=Nyelv
|
||||
settings.general.language.contribute=Segítsen a fordításban
|
||||
settings.general.show_unread=Mutassa azokat a hírcsatornákat és kategóriákat amelyekben nincsen olvasatlan bejegyzés
|
||||
settings.general.social_buttons=Mutassa a közösségi oldalak megosztás gombjait
|
||||
settings.general.scroll_marks=Kiterjesztett nézetben, görgetéssel olvasottként jelöli meg a bejegyzést
|
||||
settings.appearance=Megjelenés
|
||||
settings.scroll_speed=A görgetés sebessége, amikor a cikkek között navigál (miliszekundumban)
|
||||
settings.scroll_speed.help=Írjon be 0-át a letiltáshoz
|
||||
settings.theme=Téma
|
||||
settings.submit_your_theme=Küldje el a témáját
|
||||
settings.custom_css=Saját CSS
|
||||
|
||||
details.feed_details=Hírcsatorna részletei
|
||||
details.url=URL
|
||||
details.website=Weboldal
|
||||
details.name=Név
|
||||
details.category=Kategória
|
||||
details.position=Pozició
|
||||
details.last_refresh=Utolsó frissítés
|
||||
details.message=Utolsó frissítési üzenet
|
||||
details.next_refresh=Következő frissítés
|
||||
details.queued_for_refresh=Frissítésre vár
|
||||
details.feed_url=Hírcsatorna URL
|
||||
details.generate_api_key_first=A profiljában először egy API kulcsot kell generálnia.
|
||||
details.unsubscribe=Leiratkozás
|
||||
details.unsubscribe_confirmation=Biztos, hogy le akar iratkozni errről a csatornáról?
|
||||
details.delete_category_confirmation=Biztos, hog törölni akarja ezt a kategóriát?
|
||||
details.category_details=Kategória részletei
|
||||
details.tag_details=Címke részletei
|
||||
details.parent_category=Szülő kategória
|
||||
|
||||
profile.user_name=Felhasználói név
|
||||
profile.email=E-mail
|
||||
profile.change_password=Jelszó megváltoztatás
|
||||
profile.confirm_password=Jelszó megerősítése
|
||||
profile.minimum_6_chars=Legalább 8 karakter
|
||||
profile.passwords_do_not_match=A jelszavak nem egyeznek
|
||||
profile.api_key=API kulcs
|
||||
profile.api_key_not_generated=Még nincsen generálva
|
||||
profile.generate_new_api_key=Új API kulcs generálása
|
||||
profile.generate_new_api_key_info=A jelszó megváltoztatása új API kulcsot generál
|
||||
profile.opml_export=OPML exportálása
|
||||
profile.delete_account=Fiók törlése
|
||||
profile.delete_account_confirmation=Törli a fiókját? Innen már nincs visszatérés!
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Gyorsbillentyűk
|
||||
about.version=CommaFeed verzió
|
||||
about.line1_prefix=A CommaFeed egy nyílt forrású projekt. A forrás megtalálható a
|
||||
about.line1_suffix=oldalán.
|
||||
about.line2_prefix=Ha hibába ütközik, kérjük jelentse azt a
|
||||
about.line2_suffix=projekt oldalán.
|
||||
about.line3=Ha tetszik önnek ez a szolgáltatás, akkor kérjük támogassa a fejlesztőket és, hogy fentarthassák a weboldalt.
|
||||
about.line4=Akik jobban szeretnék az oldalt bitcon-nal támogatni, itt a cím
|
||||
about.goodies=Hasznos dolgok
|
||||
about.goodies.android_app=Android alkalmazás
|
||||
about.goodies.subscribe_url=Feliratkozás az URL-re
|
||||
about.goodies.chrome_extension=Chrome bővítmény
|
||||
about.goodies.firefox_extension=Firefox kiterjesztés
|
||||
about.goodies.opera_extension=Opera kiterjesztés
|
||||
about.goodies.subscribe_bookmarklet=Feliratkozás bookmarklet hozzáadása (klikkeléssel)
|
||||
about.goodies.subscribe_bookmarklet_asc=Régebbiek először
|
||||
about.goodies.subscribe_bookmarklet_desc=Újak először
|
||||
about.goodies.next_unread_bookmarklet=Következő olvasatlan elem bookmarklet (húzza fel a könyvjelzősávba)
|
||||
about.translation=Fordítás
|
||||
about.translation.message=Segítségét kérjük a CommaFeed fordításához.
|
||||
about.translation.link=Nézze meg, hogyan tud segíteni ebben.
|
||||
about.announcements=Bejelentések
|
||||
about.rest_api.line1=A CommaFeed a JAX-RS-re és az AngularJS-re épül. Ezért a RESTA API elérhető.
|
||||
about.rest_api.link_to_documentation=Link a dokumentációhoz.
|
||||
|
||||
about.shortcuts.mouse_middleclick=középső egérgomb
|
||||
about.shortcuts.open_next_entry=következő hír megnyitása
|
||||
about.shortcuts.open_previous_entry=előző hír megnyitása
|
||||
about.shortcuts.spacebar=szóköz/shift+szóköz
|
||||
about.shortcuts.move_page_down_up=fel/le lépkedhet az oldalon
|
||||
about.shortcuts.focus_next_entry=megnyitás nélkül fókuszál a övetkező elemre
|
||||
about.shortcuts.focus_previous_entry=megnyitás nélkül fókuszál az előző elemre
|
||||
about.shortcuts.open_next_feed=a következő hírcsatorna vagy kategória megnyitása
|
||||
about.shortcuts.open_previous_feed=az előző hírcsatorna vagy kategória megnyitása
|
||||
about.shortcuts.open_close_current_entry=a jelenlegi elem megnyitása/bezárása
|
||||
about.shortcuts.open_current_entry_in_new_window=a jelenlegi elem megnyitása új ablakban
|
||||
about.shortcuts.open_current_entry_in_new_window_background=a jelenlegi elem megnyitása a háttérben, új ablakban
|
||||
about.shortcuts.star_unstar=hírelem csillagozása
|
||||
about.shortcuts.mark_current_entry=elem megjelölése olvasottként
|
||||
about.shortcuts.mark_all_as_read=az összes elem megjelölése olvasottként
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=elem megnyitása új fülön és megjelölése olvasottként
|
||||
about.shortcuts.fullscreen=teljes képernyős mód bekapcsolása
|
||||
about.shortcuts.font_size=a jelenlegi elemnél növeli/csökkenti a betűméretet
|
||||
about.shortcuts.go_to_all=átkapcsol az Összes nézetre
|
||||
about.shortcuts.go_to_starred=átkapcsol a Csillagozott nézetre
|
||||
about.shortcuts.feed_search=név szerinti keresés a hírcsatornák között
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Download
|
||||
global.link=Link
|
||||
global.bookmark=Segnalibro
|
||||
global.close=Chiudi
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=Iscriviti
|
||||
tree.import=Importa
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sort by date asc/desc
|
||||
toolbar.titles_only=Solo titoli
|
||||
toolbar.expanded_view=Espandi
|
||||
toolbar.mark_all_as_read=Contrassegna tutto come già letto
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=Elementi più vecchi di un giorno
|
||||
toolbar.mark_all_older_week=Elementi più vecchi di una settimana
|
||||
toolbar.mark_all_older_two_weeks=Elementi più vecchi di due settimane
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Show feeds and categories with no unread entries
|
||||
settings.general.social_buttons=Visualizza i social button
|
||||
settings.general.scroll_marks=Marca come letto quando scorri
|
||||
settings.appearance=Appearance ####### Needs translation
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Tema
|
||||
settings.submit_your_theme=Sottoponi il tuo tema
|
||||
settings.custom_css=Css modificato
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=In attesa per l'aggiornamento
|
||||
details.feed_url=Feed URL
|
||||
details.generate_api_key_first=Generate an API key in your profile first.
|
||||
details.unsubscribe=Annulla l"'"iscrizione
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=Dettagli categoria
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=Parent category
|
||||
|
||||
profile.user_name=User name
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Genera una nuova chiave API
|
||||
profile.generate_new_api_key_info=Cambiando la password sarà generata una nuova chiave API
|
||||
profile.opml_export=Esporta OPML
|
||||
profile.delete_account=Elimina account
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Scorciatoie da tastiera
|
||||
|
||||
157
src/main/resources/i18n/ja.properties
Normal file
157
src/main/resources/i18n/ja.properties
Normal file
@@ -0,0 +1,157 @@
|
||||
global.save=保存
|
||||
global.cancel=取り消し
|
||||
global.delete=削除
|
||||
global.required=Required
|
||||
global.download=ダウンロード
|
||||
global.link=リンク
|
||||
global.bookmark=ブックマーク
|
||||
global.close=閉じる
|
||||
global.tags=タグ
|
||||
|
||||
tree.subscribe=購読
|
||||
tree.import=インポート
|
||||
tree.new_category=新しいカテゴリー
|
||||
tree.all=全て
|
||||
tree.starred=スター付
|
||||
|
||||
subscribe.feed_url=フィードURL
|
||||
subscribe.feed_name=フィード名
|
||||
subscribe.category=カテゴリー
|
||||
|
||||
import.google_reader_prefix=Googleアカウントからフィードを
|
||||
import.google_reader_suffix=インポートします。
|
||||
import.google_download=または、お持ちのsubscriptions.xmlファイルをアップロードします。
|
||||
import.google_download_link=このリンクからダウンロードして下さい。
|
||||
import.xml_file=OPMLファイル
|
||||
|
||||
new_category.name=名前
|
||||
new_category.parent=親カテゴリー
|
||||
|
||||
toolbar.unread=未読
|
||||
toolbar.all=全て
|
||||
toolbar.previous_entry=前のエントリー
|
||||
toolbar.next_entry=次のエントリー
|
||||
toolbar.refresh=更新
|
||||
toolbar.refresh_all=全てのフィードを更新
|
||||
toolbar.sort_by_asc_desc=昇順/降順にソート
|
||||
toolbar.titles_only=タイトルのみ
|
||||
toolbar.expanded_view=拡張ビュー
|
||||
toolbar.mark_all_as_read=全て既読にする
|
||||
toolbar.mark_all_older_12_hours=12時間以上前のアイテム
|
||||
toolbar.mark_all_older_day=前日より前のアイテム
|
||||
toolbar.mark_all_older_week=1週間以上前のアイテム
|
||||
toolbar.mark_all_older_two_weeks=2週間以上前のアイテム
|
||||
toolbar.settings=設定
|
||||
toolbar.profile=Profile
|
||||
toolbar.admin=管理者
|
||||
toolbar.about=About
|
||||
toolbar.logout=ログアウト
|
||||
toolbar.donate=寄付
|
||||
|
||||
view.entry_source= より
|
||||
view.entry_author= 著者
|
||||
view.error_while_loading_feed=フィード読み込み中にエラーが発生しました。
|
||||
view.keep_unread=未読として保持
|
||||
view.no_unread_items=未読アイテムはありません。
|
||||
view.mark_up_to_here=ここまで既読
|
||||
view.search_for=検索:
|
||||
view.no_search_results=検索結果はありません。
|
||||
|
||||
feedsearch.hint=購読フィードを入力...
|
||||
feedsearch.help=Enterキーで選択、矢印キーで移動します。
|
||||
feedsearch.result_prefix=見つかった購読フィード:
|
||||
|
||||
settings.general=一般
|
||||
settings.general.language=言語
|
||||
settings.general.language.contribute=翻訳に貢献する
|
||||
settings.general.show_unread=未読エントリーのないフィードとカテゴリーを表示
|
||||
settings.general.social_buttons=共有ボタンを表示
|
||||
settings.general.scroll_marks=拡張ビューではエントリーのスクロールで既読にする
|
||||
settings.appearance=外観
|
||||
settings.scroll_speed=エントリー間のスクロールスピード(ミリ秒)
|
||||
settings.scroll_speed.help=0に設定すると無効になります
|
||||
settings.theme=テーマ
|
||||
settings.submit_your_theme=テーマを登録する
|
||||
settings.custom_css=カスタムCSS
|
||||
|
||||
details.feed_details=フィードの詳細
|
||||
details.url=URL
|
||||
details.website=Webサイト
|
||||
details.name=名前
|
||||
details.category=カテゴリー
|
||||
details.position=位置
|
||||
details.last_refresh=最終更新
|
||||
details.message=最終更新メッセージ
|
||||
details.next_refresh=次回更新
|
||||
details.queued_for_refresh=更新キュー
|
||||
details.feed_url=フィードURL
|
||||
details.generate_api_key_first=最初にあなたのAPIキーを生成して下さい。
|
||||
details.unsubscribe=購読解除
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=カテゴリー詳細
|
||||
details.tag_details=タグ詳細
|
||||
details.parent_category=親カテゴリー
|
||||
|
||||
profile.user_name=ユーザ名
|
||||
profile.email=E-mail
|
||||
profile.change_password=パスワードの変更
|
||||
profile.confirm_password=変更パスワードの確認
|
||||
profile.minimum_6_chars=6文字以上
|
||||
profile.passwords_do_not_match=パスワードが一致しません
|
||||
profile.api_key=APIキー
|
||||
profile.api_key_not_generated=APIキーが生成されていません
|
||||
profile.generate_new_api_key=新しいAPIキーを生成
|
||||
profile.generate_new_api_key_info=パスワードの変更は新しいAPIキーが生成されます
|
||||
profile.opml_export=OPMLエクスポート
|
||||
profile.delete_account=アカウント削除
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=キーボードショートカット
|
||||
about.version=CommaFeedバージョン
|
||||
about.line1_prefix=CommaFeedはオープンソースプロジェクトです。ソースは
|
||||
about.line1_suffix=にホスティングされています。
|
||||
about.line2_prefix=もし問題を登録したい場合、
|
||||
about.line2_suffix=プロジェクトのissuesページに報告して下さい。
|
||||
about.line3=このプロジェクトを気に入った場合、開発者やWebサイトの運営コストをサポートするための寄付を検討して下さいね。
|
||||
about.line4=Bitcoinなら寄付できる方、アドレスはこちらです。
|
||||
about.goodies=Goodies
|
||||
about.goodies.android_app=Androidアプリ
|
||||
about.goodies.subscribe_url=購読URL
|
||||
about.goodies.chrome_extension=Chrome拡張
|
||||
about.goodies.firefox_extension=Firefox拡張
|
||||
about.goodies.opera_extension=Opera拡張
|
||||
about.goodies.subscribe_bookmarklet=購読ブックマークレットを追加(クリック)
|
||||
about.goodies.subscribe_bookmarklet_asc=古い順
|
||||
about.goodies.subscribe_bookmarklet_desc=新しい順
|
||||
about.goodies.next_unread_bookmarklet=次の未読アイテムブックマークレット(ブックマークバーへドラッグ)
|
||||
about.translation=翻訳
|
||||
about.translation.message=CommaFeedの翻訳に助けが必要です!
|
||||
about.translation.link=どうやって翻訳に貢献できるか見て下さい。
|
||||
about.announcements=Announcements
|
||||
about.rest_api.line1=CommaFeedはJAX-RSとAngularJSを使用しているので、REST APIも利用可能です。
|
||||
about.rest_api.link_to_documentation=ドキュメントへのリンク
|
||||
|
||||
about.shortcuts.mouse_middleclick=中クリック
|
||||
about.shortcuts.open_next_entry=次のエントリーを開く
|
||||
about.shortcuts.open_previous_entry=前のエントリーを開く
|
||||
about.shortcuts.spacebar=space/shift+space
|
||||
about.shortcuts.move_page_down_up=ページ移動
|
||||
about.shortcuts.focus_next_entry=次のエントリーを開かずにフォーカス移動
|
||||
about.shortcuts.focus_previous_entry=前のエントリーを開かずにフォーカス移動
|
||||
about.shortcuts.open_next_feed=次のフィード/カテゴリーを開く
|
||||
about.shortcuts.open_previous_feed=前のフィード/カテゴリーを開く
|
||||
about.shortcuts.open_close_current_entry=現在のエントリーを開く/閉じる
|
||||
about.shortcuts.open_current_entry_in_new_window=現在のエントリーを新しいウィンドウで開く
|
||||
about.shortcuts.open_current_entry_in_new_window_background=現在のエントリーを新しいバックグラウンドウィンドウで開く
|
||||
about.shortcuts.star_unstar=現在のエントリーにスターを付ける/解除する
|
||||
about.shortcuts.mark_current_entry=現在のエントリーを既読/未読にする
|
||||
about.shortcuts.mark_all_as_read=全エントリーを既読にする
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=エントリーを既読にして新しいタブで開く
|
||||
about.shortcuts.fullscreen=フルスクリーントグル
|
||||
about.shortcuts.font_size=現在のエントリーのフォントサイズを大きく/小さくする
|
||||
about.shortcuts.go_to_all=All viewに変更する
|
||||
about.shortcuts.go_to_starred=スター付きviewに変更する
|
||||
about.shortcuts.feed_search=購読画面(subscription nameの入力)に移動する
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Download ####### Needs translation
|
||||
global.link=Link ####### Needs translation
|
||||
global.bookmark=Bookmark ####### Needs translation
|
||||
global.close=Close ####### Needs translation
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=구독
|
||||
tree.import=임포트
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sort by date asc/desc ####### Needs translation
|
||||
toolbar.titles_only=Titles only ####### Needs translation
|
||||
toolbar.expanded_view=Expanded view ####### Needs translation
|
||||
toolbar.mark_all_as_read=읽음표시
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=Items older than a day ####### Needs translation
|
||||
toolbar.mark_all_older_week=Items older than a week ####### Needs translation
|
||||
toolbar.mark_all_older_two_weeks=Items older than two weeks ####### Needs translation
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=안읽은 항목들이 있는 피드와 카테고
|
||||
settings.general.social_buttons=소셜미디아 버튼들 보여주기
|
||||
settings.general.scroll_marks=Expanded View에서 스크롤하면 항목들을 읽음으로 저장하기
|
||||
settings.appearance=Appearance ####### Needs translation
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Theme ####### Needs translation
|
||||
settings.submit_your_theme=Submit your theme ####### Needs translation
|
||||
settings.custom_css=커스톰 CSS
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh ####### Needs translation
|
||||
details.feed_url=피드 유알엘
|
||||
details.generate_api_key_first=당신의 프로필을 위해 API Key를 먼저 생성하세요.
|
||||
details.unsubscribe=주소 삭제
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=카테고리 세부
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=부모 카테고리
|
||||
|
||||
profile.user_name=사용자 이름
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=API Key 생성하기
|
||||
profile.generate_new_api_key_info=비밀번호를 변경하면 새로운 API Key가 생성됩니다.
|
||||
profile.opml_export=OPML export ####### Needs translation
|
||||
profile.delete_account=프로필삭제
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=단축기
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
ar=العربية
|
||||
ca=Català
|
||||
en=English
|
||||
es=Español
|
||||
de=Deutsch
|
||||
@@ -7,6 +8,7 @@ fr=Français
|
||||
gl=Galician
|
||||
glk=گیلکی
|
||||
hu=Magyar
|
||||
ja=日本語
|
||||
ko=한국어
|
||||
nl=Nederlands
|
||||
nb=Norsk (bokmål)
|
||||
|
||||
@@ -6,6 +6,7 @@ global.download=Muat turun
|
||||
global.link=Pautan
|
||||
global.bookmark=Bookmark
|
||||
global.close=Tutup
|
||||
global.tags=Tags ####### Needs translation
|
||||
|
||||
tree.subscribe=Langgan
|
||||
tree.import=Import
|
||||
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Aturkan mengikut tarikh (baru/lama)
|
||||
toolbar.titles_only=Tajuk sahaja
|
||||
toolbar.expanded_view=Wide view
|
||||
toolbar.mark_all_as_read=Tanda kesemuanya telah dibaca
|
||||
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
|
||||
toolbar.mark_all_older_day=Lebih lama daripada sehari
|
||||
toolbar.mark_all_older_week=Lebih lama daripada seminggu
|
||||
toolbar.mark_all_older_two_weeks=Lebih lama daripada dua minggu
|
||||
@@ -66,6 +68,8 @@ settings.general.show_unread=Tunjuk semua feed dan kategori yang telah dibaca
|
||||
settings.general.social_buttons=Tunjuk social sharing
|
||||
settings.general.scroll_marks=Dalam wide view, tanda item dibaca ketika scrolling
|
||||
settings.appearance=Rupa
|
||||
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
|
||||
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
|
||||
settings.theme=Tema
|
||||
settings.submit_your_theme=Muat naik tema anda
|
||||
settings.custom_css=Custom CSS
|
||||
@@ -83,7 +87,10 @@ details.queued_for_refresh=Diaturkan untuk refresh
|
||||
details.feed_url=URL Feed
|
||||
details.generate_api_key_first=Janakan API key dalam profil anda dahulu.
|
||||
details.unsubscribe=Hentikan langganan
|
||||
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
|
||||
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
|
||||
details.category_details=Butir-butir kategori
|
||||
details.tag_details=Tag details ####### Needs translation
|
||||
details.parent_category=Kategori induk
|
||||
|
||||
profile.user_name=User name
|
||||
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Jana API key baru
|
||||
profile.generate_new_api_key_info=Pertukaran kata laluan akan menjanakan API key yang baru
|
||||
profile.opml_export=Export OPML
|
||||
profile.delete_account=Padam akaun
|
||||
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Pintasan papan kekunci
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user