Compare commits

..

290 Commits
2.0.3 ... 2.3.0

Author SHA1 Message Date
Athou
535f947f88 2.3.0 release 2016-03-02 11:21:10 +01:00
Athou
f27e243cc4 readme update 2016-03-02 11:20:57 +01:00
Athou
6a699ed5f1 Merge pull request #782 from badarg/improve-spanish-translation
Improved spanish translation.
2016-02-15 12:34:02 +01:00
badarg
9c1f5efab5 Improved spanish translation.
Fixed grammar, orthography, sundry mistakes, and added missing strings.
2016-02-14 20:36:28 +01:00
Athou
6b7ce56f6b Merge pull request #779 from canoine/patch-1
Update fr.js
2016-01-20 06:31:20 +01:00
canoine
b76ee4a2d0 Update fr.js
Translation of the sentences that were still in english.
Some stylistic, grammatical and orthographic corrections.
2016-01-20 01:55:09 +01:00
Athou
b444a74a44 change user agent so that self hosted instances don't point to commafeed.com 2016-01-18 10:01:05 +01:00
Athou
d43820cc82 change npm install log level to info 2016-01-08 16:12:15 +01:00
Athou
e74e8fe1c2 Merge pull request #769 from matrixik/patch-1
[add] css ids for toolbar
2015-11-30 10:09:33 +01:00
Dobrosław Żybort
9eb6e8ec27 [add] css ids for toolbar 2015-11-30 09:48:20 +01:00
Athou
fae94d3696 jdbc drivers upgrade 2015-11-25 16:50:42 +01:00
Athou
68e5ed64c9 dropwizard upgrade 2015-11-25 16:49:18 +01:00
Athou
f912d3b8bd swagger upgrade 2015-11-25 16:44:28 +01:00
Athou
fc03d2ee91 Merge pull request #768 from giucal/master
Little contribution to the Italian translation
2015-11-24 06:54:19 +01:00
Giuseppe Calabrese
523b2b8db4 Fixed a couple typos.
I felt free to change the `filtering_expression_help` and make it less ambiguous.
2015-11-23 22:30:11 +01:00
Giuseppe Calabrese
d547e9b6d7 Fixed some translation. 2015-11-23 21:22:49 +01:00
Athou
71efc9f854 fix #766 2015-11-07 23:45:04 +01:00
Athou
4f289f7467 Merge pull request #764 from JmsBnz/patch-2
Update it.js
2015-11-03 11:31:13 +01:00
JmsBnz
02ef8bee71 Update it.js
Some translation fixes
2015-10-31 11:38:21 +01:00
Athou
ff5c1b00d7 Merge pull request #762 from ebraminio/patch-4
Add magnet links support
2015-10-21 15:44:46 +02:00
ebraminio
30264be311 Add magnet links support 2015-10-19 23:45:53 +03:30
Athou
8ea44ab8c7 Merge pull request #761 from ebraminio/patch-3
Use correct characters for ru lang
2015-10-10 06:28:30 +02:00
ebraminio
1b8ff7ca61 Use correct characters for ru lang
It is similar but works better on some font setups
2015-10-10 00:45:50 +03:30
Athou
f00a066c22 languages should start with an uppercase (fix #759) 2015-09-01 16:58:18 +02:00
Athou
859cf468aa add openjdk ppa on ubuntu lts (fix #756) 2015-08-27 12:45:37 +02:00
Athou
5b486a917b dropwizard upgrade 2015-08-26 17:50:20 +02:00
Athou
9ace6b70f0 frontend-maven-plugin upgrade (#747) 2015-08-22 03:01:18 +02:00
Athou
447029ae70 skip jsoup parsing for null strings (#754) 2015-08-19 11:10:02 +02:00
Athou
8ac52690fd fix wrong parameter name (#752) 2015-08-18 13:14:35 +02:00
Athou
6934b2bd27 remove println 2015-08-17 16:30:27 +02:00
Athou
6647e4fcd4 additional timer metrics 2015-08-14 12:58:55 +02:00
Athou
21710f55f3 proxy image enclosures too (#750) 2015-08-07 10:07:42 +02:00
Athou
27bd9a7489 fix test (#750) 2015-08-06 10:42:04 +02:00
Athou
630d37125c hide enclosure if already in entry content (fix #748) 2015-08-05 09:45:07 +02:00
Athou
9424237534 cleanup 2015-07-27 14:38:52 +02:00
Athou
cba3fbeb5f generate swagger file before running gulp (#746) 2015-07-25 08:48:56 +02:00
Athou
58778ccf43 dropwizard upgrade 2015-07-10 09:05:01 +02:00
Athou
6c61d47d78 swagger.json no longer generated at runtime 2015-07-09 16:08:31 +02:00
Athou
35e02f9d98 querydsl upgrade 2015-07-09 12:34:54 +02:00
Athou
58c1650863 make mvnw executable 2015-07-05 14:10:04 +02:00
Athou
9b14ffa14c update readme to use maven wrapper 2015-07-04 17:10:16 +02:00
Athou
96c09bf4cd ending quote missing 2015-07-04 17:05:03 +02:00
Athou
737cec744a fix mvnw on windows 2015-07-04 17:02:23 +02:00
Athou
13ed92bb94 add maven-wrapper 2015-07-04 09:06:47 +02:00
Athou
076594c78e force filter expression to lowercase 2015-06-29 12:56:17 +02:00
Athou
b6b1b4ebbe fix build, 2.0.1 has been deleted (https://github.com/dlmanning/gulp-sass/issues/305) 2015-06-26 15:05:10 +02:00
Athou
4007f37492 maven 3.3 setup instructions 2015-06-26 14:13:14 +02:00
Athou
532d671feb upgrade frontend-maven-plugin (fix #743) 2015-06-26 14:01:42 +02:00
Athou
fed7a1ac84 rewrite query using subqueries 2015-06-25 11:20:50 +02:00
Athou
ddfd170ea8 make eclipse mars happy 2015-06-24 11:42:01 +02:00
Athou
bae5c67dfa dropwizard upgrade 2015-06-19 08:41:23 +02:00
Athou
84f51603fb bump version 2015-06-19 08:33:43 +02:00
Athou
f73ddc03e9 readme update 2015-06-19 08:33:12 +02:00
Athou
a16d9877cc 2.2.0 release 2015-06-19 08:26:08 +02:00
Athou
c24e9e083c changelog update 2015-06-19 08:25:12 +02:00
Athou
101602c6f6 return the correct media type for favicons (fix #736) 2015-06-08 15:53:17 +02:00
Athou
18a7bd1fd1 check both urls for favicon 2015-06-08 15:53:16 +02:00
Athou
dfbd556bb8 Revert "angularjs 1.4.0 upgrade", fixes android navigation (fix #739) 2015-06-08 06:59:05 +02:00
Athou
040cdde8ba jcl-over-slf4j is already included 2015-06-04 15:05:23 +02:00
Athou
06373480ae various upgrades 2015-06-04 14:55:36 +02:00
Athou
5713a78f2e exclude guava-jdk5 as guava is already included 2015-06-04 14:52:15 +02:00
Athou
b9f2f17a24 angularjs 1.4.0 upgrade 2015-06-04 14:46:59 +02:00
Athou
9adc993472 fix youtube favicon fetching 2015-06-04 12:29:22 +02:00
Athou
dcd5f3d529 preserve filter when a feed is rearranged with drag&drop 2015-06-04 11:50:33 +02:00
Athou
18e70a0e6b fix opml import without head element (fix #737) 2015-06-02 21:21:49 +02:00
Athou
5ad57d1608 upgrade minify-css 2015-06-02 21:10:55 +02:00
Athou
74eaf48ceb upgrade gulp-minify-css and remove workaround (#734) 2015-06-02 08:45:33 +02:00
Athou
30bb0cb291 Merge pull request #735 from RavenB/master
fix for #734 and allowing version bump of minicss
2015-06-01 08:54:57 +02:00
RavenB
b50e6b93bd Merge pull request #2 from RavenB/RavenB-patch-2
version bump to latest
2015-05-31 20:39:49 +02:00
RavenB
a0b5a1462d version bump to latest
minicss in gulpfile was corrected gulp-minify can now be safely upgraded.
2015-05-31 20:39:32 +02:00
RavenB
4910f93c94 Merge pull request #1 from RavenB/RavenB-patch-1-1
fix for #734 and allowing version bump of minicss
2015-05-31 20:37:41 +02:00
RavenB
4a52bd0cb7 fix for #734 and allowing version bump of minicss 2015-05-31 20:35:43 +02:00
Athou
b0bfb73952 fix #734 2015-05-31 09:53:45 +02:00
Athou
69d049a69a Merge pull request #732 from LelixSuper/patch-1
Update it.js (Italian language)
2015-05-31 09:41:17 +02:00
Athou
7d75153362 instructions on how to update an existing openshift installation 2015-05-27 10:14:43 +02:00
Athou
748bfa31ae build dependencies upgrade 2015-05-27 09:51:46 +02:00
Athou
e7d995edbc node/npm upgrade 2015-05-27 09:48:28 +02:00
LelixSuper
a144fb2e48 Update it.js
Translated all the strings in Italian language
2015-05-23 12:33:31 +02:00
Athou
7521013e11 fix openshift start script (#731) 2015-05-21 11:58:48 +02:00
Athou
c6321fc6b2 rename gulp task 2015-05-21 11:56:52 +02:00
Athou
7d92d5d096 Merge pull request #730 from ebraminio/patch-2
Make manifest.json accessible
2015-05-21 11:55:30 +02:00
ebraminio
ab201d5016 Make manifest.json accessible
https://commafeed.com/manifest.json is not accessible currently
2015-05-17 01:16:05 +04:30
Athou
efa38d5ee9 store and expose entry categories (#727) 2015-05-03 09:19:45 +02:00
Athou
e8769d09a8 update readme (fix #724) 2015-05-03 09:08:49 +02:00
Athou
a216444825 upgrade dependencies 2015-04-29 12:45:05 +02:00
Athou
fee3e10e6b return a more explicit error message (fix #723) 2015-04-24 16:14:18 +02:00
Athou
4d71a8f3c2 rewrite query using not exists 2015-04-23 08:55:47 +02:00
Athou
fc104b0b01 fix eclipse infinite build loop 2015-04-22 13:04:16 +02:00
Athou
3dcb351b36 Merge pull request #722 from LelixSuper/patch-1
Update it.js
2015-04-15 16:37:53 +02:00
LelixSuper
600d05d08f Update it.js
Partial update of the translation of the Italian language.
I council calls only Italian to translate, because the Italian language is complex enough.
2015-04-15 16:19:34 +02:00
Athou
6b6ff70ad3 dropwizard release 2015-04-08 15:15:49 +02:00
Athou
891f660738 Merge pull request #720 from JmsBnz/patch-1
Update it.js
2015-04-07 19:08:28 +02:00
JmsBnz
6901b9b728 Update it.js
Some adjustment to the italian translation, not complete yet.
2015-04-07 18:13:23 +02:00
Athou
c7f211a7f8 ubuntu LTS has maven 3.0.5 and this upgrade does not add much 2015-04-03 12:45:28 +02:00
Athou
c48ea1152c fix jenkins build 2015-04-01 22:05:39 +02:00
Athou
f5d0eb94b4 verious upgrades 2015-04-01 19:46:48 +02:00
Athou
cebeef04a0 remove one to many relationships 2015-03-30 11:31:58 +02:00
Athou
3e77a83ca6 unnecessary optimization 2015-03-30 10:55:42 +02:00
Athou
c872b335e7 correctly remove user and all its dependencies 2015-03-30 10:14:40 +02:00
Athou
cc1e173552 remove role link from user 2015-03-30 09:43:44 +02:00
Athou
35e0567705 fix exception when saving role for a non-existing user 2015-03-30 08:32:57 +02:00
Athou
fb2add305e fix build 2015-03-29 21:34:47 +02:00
Athou
74d4c18c4c keep only remove cascading 2015-03-29 21:28:36 +02:00
Athou
da3ce07485 fix documentation 2015-03-24 16:40:20 +01:00
Athou
c7ab179a9e cleanup 2015-03-19 13:01:03 +01:00
Athou
6fd11fcd56 don't load the feed, just update it 2015-03-19 12:35:38 +01:00
Athou
3966cf165b log exceptions in trace level only 2015-03-19 12:32:42 +01:00
Athou
0b2ada5d1c depend directly on httpclient 2015-03-19 11:22:56 +01:00
Athou
4278101bbe maven plugins update 2015-03-19 11:14:44 +01:00
Athou
8b43af49fc enable batch inserts/updates 2015-03-19 11:13:34 +01:00
Athou
6e29e8426b various js upgrades 2015-03-08 13:11:10 +01:00
Athou
af11d3c771 h2 upgrade 2015-03-06 10:36:54 +01:00
Athou
e5c5af4d57 dropwizard 0.8.0 released 2015-03-06 10:36:53 +01:00
Athou
3dbdf5adf2 smaller transactions under heavy load 2015-03-05 22:21:21 +01:00
Athou
4d7a030b70 dependencies upgrade 2015-03-01 13:19:33 +01:00
Athou
3351262dd7 swagger upgrade 2015-02-26 06:39:24 +01:00
Athou
5ec4377502 pgsql driver upgrade, no longer ships with a slf4j implementation 2015-02-26 06:39:18 +01:00
Athou
9c8402c3a5 dropwizard upgrade (jetty vulnerability fix) 2015-02-26 06:38:55 +01:00
Athou
928a45e48e skip entries that were deleted by the cleanup task 2015-02-25 15:14:01 +01:00
Athou
1d088c5eae create transaction only when needed 2015-02-23 15:33:52 +01:00
Athou
cdcf81ab7c preserve order during opml export (#707) 2015-02-23 14:52:22 +01:00
Athou
9f196bafe9 preserve order during opml import (#707) 2015-02-23 14:52:21 +01:00
Athou
5c9e1406a1 correctly display error message when email is not found during password recovery 2015-02-22 07:18:55 +01:00
Athou
0b42e00b29 exclude commons logging as it's handled by logback 2015-02-20 17:10:02 +01:00
Athou
88b98a138f swagger upgrade 2015-02-20 17:06:33 +01:00
Athou
136c37885d dropwizard upgrade 2015-02-20 16:41:20 +01:00
Athou
812988b31a log entries deleted 2015-02-20 08:51:33 +01:00
Athou
191680a01b correctly set timeout on query 2015-02-19 13:00:47 +01:00
Athou
467d1a754d distinct not needed as we don't have duplicates in the id column 2015-02-19 12:43:40 +01:00
Athou
d1973922cd check for empty lists too 2015-02-19 08:30:44 +01:00
Athou
3b7689975d unit of work not needed here 2015-02-18 12:30:34 +01:00
Athou
3386a71c5e smaller cleanup batches 2015-02-18 12:03:28 +01:00
Athou
7bb65a5e76 fix indentation 2015-02-17 16:51:37 +01:00
Athou
f3a9c8e0e2 jdom and mockito upgrades 2015-02-17 09:06:07 +01:00
Athou
22861ca8d0 Merge pull request #703 from ebraminio/master
Add fullscreen Android/iOS app capability
2015-02-15 09:21:20 +01:00
Athou
19118ea241 exclude slf4j simple 2015-02-13 09:34:34 +01:00
Ebrahim Byagowi
4a9dc7249f Add fullscreen Android/iOS app capability
* https://developer.chrome.com/multidevice/android/installtohomescreen
* https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
2015-02-12 23:46:43 +00:00
Athou
5dad9c2eb8 dependencies upgrade 2015-02-11 19:16:42 +01:00
Athou
d6b35b00b9 postgresql tweaks 2015-02-11 10:35:14 +01:00
Athou
fda8ab500b Merge pull request #702 from xmgz/patch-3
Actualizado gl.js Galego (gl, gl_ES)
2015-01-23 09:02:36 +01:00
Xose M.
66df421de2 Actualizado gl.js Galego (gl, gl_ES)
novas entradas respecto a versión antiga
2015-01-23 08:36:22 +01:00
Athou
33c62f08ca this is not needed 2015-01-21 15:42:09 +01:00
Athou
b660602809 node and npm upgrade 2015-01-21 15:42:03 +01:00
Athou
6dfce2ca30 use recent version of maven 2015-01-21 15:10:45 +01:00
Athou
655e20e99e use openshift nexus mirror 2015-01-21 14:20:19 +01:00
Athou
f2b80bdc08 build on openshift using jdk8 2015-01-21 12:31:54 +01:00
Athou
10af873fa5 build deps upgrade 2015-01-20 09:55:21 +01:00
Athou
d87a5b14f8 dropwizard upgrade 2015-01-18 08:04:02 +01:00
Athou
b87a18b993 various upgrades 2015-01-12 10:14:40 +01:00
Athou
c4185034e4 urlAfterRedirect was always null (#699) 2015-01-12 09:57:30 +01:00
Athou
9d64426b00 add testcase (#699) 2015-01-12 09:56:55 +01:00
Athou
c81cc8bea4 fix relative url detection (#699) 2015-01-12 09:56:34 +01:00
Athou
90e680d6be upgrade jdom to 2.0.5 for performance reasons (https://github.com/hunterhacker/jdom/issues/112) 2015-01-06 09:27:18 +01:00
Athou
04c0833111 run bower during maven build 2015-01-05 14:56:31 +01:00
Athou
06151eab3b dependencies upgrade 2015-01-04 06:59:21 +01:00
Athou
3dcb8590f6 project modernized, no longer needs to scan during every build 2015-01-04 06:59:21 +01:00
Athou
a9b313aa4a Merge pull request #697 from bizimakin/patch-2
Update README.md
2014-12-28 15:34:26 +01:00
Akın Ayturan
1f2e35060b Update README.md
doomrobo/CommaFeed-Android-Reader has been discontinued. - ( https://github.com/doomrobo/CommaFeed-Android-Reader)
2014-12-28 16:14:49 +02:00
Athou
a96862fffa more tests 2014-12-18 16:31:35 +01:00
Athou
68cb8e194d rewrite using lambda 2014-12-18 14:09:15 +01:00
Athou
c164926c54 rewrite using lambda 2014-12-18 10:13:44 +01:00
Athou
de7516116d deps upgrade 2014-12-18 09:37:47 +01:00
Athou
fccfe5b088 Merge pull request #695 from bizimakin/patch-1
Update tr.js
2014-12-16 08:45:35 +01:00
Akın Ayturan
23aa5fa0a3 Update tr.js
when i see on the site :)
2014-12-16 09:08:14 +02:00
Athou
d384c0a141 mockito upgrade 2014-12-15 16:02:54 +01:00
Athou
18058c2a36 slf4j upgrade 2014-12-15 16:02:47 +01:00
Athou
71727202f3 h2 upgrade 2014-12-15 16:01:27 +01:00
Athou
eee0b949de coherent logging string 2014-12-15 15:54:39 +01:00
Athou
3cbbb67b0c memory optimizations 2014-12-15 11:20:27 +01:00
Athou
7879f66e78 test for html entities 2014-12-15 11:20:26 +01:00
Athou
c14ac37495 Merge pull request #694 from bizimakin/patch-1
Update tr.js
2014-12-14 12:47:48 +01:00
Akın Ayturan
73a77183aa Update tr.js 2014-12-14 13:09:35 +02:00
Akın Ayturan
09cfa21091 Update tr.js
a little change
2014-12-14 13:07:33 +02:00
Athou
c193571ece Merge pull request #693 from bizimakin/patch-2
Update README.md
2014-12-13 14:04:40 +01:00
Athou
04bc92b071 Merge pull request #692 from bizimakin/patch-1
Update tr.js
2014-12-13 14:04:19 +01:00
Akın Ayturan
94e58a449c Update README.md 2014-12-13 12:17:37 +02:00
Akın Ayturan
9d044195aa Update tr.js 2014-12-13 12:08:26 +02:00
Athou
caff34cc3b small perf boost 2014-12-12 15:48:40 +01:00
Athou
34c5c0b1f7 fix #691 (reopens #685) 2014-12-12 15:17:36 +01:00
Athou
906801e13c runtime deps upgrade 2014-12-12 12:03:07 +01:00
Athou
dad4c6b866 build deps upgrade 2014-12-12 11:58:51 +01:00
Athou
090462022f call bower prune before calling bower install 2014-12-12 11:56:07 +01:00
Athou
cbf9f65fb4 use released version of zocial 2014-12-12 11:53:20 +01:00
Athou
5a493cd55d rewrite using lambda 2014-12-12 11:25:31 +01:00
Athou
dfc204ef05 trim some swagger fat 2014-12-12 11:19:45 +01:00
Athou
56c6e2d29c fix modernizer warnings 2014-12-12 11:05:29 +01:00
Athou
db03dd12a0 use java8 optional 2014-12-12 11:05:28 +01:00
Athou
6c67e6363a return charset instead of stirng 2014-12-12 11:05:28 +01:00
Athou
e2888beb4c add modernizer plugin 2014-12-12 11:05:27 +01:00
Athou
bba9166885 use lombok 2014-12-12 10:55:36 +01:00
Athou
504e4eab3e rewrite using lambdas 2014-12-12 10:55:35 +01:00
Athou
2e475c35cc unit of work refactoring 2014-12-12 08:59:33 +01:00
Athou
ccf18758fb now requires java8 (fix #688) 2014-12-12 08:31:18 +01:00
Athou
68f9852790 2.1.0 release 2014-12-12 08:27:49 +01:00
Athou
d0150de003 discourage h2 usage (fix #689) 2014-12-11 15:55:40 +01:00
Athou
e2b792335b split orphan cleanup task in two 2014-12-11 08:46:09 +01:00
Athou
ece38c9e59 only add enclosure if there's one 2014-12-11 08:32:48 +01:00
Athou
a19b5090bf expose enclosures in generated feeds (fix #690) 2014-12-10 18:54:35 +01:00
Athou
e4b3c35892 extract version as a variable 2014-12-09 15:30:48 +01:00
Athou
4b229a759a frontend plugin update 2014-12-09 10:57:52 +01:00
Athou
1e9e42ac48 liquibase upgrade for faster app startup, remove when 3.3.1 is included in dropwizard 2014-12-09 10:35:24 +01:00
Athou
245a48f66e guice update 2014-12-09 10:27:56 +01:00
Athou
e6d8397550 optimize opml export (fix #687) 2014-12-08 08:07:20 +01:00
Athou
d59bd43846 querydsl update 2014-12-06 17:21:14 +01:00
Athou
c1579c83c7 junit update 2014-12-06 17:21:06 +01:00
Athou
4d782e60ad readme update 2014-12-05 08:22:00 +01:00
Athou
c702f47927 more config checks on startup 2014-12-04 14:04:03 +01:00
Athou
9110cfd923 new setting for deleting old entries (fix #524) 2014-12-04 10:27:07 +01:00
Athou
e40dd14bbf reduce default sql logging level for dev 2014-12-04 09:12:08 +01:00
Athou
90aaae9959 Merge pull request #686 from ebraminio/master
Support Android 5.0 bits
2014-12-03 18:15:28 +01:00
Ebrahim Byagowi
e81dda0fa8 Support Android 5.0 bits
http://updates.html5rocks.com/2014/11/Support-for-theme-color-in-Chrome-39-for-Android
2014-12-03 16:57:35 +00:00
Athou
f93796d036 fix for "handshake alert: unrecognized_name" (fix #685) 2014-12-01 16:19:30 +01:00
Athou
d06359cb81 remove deb package creation (fix #684) 2014-12-01 16:02:41 +01:00
Athou
8b68fb578f swagger upgrade (no longer includes logback.xml) 2014-11-27 22:04:13 +01:00
Athou
cca300e419 simpler support for single quotes (#681) 2014-11-26 15:25:38 +01:00
Athou
77c3ec0bbe support for single quotes (#681) 2014-11-26 14:19:08 +01:00
Athou
ed81fc576a fix tagging issues 2014-11-25 10:30:14 +01:00
Athou
435fcb9669 unused method 2014-11-24 16:27:32 +01:00
Athou
9020d95b62 better character encoding detection 2014-11-24 14:55:20 +01:00
Athou
84d7a501d4 directory name change since it's devicejs on bower 2014-11-24 14:17:38 +01:00
Athou
e65dd49d69 use released version of device.js 2014-11-24 13:13:43 +01:00
Athou
a705cbe6c2 instantiate filtering service only once 2014-11-24 12:56:55 +01:00
Athou
60b8af3860 typos 2014-11-24 12:53:13 +01:00
Athou
9ac4187aa8 let the module decide what tasks are registered 2014-11-24 12:49:54 +01:00
Athou
6419d29489 demo account creation is now skipped by default 2014-11-23 13:27:34 +01:00
Athou
4684e43f42 relax opml import conditions (#677) 2014-11-22 22:29:56 +01:00
Athou
a477c9fa6d fix error display 2014-11-22 11:53:53 +01:00
Athou
d1be331f99 handle nulls correctly 2014-11-22 11:53:46 +01:00
Athou
cbc792d406 use the old id generator as it's the one we were using before dropwizard 2014-11-21 16:50:20 +01:00
Athou
0313c5c560 fix placeholder display 2014-11-21 12:04:59 +01:00
Athou
18aa2fcd92 fix subscription error handling 2014-11-21 10:06:48 +01:00
Athou
10461941d7 fix nightsky loadingbar color 2014-11-21 10:01:34 +01:00
Athou
e6050219bc log runtime exceptions 2014-11-20 14:50:24 +01:00
Athou
81481c37fe use daemon threads 2014-11-20 12:26:48 +01:00
Athou
5ea92a7d18 jedis upgrade 2014-11-18 17:27:06 +01:00
Athou
f40630aced upgrade dev dependencies 2014-11-18 12:43:23 +01:00
Athou
81850acdfe enable livereload 2014-11-18 12:24:36 +01:00
Athou
6819d5aa8b highlight unread categories 2014-11-18 12:13:13 +01:00
Athou
2aef4e5d05 typo 2014-11-17 16:06:31 +01:00
Athou
6d4d2c3e7e lang() is deprecated 2014-11-17 16:05:19 +01:00
Athou
87bcaa4731 nightsky theme tweaks 2014-11-17 15:37:19 +01:00
Athou
5d2378f291 swagger ui upgrade 2014-11-17 14:44:05 +01:00
Athou
253507d14b momentjs upgrade 2014-11-17 14:37:48 +01:00
Athou
548fb7099b ui router upgrade 2014-11-17 14:28:24 +01:00
Athou
0dd7c777ee bootstrap upgrade 2014-11-17 14:24:32 +01:00
Athou
6812bf2388 jquery upgrade 2014-11-17 14:24:27 +01:00
Athou
12bcbfa9f7 angular upgrade 2014-11-17 14:24:16 +01:00
Athou
b5dfd371d9 typo 2014-11-17 06:44:11 +01:00
Athou
e09d7fb103 new dark theme 'nightsky' 2014-11-14 16:10:06 +01:00
Athou
0fe3afe254 remove underline on focused link 2014-11-12 09:05:25 +01:00
Athou
db50d50c19 remove dotted lines on focused links 2014-11-11 20:45:55 +01:00
Athou
691bdb1512 force g++ install (fix #673) 2014-11-11 08:55:29 +01:00
Athou
d50b712bca commons-codec upgrade 2014-11-10 10:23:01 +01:00
Athou
3b68e4f32b changelog update 2014-11-10 10:18:20 +01:00
Athou
259b9a90dd clarify help text 2014-11-10 10:16:15 +01:00
Athou
f4c5fd7eb4 wrap class cast exceptions 2014-11-10 10:14:19 +01:00
Athou
3cd42d03f0 reduce horizontal form height 2014-11-10 10:10:41 +01:00
Athou
3497b82e8c liquibase upgrade seems to change checksum here 2014-11-10 10:10:25 +01:00
Athou
15a24e4e75 fix components layout 2014-11-10 10:04:29 +01:00
Athou
96837f908e refactor into a service 2014-11-10 09:49:59 +01:00
Athou
4ea5ebbf9e Merge branch 'entry-filtering' 2014-11-10 09:20:26 +01:00
Athou
281e015376 help block added 2014-11-10 09:12:10 +01:00
Athou
5825a16aff changelog update 2014-11-09 21:25:03 +01:00
Athou
2586a8c433 dropwizard upgrade 2014-11-09 21:13:39 +01:00
Athou
9f7c9c3428 unused code cleanup 2014-11-07 15:02:38 +01:00
Athou
9790ba735b facebook favicon fetcher 2014-11-07 15:02:11 +01:00
Athou
e3dbcac9fb dependencies upgrade 2014-11-07 10:59:04 +01:00
Athou
1c99929429 exclude terms from search (fix #666) 2014-11-07 10:53:49 +01:00
Athou
9b2cdbbb18 fix readability icon position on safari (fix #651) 2014-11-07 10:26:58 +01:00
Athou
928cf1220e config placeholder 2014-11-07 09:41:01 +01:00
Athou
c0557856a3 configurable "from" address (fix #664) 2014-11-07 09:38:55 +01:00
Athou
97c2cc3d15 unit tests for opml importer (#636) 2014-11-07 09:00:53 +01:00
Athou
a94ef980bb cannot loop forever 2014-11-07 08:46:09 +01:00
Athou
eea0c24d2b engine is now strict and throws exceptions instead of returning nulls 2014-11-04 16:25:34 +01:00
Athou
c8fded3c56 don't crash if we cannot evaluate the filter 2014-11-04 16:19:50 +01:00
Athou
8f2ba5e186 initial ui for entry filtering 2014-11-04 16:01:37 +01:00
Athou
5ce2823d0b strip html tags 2014-11-04 15:22:43 +01:00
Athou
a0c70d326f class not used anymore 2014-11-04 15:09:45 +01:00
Athou
5f28fd4114 initial support for entry filtering 2014-11-04 15:07:12 +01:00
Athou
7151db0909 Merge pull request #662 from Athou/dw8
dropwizard upgrade
2014-11-01 11:20:44 +01:00
Athou
e82888f8f3 Merge pull request #661 from Hubcapp/master
Mark Read button now respects filters
2014-11-01 11:18:50 +01:00
Tyler Gebhard
4fb60a6ec6 The "Mark Read" button now only marks the visible entries as read, instead of the entire feed regardless of what keywords you've entered. This should allow better management of RSS feeds, if you don't want to ever look at any content which has certain keywords in it. 2014-11-01 02:10:31 -04:00
Hubcapp
27f22f6094 Merge pull request #3 from Athou/master
back up to date
2014-11-01 02:01:24 -04:00
Athou
7497a0151a upgrade instructions 2014-10-29 08:46:24 +01:00
Athou
41f133afb1 liquibade upgrade fix 2014-10-29 08:46:24 +01:00
Athou
4b15ecbc1b more comments 2014-10-29 08:46:23 +01:00
Athou
6498130850 remove app.contextPath setting 2014-10-29 08:46:23 +01:00
Athou
24bd1121af commons-lang upgrade to v3 2014-10-29 08:46:23 +01:00
Athou
3cccf741d6 dropwizard upgrade to 0.8.0 2014-10-29 08:46:10 +01:00
Athou
0a2d2c3f43 fix travis build 2014-10-29 08:42:32 +01:00
169 changed files with 3520 additions and 1904 deletions

View File

@@ -1,6 +1,23 @@
#!/bin/bash #!/bin/bash
cd $OPENSHIFT_REPO_DIR if [ ! -d $OPENSHIFT_DATA_DIR/jdk1.8.0_20 ]
then
cd $OPENSHIFT_DATA_DIR
wget http://www.java.net/download/jdk8u20/archive/b17/binaries/jdk-8u20-ea-bin-b17-linux-x64-04_jun_2014.tar.gz
tar xvf *.tar.gz
rm -f *.tar.gz
fi
if [ ! -d $OPENSHIFT_DATA_DIR/apache-maven-3.2.3 ]
then
cd $OPENSHIFT_DATA_DIR
wget http://archive.apache.org/dist/maven/maven-3/3.2.3/binaries/apache-maven-3.2.3-bin.tar.gz
tar xvf *.tar.gz
rm -f *.tar.gz
fi
export M2=$OPENSHIFT_DATA_DIR/apache-maven-3.2.3/bin
export JAVA_HOME=$OPENSHIFT_DATA_DIR/jdk1.8.0_20
export PATH=$JAVA_HOME/bin:$M2:$PATH
cd $OPENSHIFT_REPO_DIR
rm -rf $OPENSHIFT_REPO_DIR/node rm -rf $OPENSHIFT_REPO_DIR/node
rm -rf $OPENSHIFT_REPO_DIR/node_modules rm -rf $OPENSHIFT_REPO_DIR/node_modules
rm -rf $OPENSHIFT_TMP_DIR/npm rm -rf $OPENSHIFT_TMP_DIR/npm
@@ -16,7 +33,4 @@ export HOME="$OPENSHIFT_TMP_DIR/local"
export NPM_CONFIG_ARCH="x64" export NPM_CONFIG_ARCH="x64"
npm install npm mvn clean package -DskipTests -Dos.arch=x64 -s .openshift/settings.xml
export PATH="$OPENSHIFT_REPO_DIR/node_modules/.bin:$PATH"
mvn clean package -DskipTests -Dos.arch=x64

View File

@@ -1,3 +1,4 @@
#!/bin/bash #!/bin/bash
cd $OPENSHIFT_REPO_DIR cd $OPENSHIFT_REPO_DIR
nohup java -jar target/commafeed.jar server .openshift/config.mysql.yml > ${OPENSHIFT_DIY_LOG_DIR}/commafeed.log 2>&1 & export JAVA_HOME=$OPENSHIFT_DATA_DIR/jdk1.8.0_20
nohup $JAVA_HOME/bin/java -jar target/commafeed.jar server .openshift/config.mysql.yml > ${OPENSHIFT_DIY_LOG_DIR}/commafeed.log 2>&1 &

View File

@@ -1,18 +1,21 @@
# CommaFeed settings # CommaFeed settings
# ------------------ # ------------------
app: app:
# context path of the application
contextPath: /
# url used to access commafeed # url used to access commafeed
publicUrl: https://@OPENSHIFT_APP_DNS@/ publicUrl: https://@OPENSHIFT_APP_DNS@/
# wether to allow user registrations # wether to allow user registrations
allowRegistrations: false allowRegistrations: false
# create a demo account the first time the app starts
createDemoAccount: false
# put your google analytics tracking code here # put your google analytics tracking code here
googleAnalyticsTrackingCode: googleAnalyticsTrackingCode:
# put your google server key (used for youtube favicon fetching)
googleAuthKey:
# number of http threads # number of http threads
backgroundThreads: 3 backgroundThreads: 3
@@ -47,6 +50,9 @@ app:
# time to keep unread statuses (in days), 0 to disable # time to keep unread statuses (in days), 0 to disable
keepStatusDays: 0 keepStatusDays: 0
# entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop

0
.openshift/markers/java8 Normal file
View File

View File

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

View File

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

View File

@@ -1,3 +1,22 @@
v 2.3.0
- dropwizard upgrade 0.9.1
- feed enclosures are hidden if they already displayed in the content
- fix youtube favicons
- various internationalization fixes
v 2.2.0
- fix youtube and instagram favicon fetching
- mark as read filter was lost when a feed was rearranged with drag&drop
- feed entry categories are now displayed if available
- various performance and dependencies upgrades
- java8 is now required
v 2.1.0
- dropwizard upgrade to 0.8.0
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use server.applicationContextPath instead
- new setting app.maxFeedCapacity for deleting old entries
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title, content, author or url.
- ability to use !keyword or -keyword to exclude a keyword from a search query
- facebook feeds now show user favicon instead of facebook favicon
- new dark theme 'nightsky'
v 2.0.3 v 2.0.3
- internet explorer ajax cache workaround - internet explorer ajax cache workaround
- categories are now deletable again - categories are now deletable again

View File

@@ -1,34 +1,55 @@
CommaFeed [![Build Status](https://travis-ci.org/Athou/commafeed.svg?branch=master)](https://travis-ci.org/Athou/commafeed) # CommaFeed [![Build Status](https://travis-ci.org/Athou/commafeed.svg?branch=master)](https://travis-ci.org/Athou/commafeed)
=========
Sources for [CommaFeed.com](http://www.commafeed.com/). Sources for [CommaFeed.com](http://www.commafeed.com/).
Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS. Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS.
Related open-source projects ## Related open-source projects
----------------------------
Android apps: [News+ extension](https://github.com/Athou/commafeed-newsplus) - [Android app](https://github.com/doomrobo/CommaFeed-Android-Reader)
Android apps: [News+ extension](https://github.com/Athou/commafeed-newsplus)
Browser extensions: [Chrome](https://github.com/Athou/commafeed-chrome) - [Firefox](https://github.com/Athou/commafeed-firefox) - [Opera](https://github.com/Athou/commafeed-opera) - [Safari](https://github.com/Athou/commafeed-safari) Browser extensions: [Chrome](https://github.com/Athou/commafeed-chrome) - [Firefox](https://github.com/Athou/commafeed-firefox) - [Opera](https://github.com/Athou/commafeed-opera) - [Safari](https://github.com/Athou/commafeed-safari)
Deployment on your own server ## Deployment on your own server
-----------------------------
### The very short version (download precompiled package)
mkdir commafeed && cd commafeed
wget https://github.com/Athou/commafeed/releases/download/2.2.0/commafeed.jar
wget https://raw.githubusercontent.com/Athou/commafeed/2.2.0/config.yml.example -O config.yml
vi config.yml
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
### The short version (build from sources)
git clone https://github.com/Athou/commafeed.git
cd commafeed
./mvnw clean package
cp config.yml.example config.yml
vi config.yml
java -Djava.net.preferIPv4Stack=true -jar target/commafeed.jar server config.yml
### The long version (same as the short version, but more detailed)
CommaFeed 2.0 has been rewritten to use Dropwizard and gulp instead of using tomee and wro4j. The latest version of the 1.x branch is available [here](https://github.com/Athou/commafeed/tree/1.x). CommaFeed 2.0 has been rewritten to use Dropwizard and gulp instead of using tomee and wro4j. The latest version of the 1.x branch is available [here](https://github.com/Athou/commafeed/tree/1.x).
For storage, you can either use an embedded H2 database or an external MySQL, PostgreSQL or SQLServer database. For storage, you can either use an embedded H2 database (use it only to test CommaFeed) or an external MySQL, PostgreSQL or SQLServer database.
You also need Maven 3.x (and a Java 1.7+ JDK) installed in order to build the application. You also need the Java 1.8+ JDK in order to build the application.
To install maven and openjdk on Ubuntu, issue the following commands To install the required packages to build CommaFeed on Ubuntu, issue the following commands
sudo apt-get install build-essential openjdk-7-jdk maven # if openjdk-8-jdk is not available on your ubuntu version (14.04 LTS), add the following repo first
# Make sure java7 is the selected java version sudo add-apt-repository ppa:openjdk-r/ppa
sudo apt-get update
sudo apt-get install g++ build-essential openjdk-8-jdk
# Make sure java8 is the selected java version
sudo update-alternatives --config java sudo update-alternatives --config java
sudo update-alternatives --config javac sudo update-alternatives --config javac
On Windows and other operating systems, just download maven 3.x from the [official site](http://maven.apache.org/), extract it somewhere and add the `bin` directory to your `PATH` environment variable.
Clone this repository. If you don't have git you can download the sources as a zip file from [here](https://github.com/Athou/commafeed/archive/master.zip) Clone this repository. If you don't have git you can download the sources as a zip file from [here](https://github.com/Athou/commafeed/archive/master.zip)
git clone https://github.com/Athou/commafeed.git git clone https://github.com/Athou/commafeed.git
@@ -36,17 +57,16 @@ Clone this repository. If you don't have git you can download the sources as a z
Now build the application Now build the application
mvn clean package ./mvnw clean package
Copy `config.yml.example` to `config.yml` then edit the file to your liking. Copy `config.yml.example` to `config.yml` then edit the file to your liking.
Issue the following command to run the app, the server will listen by default on `http://localhost:8082`. The default user is `admin` and the default password is `admin`. Issue the following command to run the app, the server will listen by default on `http://localhost:8082`. The default user is `admin` and the default password is `admin`.
java -jar target/commafeed.jar server config.yml java -Djava.net.preferIPv4Stack=true -jar target/commafeed.jar server config.yml
You can use a proxy http server such as nginx or apache. You can use a proxy http server such as nginx or apache.
Deployment on OpenShift ## Deployment on OpenShift
-----------------------------
[OpenShift](https://openshift.redhat.com) is Red Hat's Platform-as-a-Service (PaaS) that allows developers to quickly develop, host, and scale applications in a cloud environment. CommaFeed runs perfectly on OpenShift and can even be used in the free tier. Follow the [Getting Started](https://developers.openshift.com/en/getting-started-overview.html) guide and after you sign up and install the Command Line Tools (RHC), do: [OpenShift](https://openshift.redhat.com) is Red Hat's Platform-as-a-Service (PaaS) that allows developers to quickly develop, host, and scale applications in a cloud environment. CommaFeed runs perfectly on OpenShift and can even be used in the free tier. Follow the [Getting Started](https://developers.openshift.com/en/getting-started-overview.html) guide and after you sign up and install the Command Line Tools (RHC), do:
@@ -55,9 +75,12 @@ Deployment on OpenShift
git remote add upstream -m master https://github.com/Athou/commafeed.git git remote add upstream -m master https://github.com/Athou/commafeed.git
git pull -s recursive -X theirs upstream master git pull -s recursive -X theirs upstream master
git push git push
# To upgrade an existing openshift installation
git pull upstream master
git push
Translate CommaFeed into your language ## Translate CommaFeed into your language
--------------------------------------
Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/src/main/app/i18n). Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/src/main/app/i18n).
@@ -65,18 +88,16 @@ To add a new language, create a new file in that directory.
The name of the file should be the two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). The name of the file should be the two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
The language has to be referenced in the `src/main/app/js/i18n.js` file to be picked up. The language has to be referenced in the `src/main/app/js/i18n.js` file to be picked up.
Themes ## Themes
---------------------
To create a theme, create a new file `src/main/webapp/sass/themes/_<theme>.scss`. Your styles should be wrapped in a `#theme-<theme>` element and use the [SCSS format](http://sass-lang.com/) which is a superset of CSS. To create a theme, create a new file `src/main/app/sass/themes/_<theme>.scss`. Your styles should be wrapped in a `#theme-<theme>` element and use the [SCSS format](http://sass-lang.com/) which is a superset of CSS.
Don't forget to reference your theme in `src/main/webapp/sass/app.scss` and in `src/main/webapp/js/controllers.js` (look for `$scope.themes`). Don't forget to reference your theme in `src/main/app/sass/app.scss` and in `src/main/app/js/controllers.js` (look for `$scope.themes`).
See [_test.scss](https://github.com/Athou/commafeed/blob/master/src/main/webapp/sass/themes/_test.scss) for an example. See [_test.scss](https://github.com/Athou/commafeed/blob/master/src/main/app/sass/themes/_test.scss) for an example.
Local development ## Local development
-----------------
Steps to configuring a development environment for CommaFeed may include, but may not be limited to: Steps to configuring a development environment for CommaFeed may include, but may not be limited to:
@@ -100,10 +121,9 @@ Steps to configuring a development environment for CommaFeed may include, but ma
14. When you're done developing, create a fork at the top of https://github.com/Athou/CommaFeed page and commit your changes to it. 14. When you're done developing, create a fork at the top of https://github.com/Athou/CommaFeed page and commit your changes to it.
15. If you'd like to contribute to CommaFeed, create a pull request from your repository to https://github.com/Athou/CommaFeed when your changes are ready. There's a button to do so at the top of https://github.com/Athou/CommaFeed. 15. If you'd like to contribute to CommaFeed, create a pull request from your repository to https://github.com/Athou/CommaFeed when your changes are ready. There's a button to do so at the top of https://github.com/Athou/CommaFeed.
Copyright and license ## Copyright and license
---------------------
Copyright 2013-2014 CommaFeed. Copyright 2013-2015 CommaFeed.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License. you may not use this work except in compliance with the License.

View File

@@ -2,32 +2,36 @@
"name": "commafeed", "name": "commafeed",
"version": "2.0.0", "version": "2.0.0",
"dependencies": { "dependencies": {
"jquery": "1.11.0", "jquery": "2.1.3",
"jquery-ui": "1.10.3", "jquery-ui": "1.10.3",
"jquery-mousewheel": "3.1.12", "jquery-mousewheel": "3.1.12",
"lodash": "2.4.1", "lodash": "3.4.0",
"bootstrap": "3.1.1", "bootstrap": "3.3.2",
"font-awesome": "3.2.1", "font-awesome": "3.2.1",
"angular": "1.2.16", "angular": "1.3.14",
"angular-resource": "1.2.16", "angular-resource": "1.3.14",
"angular-route": "1.2.16", "angular-route": "1.3.14",
"angular-sanitize": "1.2.16", "angular-sanitize": "1.3.14",
"angular-touch": "1.2.16", "angular-touch": "1.3.14",
"angular-animate": "1.2.16", "angular-animate": "1.3.14",
"angular-ui-router": "0.2.8", "angular-ui-router": "0.2.13",
"angular-ui-utils": "0.1.0", "angular-ui-utils": "0.1.0",
"angular-ui-select2": "0.0.5", "angular-ui-select2": "0.0.5",
"angular-bootstrap": "0.2.0", "angular-bootstrap": "0.2.0",
"angular-loading-bar": "0.5.0", "angular-loading-bar": "0.6.0",
"angular-translate": "2.2.0", "angular-translate": "2.6.1",
"angular-translate-loader-static-files": "2.2.0", "angular-translate-loader-static-files": "2.6.1",
"ngInfiniteScroll": "1.0.0", "ngInfiniteScroll": "1.0.0",
"ng-grid": "2.0.6", "ng-grid": "2.0.6",
"mousetrap": "1.4.6", "mousetrap": "1.4.6",
"momentjs": "2.6.0", "momentjs": "2.9.0",
"device.js": "matthewhudson/device.js#2ae5c775e35ccc837589e5af34e292c54936778c", "devicejs": "0.2.4",
"readabilicons": "arc90/readability-readabilicons#34c55561c5b8ec6e90714b50237c06b13cb9d59c", "readabilicons": "arc90/readability-readabilicons#34c55561c5b8ec6e90714b50237c06b13cb9d59c",
"zocial": "samcollins/css-social-buttons#1f59ecacde475e563fb6771667597493ec4eecb6", "zocial-less": "1.0.0",
"swagger-ui": "2.0.21" "swagger-ui": "2.1.0"
},
"resolutions": {
"angular": "1.3.14",
"angular-translate": "2.6.1"
} }
} }

View File

@@ -1,18 +1,21 @@
# CommaFeed settings # CommaFeed settings
# ------------------ # ------------------
app: app:
# context path of the application
contextPath: /
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# wether to allow user registrations # wether to allow user registrations
allowRegistrations: true allowRegistrations: true
# create a demo account the first time the app starts
createDemoAccount: false
# put your google analytics tracking code here # put your google analytics tracking code here
googleAnalyticsTrackingCode: googleAnalyticsTrackingCode:
# put your google server key (used for youtube favicon fetching)
googleAuthKey:
# number of http threads # number of http threads
backgroundThreads: 3 backgroundThreads: 3
@@ -47,6 +50,9 @@ app:
# time to keep unread statuses (in days), 0 to disable # time to keep unread statuses (in days), 0 to disable
keepStatusDays: 0 keepStatusDays: 0
# entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop
@@ -69,17 +75,12 @@ app:
database: database:
driverClass: org.h2.Driver driverClass: org.h2.Driver
url: jdbc:h2:./target/example url: jdbc:h2:./target/example;mv_store=false
user: sa user: sa
password: sa password: sa
properties: properties:
charSet: UTF-8 charSet: UTF-8
maxWaitForConnection: 1s validationQuery: "/* CommaFeed Health Check */ SELECT 1"
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
minSize: 1
maxSize: 50
checkConnectionWhileIdle: true
maxConnectionAge: 30m
server: server:
applicationConnectors: applicationConnectors:
@@ -94,7 +95,7 @@ logging:
loggers: loggers:
com.commafeed: DEBUG com.commafeed: DEBUG
liquibase: INFO liquibase: INFO
org.hibernate.SQL: ALL org.hibernate.SQL: INFO # or ALL for sql debugging
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
appenders: appenders:
- type: console - type: console

View File

@@ -1,18 +1,21 @@
# CommaFeed settings # CommaFeed settings
# ------------------ # ------------------
app: app:
# context path of the application
contextPath: /
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# wether to allow user registrations # wether to allow user registrations
allowRegistrations: false allowRegistrations: false
# create a demo account the first time the app starts
createDemoAccount: false
# put your google analytics tracking code here # put your google analytics tracking code here
googleAnalyticsTrackingCode: googleAnalyticsTrackingCode:
# put your google server key (used for youtube favicon fetching)
googleAuthKey:
# number of http threads # number of http threads
backgroundThreads: 3 backgroundThreads: 3
@@ -25,6 +28,7 @@ app:
smtpTls: false smtpTls: false
smtpUserName: smtpUserName:
smtpPassword: smtpPassword:
smtpFromAddress:
# wether this commafeed instance has a lot of feeds to refresh # wether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
@@ -47,6 +51,9 @@ app:
# time to keep unread statuses (in days), 0 to disable # time to keep unread statuses (in days), 0 to disable
keepStatusDays: 0 keepStatusDays: 0
# entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop
@@ -69,17 +76,15 @@ app:
database: database:
driverClass: org.h2.Driver driverClass: org.h2.Driver
url: jdbc:h2:/home/commafeed/db url: jdbc:h2:/home/commafeed/db;mv_store=false
user: sa user: sa
password: sa password: sa
properties: properties:
charSet: UTF-8 charSet: UTF-8
maxWaitForConnection: 1s validationQuery: "/* CommaFeed Health Check */ SELECT 1"
validationQuery: "/* CommaFeed Health Check */ SELECT 1" minSize: 1
minSize: 1 maxSize: 50
maxSize: 50 maxConnectionAge: 30m
checkConnectionWhileIdle: true
maxConnectionAge: 30m
server: server:
applicationConnectors: applicationConnectors:

View File

@@ -4,7 +4,6 @@ var revReplace = require('gulp-rev-replace');
var minifyCSS = require('gulp-minify-css'); var minifyCSS = require('gulp-minify-css');
var uglify = require('gulp-uglify'); var uglify = require('gulp-uglify');
var filter = require('gulp-filter'); var filter = require('gulp-filter');
var bower = require('gulp-bower');
var connect = require('gulp-connect'); var connect = require('gulp-connect');
var modRewrite = require('connect-modrewrite'); var modRewrite = require('connect-modrewrite');
var sass = require('gulp-sass'); var sass = require('gulp-sass');
@@ -15,10 +14,6 @@ var SRC_DIR = 'src/main/app/';
var TEMP_DIR = 'target/gulp/' var TEMP_DIR = 'target/gulp/'
var BUILD_DIR = 'target/classes/assets/'; var BUILD_DIR = 'target/classes/assets/';
gulp.task('bower', function() {
return bower();
});
gulp.task('images', function() { gulp.task('images', function() {
return gulp.src(SRC_DIR + 'images/**/*').pipe(gulp.dest(BUILD_DIR + 'images')); return gulp.src(SRC_DIR + 'images/**/*').pipe(gulp.dest(BUILD_DIR + 'images'));
}); });
@@ -27,34 +22,36 @@ gulp.task('i18n', function() {
return gulp.src(SRC_DIR + 'i18n/**/*.js').pipe(gulp.dest(BUILD_DIR + 'i18n')); return gulp.src(SRC_DIR + 'i18n/**/*.js').pipe(gulp.dest(BUILD_DIR + 'i18n'));
}); });
gulp.task('favicons', function() { gulp.task('resources', function() {
var favicons_png = SRC_DIR + '*.png'; var favicons_png = SRC_DIR + '*.png';
var favicons_ico = SRC_DIR + '*.ico'; var favicons_ico = SRC_DIR + '*.ico';
var favicons_svg = SRC_DIR + '*.svg'; var favicons_svg = SRC_DIR + '*.svg';
return gulp.src([favicons_png, favicons_ico, favicons_svg]).pipe(gulp.dest(BUILD_DIR)); var manifest = SRC_DIR + 'manifest.json';
return gulp.src([favicons_png, favicons_ico, favicons_svg, manifest]).pipe(gulp.dest(BUILD_DIR));
}); });
gulp.task('sass', function() { gulp.task('sass', function() {
return gulp.src(SRC_DIR + 'sass/app.scss').pipe(sass()).pipe(gulp.dest(TEMP_DIR + 'css')); return gulp.src(SRC_DIR + 'sass/app.scss').pipe(sass()).pipe(gulp.dest(TEMP_DIR + 'css'));
}); });
gulp.task('fonts', ['bower'], function() { gulp.task('fonts', function() {
var font_awesome = SRC_DIR + 'lib/font-awesome/font/fontawesome-webfont.*'; var font_awesome = SRC_DIR + 'lib/font-awesome/font/fontawesome-webfont.*';
var zocial = SRC_DIR + 'lib/zocial/css/zocial-regular-*'; var zocial = SRC_DIR + 'lib/zocial-less/css/zocial-regular-*';
var readabilicons = SRC_DIR + 'lib/readabilicons/webfont/fonts/readabilicons-*'; var readabilicons = SRC_DIR + 'lib/readabilicons/webfont/fonts/readabilicons-*';
return gulp.src([font_awesome, zocial, readabilicons]).pipe(gulp.dest(BUILD_DIR + 'font')); return gulp.src([font_awesome, zocial, readabilicons]).pipe(gulp.dest(BUILD_DIR + 'font'));
}); });
gulp.task('select2', ['bower'], function() { gulp.task('select2', function() {
var gif = SRC_DIR + 'lib/select2/*.gif'; var gif = SRC_DIR + 'lib/select2/*.gif';
var png = SRC_DIR + 'lib/select2/*.png'; var png = SRC_DIR + 'lib/select2/*.png';
return gulp.src([gif, png]).pipe(gulp.dest(BUILD_DIR + 'css')); return gulp.src([gif, png]).pipe(gulp.dest(BUILD_DIR + 'css'));
}); });
gulp.task('swagger-ui', ['bower'], function() { gulp.task('swagger-ui', function() {
var index_html = SRC_DIR + 'api/index.html'; var index_html = SRC_DIR + 'api/index.html';
var swagger_json = 'target/swagger/swagger.json';
var lib = SRC_DIR + 'lib/swagger-ui/dist/**/*'; var lib = SRC_DIR + 'lib/swagger-ui/dist/**/*';
return gulp.src([lib, index_html]).pipe(gulp.dest(BUILD_DIR + 'api')); return gulp.src([lib, index_html, swagger_json]).pipe(gulp.dest(BUILD_DIR + 'api'));
}); });
gulp.task('template-cache', function() { gulp.task('template-cache', function() {
@@ -65,17 +62,17 @@ gulp.task('template-cache', function() {
return gulp.src(SRC_DIR + 'templates/**/*.html').pipe(templateCache(options)).pipe(gulp.dest(TEMP_DIR + 'js')); return gulp.src(SRC_DIR + 'templates/**/*.html').pipe(templateCache(options)).pipe(gulp.dest(TEMP_DIR + 'js'));
}); });
gulp.task('build-dev', ['images', 'i18n', 'favicons', 'sass', 'fonts', 'select2', 'swagger-ui', 'template-cache', 'bower'], function() { gulp.task('build-dev', ['images', 'i18n', 'resources', 'sass', 'fonts', 'select2', 'swagger-ui', 'template-cache'], function() {
var assets = useref.assets({ var assets = useref.assets({
searchPath : [SRC_DIR, TEMP_DIR] searchPath : [SRC_DIR, TEMP_DIR]
}); });
var jsFilter = filter("**/*.js"); var jsFilter = filter("**/*.js");
var cssFilter = filter("**/*.css"); var cssFilter = filter("**/*.css");
return gulp.src([SRC_DIR + 'index.html', TEMP_DIR + 'app.css']).pipe(assets).pipe(rev()).pipe(assets.restore()).pipe(useref()).pipe( return gulp.src([SRC_DIR + 'index.html', TEMP_DIR + 'app.css']).pipe(assets).pipe(rev()).pipe(assets.restore()).pipe(useref()).pipe(
revReplace()).pipe(gulp.dest(BUILD_DIR)); revReplace()).pipe(gulp.dest(BUILD_DIR)).pipe(connect.reload());
}); });
gulp.task('build', ['images', 'i18n', 'favicons', 'sass', 'fonts', 'select2', 'swagger-ui', 'template-cache', 'bower'], function() { gulp.task('build', ['images', 'i18n', 'resources', 'sass', 'fonts', 'select2', 'swagger-ui', 'template-cache'], function() {
var assets = useref.assets({ var assets = useref.assets({
searchPath : [SRC_DIR, TEMP_DIR] searchPath : [SRC_DIR, TEMP_DIR]
}); });
@@ -101,7 +98,9 @@ gulp.task('serve', function() {
connect.server({ connect.server({
root : BUILD_DIR, root : BUILD_DIR,
port : 8082, port : 8082,
livereload : true,
middleware : function() { middleware : function() {
var api = '^/api/(.*)$ http://localhost:8083/rest/$1 [P]';
var rest = '^/rest/(.*)$ http://localhost:8083/rest/$1 [P]'; var rest = '^/rest/(.*)$ http://localhost:8083/rest/$1 [P]';
var next = '^/next(.*)$ http://localhost:8083/next$1 [P]'; var next = '^/next(.*)$ http://localhost:8083/next$1 [P]';
var logout = '^/logout$ http://localhost:8083/logout [P]'; var logout = '^/logout$ http://localhost:8083/logout [P]';
@@ -113,4 +112,4 @@ gulp.task('serve', function() {
}); });
gulp.task('dev', ['build-dev', 'watch', 'serve']); gulp.task('dev', ['build-dev', 'watch', 'serve']);
gulp.task('default', ['build']); gulp.task('default', ['build']);

BIN
maven/maven-wrapper.jar Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
#Maven download properties
#Sat Jul 04 09:06:32 CEST 2015
distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip

234
mvnw vendored Executable file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven2 Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
#
# Look for the Apple JDKs first to preserve the existing behaviour, and then look
# for the new JDKs provided by Oracle.
#
if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
#
# Apple JDKs
#
export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
fi
if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
#
# Apple JDKs
#
export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
fi
if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
#
# Oracle JDKs
#
export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
fi
if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
#
# Apple JDKs
#
export JAVA_HOME=`/usr/libexec/java_home`
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Migwn, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
# TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
local basedir=$(pwd)
local wdir=$(pwd)
while [ "$wdir" != '/' ] ; do
wdir=$(cd "$wdir/.."; pwd)
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER="org.apache.maven.wrapper.MavenWrapperMain"
exec "$JAVACMD" \
$MAVEN_OPTS \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
-classpath \
"$MAVEN_PROJECTBASEDIR/maven/maven-wrapper.jar" \
${WRAPPER_LAUNCHER} "$@"

141
mvnw.bat Normal file
View File

@@ -0,0 +1,141 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:init
set MAVEN_CMD_LINE_ARGS=%*
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\maven\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
%MAVEN_JAVA_EXE% -Dmaven.multiModuleProjectDirectory="" %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%

View File

@@ -4,17 +4,17 @@
"main": "main.js", "main": "main.js",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"gulp": "3.8.7", "bower": "1.4.1",
"gulp-rev": "1.0.0", "gulp": "3.8.11",
"gulp-rev-replace": "0.3.0", "gulp-rev": "4.0.0",
"gulp-minify-css": "0.3.7", "gulp-rev-replace": "0.4.1",
"gulp-uglify": "0.3.1", "gulp-minify-css": "1.1.5",
"gulp-filter": "1.0.0", "gulp-uglify": "1.2.0",
"gulp-bower": "0.0.6", "gulp-filter": "2.0.2",
"gulp-connect": "2.0.6", "gulp-connect": "2.2.0",
"connect-modrewrite": "0.7.7", "connect-modrewrite": "0.8.1",
"gulp-sass": "0.7.2", "gulp-sass": "2.0.2",
"gulp-useref": "0.6.0", "gulp-useref": "1.1.2",
"gulp-angular-templatecache": "1.3.0" "gulp-angular-templatecache": "1.6.0"
} }
} }

260
pom.xml
View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

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

View File

@@ -7,24 +7,26 @@
"download" : "Descargar", "download" : "Descargar",
"link" : "Enlace", "link" : "Enlace",
"bookmark" : "Marcador", "bookmark" : "Marcador",
"close" : "Close ", "close" : "Cerrar",
"tags" : "Tags " "tags" : "Etiquetas"
}, },
"tree" : { "tree" : {
"subscribe" : "Subscribir", "subscribe" : "Suscribirse",
"import" : "Importar", "import" : "Importar",
"new_category" : "Nueva categoría", "new_category" : "Nueva categoría",
"all" : "Todos", "all" : "Todos",
"starred" : "Destacado" "starred" : "Destacados"
}, },
"subscribe" : { "subscribe" : {
"feed_url" : "URL del Canal", "feed_url" : "URL del canal",
"feed_name" : "Nombre del Canal", "filtering_expression" : "Expresión de filtrado",
"filtering_expression_help" : "Si no está vacía, una expresión se evalúa como 'cierta' o 'falsa'. Si es falsa, las nueva entradas de este canal se marcarán como leídas automáticamente.\nLas variables disponibles son 'title' (título), 'content'(contenido), 'url' (URL), 'author' (autor), y 'categories' (categorías) y sus contenidos son convertidos a minúsculas para facilitar la comparación de strings (cadenas de texto).\nEjemplo: url.contains('youtube') or (author eq 'athou' and title.contains('github').\nLa sintaxis completa está disponible <a href='http://commons.apache.org/proper/commons-jexl/reference/syntax.html' target='_blank'>aquí</a>.",
"feed_name" : "Nombre del canal",
"category" : "Categoría" "category" : "Categoría"
}, },
"import" : { "import" : {
"google_reader_prefix" : "Déjame importar tus canales de tu", "google_reader_prefix" : "Déjame importar tus canales de tu cuenta ",
"google_reader_suffix" : " cuenta.", "google_reader_suffix" : ".",
"google_download" : "También puedes subir tu archivo subscriptions.xml.", "google_download" : "También puedes subir tu archivo subscriptions.xml.",
"google_download_link" : "Descárgalo de aquí.", "google_download_link" : "Descárgalo de aquí.",
"xml_file" : "Archivo OPML" "xml_file" : "Archivo OPML"
@@ -34,147 +36,147 @@
"parent" : "Padre" "parent" : "Padre"
}, },
"toolbar" : { "toolbar" : {
"unread" : "Sin Leer", "unread" : "No leídos",
"all" : "Todos", "all" : "Todos",
"previous_entry" : "Entrada Anterior", "previous_entry" : "Entrada anterior",
"next_entry" : "Próxima Entrada", "next_entry" : "Entrada siguiente",
"refresh" : "Atualizar", "refresh" : "Atualizar",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Forzar la actualización de todos mis canales.",
"sort_by_asc_desc" : "Ordenar por fecha asc/desc", "sort_by_asc_desc" : "Ordenar por fecha asc/desc.",
"titles_only" : "Sólo Títulos", "titles_only" : "Sólo títulos",
"expanded_view" : "Vista Expandida", "expanded_view" : "Vista expandida",
"mark_all_as_read" : "Marcar todos como leído", "mark_all_as_read" : "Marcar todos como leído",
"mark_all_older_12_hours" : "Items older than 12 hours ", "mark_all_older_12_hours" : "Entradas anteriores a 12 horas.",
"mark_all_older_day" : "Artículos anteriores a un día", "mark_all_older_day" : "Entradas anteriores a un día.",
"mark_all_older_week" : "Artículos más de una semana", "mark_all_older_week" : "Entradas anteriores a una semana.",
"mark_all_older_two_weeks" : "Artículos más de does semanas", "mark_all_older_two_weeks" : "Entradas anteriores a 2 semanas.",
"settings" : "Ajustes", "settings" : "Ajustes",
"profile" : "Perfil", "profile" : "Perfil",
"admin" : "Admin", "admin" : "Admin",
"about" : "Acerca de", "about" : "Acerca de...",
"logout" : "Cerrar sesión", "logout" : "Cerrar sesión",
"donate" : "Donar" "donate" : "Donar"
}, },
"view" : { "view" : {
"entry_source" : "from ", "entry_source" : "de ",
"entry_author" : "by ", "entry_author" : "por ",
"error_while_loading_feed" : "Error mientras se cargaba este canal", "error_while_loading_feed" : "Error mientras se cargaba este canal.",
"keep_unread" : "Guardar no leídos", "keep_unread" : "Mantener como no leído.",
"no_unread_items" : "no tiene items sin leer.", "no_unread_items" : "no tiene entradas sin leer.",
"mark_up_to_here" : "Mark as read up to here ", "mark_up_to_here" : "Marcar como leídos hasta aquí.",
"search_for" : "searching for: ", "search_for" : "buscando: ",
"no_search_results" : "No match found for the requested keywords " "no_search_results" : "No se han encontrado resultados para las palabras clave especificadas."
}, },
"feedsearch" : { "feedsearch" : {
"hint" : "Type in a subscription... ", "hint" : "Introduce una suscripción...",
"help" : "Use the return key to select and arrow keys to navigate. ", "help" : "Usa la tecla Intro para seleccionar y las teclas de flecha para navegar.",
"result_prefix" : "Your subscriptions: " "result_prefix" : "Tus suscripciones:"
}, },
"settings" : { "settings" : {
"general" : { "general" : {
"value" : "General", "value" : "General",
"language" : "Lenguaje", "language" : "Idioma",
"language_contribute" : "Contribuye con traducciones", "language_contribute" : "Contribuye con traducciones.",
"show_unread" : "Mostrar canales y categorías sin entradas no leídas.", "show_unread" : "Mostrar canales y categorías sin entradas no leídas.",
"social_buttons" : "Mostrar botones de compartir de redes sociales.", "social_buttons" : "Mostrar botones para compartir de redes sociales.",
"scroll_marks" : "En vista expandida, el desplazamiento por las entradas las marca como leídas" "scroll_marks" : "En vista expandida, el desplazamiento por las entradas las marca como leídas."
}, },
"appearance" : "Appearance ", "appearance" : "Apariencia",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ", "scroll_speed" : "Velocidad de desplazamiento al navegar entre entradas (en milisegundos)",
"scroll_speed_help" : "set to 0 to disable ", "scroll_speed_help" : "ponlo a 0 para desactivarlo",
"theme" : "Theme ", "theme" : "Tema",
"submit_your_theme" : "Submit your theme ", "submit_your_theme" : "Envía tu tema ",
"custom_css" : "CSS Personalizado" "custom_css" : "CSS personalizado"
}, },
"details" : { "details" : {
"feed_details" : "Detalles de Canales", "feed_details" : "Detalles del canal",
"url" : "URL", "url" : "URL",
"website" : "Website ", "website" : "Sitio web",
"name" : "Nombre", "name" : "Nombre",
"category" : "Categoría", "category" : "Categoría",
"position" : "Position ", "position" : "Posicióon",
"last_refresh" : "Última actualización", "last_refresh" : "Última actualización",
"message" : "Last refresh message ", "message" : "Último mensaje de actualización",
"next_refresh" : "Next refresh ", "next_refresh" : "Próxima actualización",
"queued_for_refresh" : "Queued for refresh ", "queued_for_refresh" : "En cola para actualizar",
"feed_url" : "URL del Canal", "feed_url" : "URL del canal",
"generate_api_key_first" : "Genera una llave API en tu perfil primero.", "generate_api_key_first" : "Genera una clave API en tu perfil primero.",
"unsubscribe" : "Terminar subscripción", "unsubscribe" : "Terminar suscripción",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ", "unsubscribe_confirmation" : "¿Estás seguro de querer terminar tu suscripción a este canal?",
"delete_category_confirmation" : "Are you sure you want to delete this category? ", "delete_category_confirmation" : "¿Estás seguro de querer eliminar esta categoría?",
"category_details" : "Detalles de la categoría", "category_details" : "Detalles de la categoría",
"tag_details" : "Tag details ", "tag_details" : "Detalles de las etiuqetas ",
"parent_category" : "Categoría principal" "parent_category" : "Categoría principal"
}, },
"profile" : { "profile" : {
"user_name" : "Nombre de usuario", "user_name" : "Nombre de usuario",
"email" : "Correo", "email" : "Correo electrónico",
"change_password" : "Cambiar contraseña", "change_password" : "Cambiar contraseña",
"confirm_password" : "Confirmar contraseña", "confirm_password" : "Confirmar contraseña",
"minimum_6_chars" : "Mínimo 6 caracteres", "minimum_6_chars" : "Mínimo 6 caracteres",
"passwords_do_not_match" : "Las contraseñas no coinciden", "passwords_do_not_match" : "Las contraseñas no coinciden",
"api_key" : "Llave API", "api_key" : "Clave API",
"api_key_not_generated" : "No generado todavía", "api_key_not_generated" : "No generado todavía",
"generate_new_api_key" : "Generar nueva llave API", "generate_new_api_key" : "Generar nueva clave API",
"generate_new_api_key_info" : "Al cambiar la contraseña se generará una nueva llave API", "generate_new_api_key_info" : "Al cambiar la contraseña se generará una nueva clave API.",
"opml_export" : "Exportación de OPML", "opml_export" : "Exportación de OPML",
"delete_account" : "Eliminar cuenta", "delete_account" : "Eliminar cuenta",
"delete_account_confirmation" : "Delete your acount? There's no turning back! " "delete_account_confirmation" : "¿Eliminar tu cuenta? ¡No habrá vuelta atrás! "
}, },
"about" : { "about" : {
"rest_api" : { "rest_api" : {
"value" : "REST API", "value" : "REST API",
"line1" : "CommaFeed está construido con el uso de JAX-RS y AngularJS. Por eso, un REST API esta disponible.", "line1" : "CommaFeed está construido sobre JAX-RS y AngularJS. Por lo tanto, una REST API está disponible.",
"link_to_documentation" : "Vínculo de la documentación." "link_to_documentation" : "Enlace a la documentación."
}, },
"keyboard_shortcuts" : "Atajos de teclado", "keyboard_shortcuts" : "Atajos de teclado",
"version" : "CommaFeed version ", "version" : "Versión de CommaFeed",
"line1_prefix" : "CommaFeed es un proyecto open-source. El código se encuentra en ", "line1_prefix" : "CommaFeed es un proyecto de código abierto. El código se encuentra en ",
"line1_suffix" : ".", "line1_suffix" : ".",
"line2_prefix" : "Si encuentras un problema, por favor reportalo en la página de problemas de ", "line2_prefix" : "Si encuentras un problema, por favor repórtalo en la página de problemas de ",
"line2_suffix" : " del proyecto.", "line2_suffix" : " del proyecto.",
"line3" : "Si te gusta este proyecto, por favor considera realizar una donacion para apoyar al desarrollador y ayudar a cubrir los costos de mantenimiento.", "line3" : "Si te gusta este proyecto, por favor considera realizar una donación para apoyar al desarrollador y ayudar a cubrir los costes de mantenimiento.",
"line4" : "For those of you who prefer bitcoin, here is the address ", "line4" : "Para aquellos de vosotros que prefieran bitcoin, aquí está la dirección ",
"goodies" : { "goodies" : {
"value" : "Goodies", "value" : "Extras",
"android_app" : "Android app ", "android_app" : "Apps para Android",
"subscribe_url" : "Subscribe URL ", "subscribe_url" : "URL para suscribirse ",
"chrome_extension" : "Extensión de Chrome", "chrome_extension" : "Extensión para Chrome.",
"firefox_extension" : "Extensión de Firefox", "firefox_extension" : "Extensión para Firefox.",
"opera_extension" : "Opera extension ", "opera_extension" : "Extensón para Opera.",
"subscribe_bookmarklet" : "Add subscription bookmarklet (click) ", "subscribe_bookmarklet" : "Añadir marcador de suscripción (clic).",
"subscribe_bookmarklet_asc" : "Oldest first ", "subscribe_bookmarklet_asc" : "Más antiguos primero",
"subscribe_bookmarklet_desc" : "Newest first ", "subscribe_bookmarklet_desc" : "Más recientes primero",
"next_unread_bookmarklet" : "Next unread item bookmarklet (drag to bookmark bar) " "next_unread_bookmarklet" : "Marcador a la siguiente entrada no leída (arástralo a la barra de marcadores) "
}, },
"translation" : { "translation" : {
"value" : "Traducción", "value" : "Traducción",
"message" : "Necesitamos tu ayuda para ayudar a traducir CommaFeed.", "message" : "Necesitamos tu ayuda para ayudar a traducir CommaFeed.",
"link" : "Ver como contribuir con traducciones." "link" : "Ver cómo contribuir con traducciones."
}, },
"announcements" : "Anuncios", "announcements" : "Anuncios",
"shortcuts" : { "shortcuts" : {
"mouse_middleclick" : "ratón botón medio", "mouse_middleclick" : "click medio",
"open_next_entry" : "abrir próxima entrada", "open_next_entry" : "abrir la siguiente entrada",
"open_previous_entry" : "abrir entrada previa", "open_previous_entry" : "abrir la entrada anterior",
"spacebar" : "space/shift+space ", "spacebar" : "espacio/mayúsculas+espacio",
"move_page_down_up" : "moves the page down/up ", "move_page_down_up" : "mueve la página arriba/abajo",
"focus_next_entry" : "Establecer el foco en la próxima entrada sin abrirlo", "focus_next_entry" : "establecer el foco en la siguiente entrada sin abrirla",
"focus_previous_entry" : "Establecer el foco en la entrada anterior sin abrirlo", "focus_previous_entry" : "establecer el foco en la entrada anterior sin abrirla",
"open_next_feed" : "open next feed or category ", "open_next_feed" : "abrir el siguiente canal o categoría",
"open_previous_feed" : "open previous feed or category ", "open_previous_feed" : "abrir el canal o categoría previo",
"open_close_current_entry" : "abrir/cerrar entrada actual", "open_close_current_entry" : "abrir/cerrar la entrada actual",
"open_current_entry_in_new_window" : "abrir entrada actual en una nueva ventana", "open_current_entry_in_new_window" : "abrir la entrada actual en una nueva ventana",
"open_current_entry_in_new_window_background" : "open current entry in a new window in the background ", "open_current_entry_in_new_window_background" : "abrir la entrada actual en una nueva ventana en segundo plano",
"star_unstar" : "marcar/no marcar la entrada actual", "star_unstar" : "destacar la entrada actual",
"mark_current_entry" : "marcar como leída/no la leída entrada actual", "mark_current_entry" : "marcar la entrada actual como leída/no la leída",
"mark_all_as_read" : "marcar todas las entradas como leídas", "mark_all_as_read" : "marcar todas las entradas como leídas",
"open_in_new_tab_mark_as_read" : "abrir entrada en una nueva pestaña y marcar como leída", "open_in_new_tab_mark_as_read" : "abrir entrada en una nueva pestaña y marcar como leída",
"fullscreen" : "toggle full screen mode ", "fullscreen" : "activar/desactivar el modo pantalla completa ",
"font_size" : "increase/decrease font size of the current entry ", "font_size" : "aumentar/reducir el tamaño de la fuente de la entrada actual",
"go_to_all" : "go to the All view ", "go_to_all" : "ver Todos",
"go_to_starred" : "go to the Starred view ", "go_to_starred" : "ver Destacados",
"feed_search" : "navigate to a subscription by entering the subscription name " "feed_search" : "navega a una suscripción al introducir su nombre"
} }
} }
} }

View File

@@ -34,24 +34,24 @@
"parent" : "Parent" "parent" : "Parent"
}, },
"toolbar" : { "toolbar" : {
"unread" : "Non-lus", "unread" : "Non lus",
"all" : "Tous", "all" : "Tous",
"previous_entry" : "Article précédent", "previous_entry" : "Article précédent",
"next_entry" : "Article suivant", "next_entry" : "Article suivant",
"refresh" : "Rafraîchir", "refresh" : "Rafraîchir",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Rafraîchir tous les flux",
"sort_by_asc_desc" : "Trier par date croissante/décroissante", "sort_by_asc_desc" : "Trier par date croissante/décroissante",
"titles_only" : "Titres uniquement", "titles_only" : "Titres uniquement",
"expanded_view" : "Vue étendue", "expanded_view" : "Vue étendue",
"mark_all_as_read" : "Tout marquer comme lu", "mark_all_as_read" : "Tout marquer comme lu",
"mark_all_older_12_hours" : "Items older than 12 hours ", "mark_all_older_12_hours" : "Articles de plus de 12 heures",
"mark_all_older_day" : "Articles de plus d'un jour", "mark_all_older_day" : "Articles de plus d'une journée",
"mark_all_older_week" : "Articles de plus d'une semaine", "mark_all_older_week" : "Articles de plus d'une semaine",
"mark_all_older_two_weeks" : "Articles de plus d'un mois", "mark_all_older_two_weeks" : "Articles de plus d'un mois",
"settings" : "Préférences", "settings" : "Préférences",
"profile" : "Profil", "profile" : "Profil",
"admin" : "Administration", "admin" : "Administration",
"about" : "A propos", "about" : "À propos",
"logout" : "Déconnexion", "logout" : "Déconnexion",
"donate" : "Faire un don" "donate" : "Faire un don"
}, },
@@ -59,16 +59,16 @@
"entry_source" : "sur", "entry_source" : "sur",
"entry_author" : "par ", "entry_author" : "par ",
"error_while_loading_feed" : "Erreur durant le chargement de ce flux", "error_while_loading_feed" : "Erreur durant le chargement de ce flux",
"keep_unread" : "Garder non-lu", "keep_unread" : "Garder non lu",
"no_unread_items" : "n'a pas d'articles non-lus.", "no_unread_items" : "n'a pas d'articles non lus.",
"mark_up_to_here" : "Marquer comme lu jusqu'ici", "mark_up_to_here" : "Marquer comme lu jusqu'ici",
"search_for" : "searching for: ", "search_for" : "recherche : ",
"no_search_results" : "No match found for the requested keywords " "no_search_results" : "Pas de résultats avec le terme indiqué."
}, },
"feedsearch" : { "feedsearch" : {
"hint" : "Tapez un nom de flux", "hint" : "Tapez un nom de flux",
"help" : "Utilisez la touche entrée pour sélectionner et les flèches pour naviguer", "help" : "Utilisez la touche entrée pour sélectionner et les flèches pour naviguer",
"result_prefix" : "Vos flux:" "result_prefix" : "Vos flux :"
}, },
"settings" : { "settings" : {
"general" : { "general" : {
@@ -76,12 +76,12 @@
"language" : "Langue", "language" : "Langue",
"language_contribute" : "Contribuer aux traductions", "language_contribute" : "Contribuer aux traductions",
"show_unread" : "Afficher les flux et les catégories pour lesquels tout est déjà lu", "show_unread" : "Afficher les flux et les catégories pour lesquels tout est déjà lu",
"social_buttons" : "Afficher les boutons de partage sur réseaux sociaux", "social_buttons" : "Afficher les boutons de partage sur les réseaux sociaux",
"scroll_marks" : "En mode de lecture étendu, marquer comme lu les éléments lorsque la fenêtre descend." "scroll_marks" : "En mode de lecture étendu, marquer les éléments comme lus lorsque la fenêtre descend."
}, },
"appearance" : "Apparence", "appearance" : "Apparence",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ", "scroll_speed" : "Vitesse de défilement entre les entrées (en millisecondes) ",
"scroll_speed_help" : "set to 0 to disable ", "scroll_speed_help" : "Mettez 0 pour désactiver",
"theme" : "Thème", "theme" : "Thème",
"submit_your_theme" : "Soumettez votre thème.", "submit_your_theme" : "Soumettez votre thème.",
"custom_css" : "CSS personnelle" "custom_css" : "CSS personnelle"
@@ -94,16 +94,16 @@
"category" : "Catégorie", "category" : "Catégorie",
"position" : "Position", "position" : "Position",
"last_refresh" : "Dernière mise à jour", "last_refresh" : "Dernière mise à jour",
"message" : "Last refresh message ", "message" : "Message de la dernière mise à jour ",
"next_refresh" : "Prochaine mise à jour", "next_refresh" : "Prochaine mise à jour",
"queued_for_refresh" : "En file d'attente", "queued_for_refresh" : "En file d'attente",
"feed_url" : "URL du flux", "feed_url" : "URL du flux",
"generate_api_key_first" : "Générez une clé API dans votre profil d'abord.", "generate_api_key_first" : "Générez d'abord une clé API dans votre profil.",
"unsubscribe" : "Se désabonner", "unsubscribe" : "Se désabonner",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ", "unsubscribe_confirmation" : "Êtes-vous sûr de vouloir vous désabonner de de flux ? ",
"delete_category_confirmation" : "Are you sure you want to delete this category? ", "delete_category_confirmation" : "Êtes-vous sûr de vouloir supprimer cette catégorie ? ",
"category_details" : "Détails de la catégorie", "category_details" : "Détails de la catégorie",
"tag_details" : "Tag details ", "tag_details" : "Détails du tag",
"parent_category" : "Catégorie parente" "parent_category" : "Catégorie parente"
}, },
"profile" : { "profile" : {
@@ -116,15 +116,15 @@
"api_key" : "Clé API", "api_key" : "Clé API",
"api_key_not_generated" : "Pas encore générée", "api_key_not_generated" : "Pas encore générée",
"generate_new_api_key" : "Générer une nouvelle clé API", "generate_new_api_key" : "Générer une nouvelle clé API",
"generate_new_api_key_info" : "Changer de mot de passe va générer une nouvelle clé API", "generate_new_api_key_info" : "Changer de mot de passe générera une nouvelle clé API",
"opml_export" : "Export du fichier OPML", "opml_export" : "Export du fichier OPML",
"delete_account" : "Effacer le compte", "delete_account" : "Effacer le compte",
"delete_account_confirmation" : "Delete your acount? There's no turning back! " "delete_account_confirmation" : "Êtes-vous sûr de vouloir supprimer définitivement votre compte ?"
}, },
"about" : { "about" : {
"rest_api" : { "rest_api" : {
"value" : "API REST", "value" : "API REST",
"line1" : "CommaFeed utilise JAX-RS et AngularJS, donc une API REST est disponible.", "line1" : "CommaFeed utilise JAX-RS et AngularJS, une API REST est donc disponible.",
"link_to_documentation" : "Lien vers la documentation." "link_to_documentation" : "Lien vers la documentation."
}, },
"keyboard_shortcuts" : "Raccourcis clavier", "keyboard_shortcuts" : "Raccourcis clavier",
@@ -145,7 +145,7 @@
"subscribe_bookmarklet" : "Bookmarklet d'ajout d'abonnement", "subscribe_bookmarklet" : "Bookmarklet d'ajout d'abonnement",
"subscribe_bookmarklet_asc" : "Du plus ancien au plus récent", "subscribe_bookmarklet_asc" : "Du plus ancien au plus récent",
"subscribe_bookmarklet_desc" : "Du plus récent au plus ancien", "subscribe_bookmarklet_desc" : "Du plus récent au plus ancien",
"next_unread_bookmarklet" : "Bookmarklet vers le prochain article non-lu" "next_unread_bookmarklet" : "Bookmarklet vers le prochain article non lu"
}, },
"translation" : { "translation" : {
"value" : "Traduction", "value" : "Traduction",
@@ -165,9 +165,9 @@
"open_previous_feed" : "Sélectionner le flux ou la catégorie précédente", "open_previous_feed" : "Sélectionner le flux ou la catégorie précédente",
"open_close_current_entry" : "Ouvrir/fermer l'article courant", "open_close_current_entry" : "Ouvrir/fermer l'article courant",
"open_current_entry_in_new_window" : "Ouvrir l'article courant dans une nouvelle fenêtre", "open_current_entry_in_new_window" : "Ouvrir l'article courant dans une nouvelle fenêtre",
"open_current_entry_in_new_window_background" : "Ouvrir l'article courant dans une nouvelle fenêtre en arrière plan", "open_current_entry_in_new_window_background" : "Ouvrir l'article courant dans une nouvelle fenêtre en arrière-plan",
"star_unstar" : "Ajouter/enlever l'article courant des favoris", "star_unstar" : "Ajouter/enlever l'article courant des favoris",
"mark_current_entry" : "Marquer comme lue/non-lue l'article courant", "mark_current_entry" : "Marquer comme lu/non lu l'article courant",
"mark_all_as_read" : "Marquer tous les articles comme lus", "mark_all_as_read" : "Marquer tous les articles comme lus",
"open_in_new_tab_mark_as_read" : "Ouvrir l'article courant dans une nouvelle fenêtre et marquer comme lu", "open_in_new_tab_mark_as_read" : "Ouvrir l'article courant dans une nouvelle fenêtre et marquer comme lu",
"fullscreen" : "Activer/désactiver le mode plein-écran", "fullscreen" : "Activer/désactiver le mode plein-écran",
@@ -177,4 +177,4 @@
"feed_search" : "Naviguer vers un flux en entrant son nom" "feed_search" : "Naviguer vers un flux en entrant son nom"
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -322,17 +322,21 @@ module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedS
$scope.save = function() { $scope.save = function() {
var sub = $scope.sub; var sub = $scope.sub;
$scope.error = null;
FeedService.modify({ FeedService.modify({
id : sub.id, id : sub.id,
name : sub.name, name : sub.name,
position : sub.position, position : sub.position,
categoryId : sub.categoryId categoryId : sub.categoryId,
filter : sub.filter
}, function() { }, function() {
CategoryService.init(); CategoryService.init();
$state.transitionTo('feeds.view', { $state.transitionTo('feeds.view', {
_id : 'all', _id : 'all',
_type : 'category' _type : 'category'
}); });
}, function(e) {
$scope.error = e.data;
}); });
}; };
}]); }]);
@@ -489,6 +493,7 @@ module.controller('ToolbarCtrl', [
type : $stateParams._type, type : $stateParams._type,
id : $stateParams._id, id : $stateParams._id,
olderThan : olderThan, olderThan : olderThan,
keywords : $location.search().q,
read : true read : true
}); });
}; };
@@ -881,6 +886,7 @@ module.controller('FeedListCtrl', [
service.mark({ service.mark({
id : $scope.selectedId, id : $scope.selectedId,
olderThan : olderThan || $scope.timestamp, olderThan : olderThan || $scope.timestamp,
keywords : $location.search().q,
read : true read : true
}, function() { }, function() {
CategoryService.refresh(function() { CategoryService.refresh(function() {
@@ -1365,7 +1371,7 @@ module.controller('SettingsCtrl', ['$scope', '$location', 'SettingsService', 'An
$scope.langs = LangService.langs; $scope.langs = LangService.langs;
$scope.themes = ['default', 'bootstrap', 'dark', 'ebraminio', 'MRACHINI', 'svetla', 'third']; $scope.themes = ['default', 'bootstrap', 'dark', 'ebraminio', 'MRACHINI', 'nightsky', 'svetla', 'third'];
$scope.settingsService = SettingsService; $scope.settingsService = SettingsService;
$scope.$watch('settingsService.settings', function(value) { $scope.$watch('settingsService.settings', function(value) {

View File

@@ -72,7 +72,7 @@ module.directive('tags', function() {
tags : [] tags : []
}; };
if (newValue) { if (newValue) {
data.tags = newValue.split(','); data.tags = newValue;
} }
EntryService.tag(data); EntryService.tag(data);
} }
@@ -308,7 +308,8 @@ module.directive('droppable', ['CategoryService', 'FeedService', function(Catego
var data = { var data = {
id : source.id, id : source.id,
name : source.name name : source.name,
filter : source.filter
}; };
if (source.children) { if (source.children) {
@@ -359,3 +360,14 @@ module.directive('metricGauge', function() {
templateUrl : 'templates/_metrics.gauge.html' templateUrl : 'templates/_metrics.gauge.html'
}; };
}); });
module.directive('metricTimer', function() {
return {
scope : {
metric : '=',
label : '='
},
restrict : 'E',
templateUrl : 'templates/_metrics.timer.html'
};
});

View File

@@ -19,7 +19,7 @@ module.service('LangService', [function() {
'nn': 'Norsk (nynorsk)', 'nn': 'Norsk (nynorsk)',
'pt': 'Português', 'pt': 'Português',
'pl': 'Polski', 'pl': 'Polski',
'ru': 'русский', 'ru': 'Русский',
'fi': 'Suomi', 'fi': 'Suomi',
'sv': 'Svenska', 'sv': 'Svenska',
'zh': '简体中文', 'zh': '简体中文',
@@ -31,4 +31,4 @@ module.service('LangService', [function() {
'cs': 'Čeština', 'cs': 'Čeština',
'ms': 'Bahasa Malaysian' 'ms': 'Bahasa Malaysian'
} }
}]); }]);

View File

@@ -23,26 +23,23 @@ app.config([
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/); $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/);
var interceptor = ['$rootScope', '$q', '$injector', function(scope, $q, $injector) { var interceptor = ['$rootScope', '$q', '$injector', function(scope, $q, $injector) {
var f = {};
var success = function(response) {
f.response = function(response) {
return response; return response;
}; };
var error = function(response) {
f.responseError = function(response) {
var status = response.status; var status = response.status;
if (status == 401) { if (status == 401) {
$injector.get('$state').transitionTo('welcome'); $injector.get('$state').transitionTo('welcome');
} }
return $q.reject(response); return $q.reject(response);
}; };
return f;
var promise = function(promise) {
return promise.then(success, error);
};
return promise;
}]; }];
$httpProvider.responseInterceptors.push(interceptor); $httpProvider.interceptors.push(interceptor);
$stateProvider.state('feeds', { $stateProvider.state('feeds', {
'abstract' : true, 'abstract' : true,

View File

@@ -62,7 +62,7 @@ module.factory('SettingsService', ['$resource', '$translate', function($resource
} else if (lang === 'ms') { } else if (lang === 'ms') {
lang = 'ms-my'; lang = 'ms-my';
} }
moment.lang(lang, {}); moment.locale(lang, {});
if (callback) { if (callback) {
callback(data); callback(data);
} }
@@ -298,6 +298,7 @@ module.factory('EntryService', ['$resource', '$http', function($resource, $http)
$http.get('rest/entry/tags').success(function(data) { $http.get('rest/entry/tags').success(function(data) {
res.tags = []; res.tags = [];
res.tags.push.apply(res.tags, data); res.tags.push.apply(res.tags, data);
res.tags.sort();
}); });
}; };
var oldTag = res.tag; var oldTag = res.tag;

View File

@@ -0,0 +1,31 @@
{
"name": "CommaFeed",
"icons": [
{
"src": "app-icon-72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "app-icon-114.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "app-icon-144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "app-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
}
],
"start_url": "/",
"display": "standalone"
}

View File

@@ -17,6 +17,7 @@
@import "themes/bootstrap"; @import "themes/bootstrap";
@import "themes/ebraminio"; @import "themes/ebraminio";
@import "themes/MRACHINI"; @import "themes/MRACHINI";
@import "themes/nightsky";
@import "themes/svetla"; @import "themes/svetla";
@import "themes/dark"; @import "themes/dark";
@import "themes/third"; @import "themes/third";

View File

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

View File

@@ -1,3 +1,8 @@
a:focus {
outline: none;
text-decoration: none;
}
.container-full { .container-full {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
@@ -43,6 +48,10 @@ label {
display: block; display: block;
} }
.pre-wrap {
white-space: pre-wrap;
}
.form-horizontal .control-group { .form-horizontal .control-group {
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -116,19 +125,23 @@ blockquote p {
font-size: 14px; font-size: 14px;
} }
.btn,.btn-large,.btn-small,.btn-mini { .form-group {
margin-bottom: 10px;
}
.btn, .btn-large, .btn-small, .btn-mini {
border-top-left-radius: 2px; border-top-left-radius: 2px;
border-top-right-radius: 2px; border-top-right-radius: 2px;
border-bottom-left-radius: 2px; border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px; border-bottom-right-radius: 2px;
} }
.btn-group>.btn:first-child,.btn-group>.btn-large:first-child { .btn-group>.btn:first-child, .btn-group>.btn-large:first-child {
border-top-left-radius: 2px; border-top-left-radius: 2px;
border-bottom-left-radius: 2px; border-bottom-left-radius: 2px;
} }
.btn-group>.btn:last-child,.btn-group>.btn-large:last-child,.btn-group>.dropdown-toggle .btn-group>.btn:last-child, .btn-group>.btn-large:last-child, .btn-group>.dropdown-toggle
{ {
border-top-right-radius: 2px; border-top-right-radius: 2px;
border-bottom-right-radius: 2px; border-bottom-right-radius: 2px;

View File

@@ -0,0 +1,126 @@
#theme-nightsky {
a {
color: #2A9FD6;
}
.nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus {
color: #FFF;
background-color: #2A9FD6;
}
body, .toolbar {
color: #C6C6C6;
background-color: #2F2F2F;
}
.btn-default {
color: #C6C6C6;
background-color: #424242;
border-color: #424242;
}
.btn-default:hover, .btn-default:focus, .btn-default.focus, .btn-default:active,
.btn-default.active, .open>.dropdown-toggle.btn-default {
background-color: #282828;
border-color: #232323;
}
.css-treeview li .tree-item:hover {
background-color: #282828;
}
.css-treeview .unread-counter {
color: #939393;
}
.css-treeview .category-link, .css-treeview a {
color: #939393;
}
.css-treeview a .unread, .css-treeview .category-link .unread {
color: #C6C6C6;
}
.css-treeview .selected {
color: #C00;
}
.entrylist-header {
border-bottom: 1px solid #282828;
}
#feed-accordion .entry {
border-bottom: 1px solid #282828;
}
#feed-accordion .entry-body .entry-title {
font-weight: normal;
}
#feed-accordion .entry-heading .entry-name {
color: #939393;
}
#feed-accordion .unread .entry-heading .entry-name {
font-weight: normal;
color: #C6C6C6;
}
#feed-accordion .unread .entry-heading {
background-color: #444;
}
#feed-accordion .entry-heading {
background-color: #2F2F2F;
}
#feed-accordion .unread .entry-heading:hover {
background-color: #2F2F2F;
}
#feed-accordion .current.closed .entry-heading {
background-color: #151515;
}
#feed-accordion .entry-heading-link {
color: #C6C6C6;
}
#feed-accordion .entry-external-link {
color: #C6C6C6;
}
#feed-accordion .icon-star-empty {
color: #C6C6C6;
}
#feed-accordion .entry-heading .feed-name {
color: #939393;
}
#feed-accordion .entry-body-content {
color: #C6C6C6;
}
#feed-accordion .entry-buttons {
background-color: #494949;
border-top: 1px solid #282828;
}
#feed-accordion .share-buttons a {
color: #C6C6C6;
}
#feed-accordion a.mark-up-to {
color: #C6C6C6;
}
#loading-bar .bar {
background: #C6C6C6;
}
#loading-bar .peg {
box-shadow: 0 0 10px #C6C6C6, 0 0 5px #C6C6C6;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,14 +6,14 @@
</button> </button>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-nav">
<a type="button" class="btn btn-default" ng-click="previousEntry()" title="{{ 'toolbar.previous_entry' | translate }}"> <a type="button" class="btn btn-default" ng-click="previousEntry()" title="{{ 'toolbar.previous_entry' | translate }}">
<i class="icon-chevron-up"></i> <i class="icon-chevron-up"></i>
</a> </a>
<a type="button" class="btn btn-default" ng-click="nextEntry()" title="{{ 'toolbar.next_entry' | translate }}"> <a type="button" class="btn btn-default" ng-click="nextEntry()" title="{{ 'toolbar.next_entry' | translate }}">
<i class="icon-chevron-down"></i> <i class="icon-chevron-down"></i>
</a> </a>
<div class="btn-group"> <div class="btn-group" id="toolbar-refresh">
<a type="button" class="btn btn-default" ng-click="refresh()" title="{{ 'toolbar.refresh' | translate }}"> <a type="button" class="btn btn-default" ng-click="refresh()" title="{{ 'toolbar.refresh' | translate }}">
<i class="icon-refresh"></i> <i class="icon-refresh"></i>
</a> </a>
@@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-mark-read">
<a type="button" class="btn btn-default" ng-click="markAllAsRead()" title="{{ 'toolbar.mark_all_as_read' | translate }}"> <a type="button" class="btn btn-default" ng-click="markAllAsRead()" title="{{ 'toolbar.mark_all_as_read' | translate }}">
<i class="icon-ok"></i> <i class="icon-ok"></i>
</a> </a>
@@ -57,7 +57,7 @@
</button> </button>
</div> </div>
<div class="actions btn-group"> <div class="actions btn-group" id="toolbar-read-mode">
<div ng-if="!MobileService.mobile || MobileService.rightMenu"> <div ng-if="!MobileService.mobile || MobileService.rightMenu">
<div class="btn-group read-mode"> <div class="btn-group read-mode">
<button type="button" class="btn btn-default" ng-click="settingsService.settings.readingMode = 'unread'" <button type="button" class="btn btn-default" ng-click="settingsService.settings.readingMode = 'unread'"
@@ -66,14 +66,14 @@
ng-class="{'active': settingsService.settings.readingMode == 'all'}">{{ 'toolbar.all' | translate }}</button> ng-class="{'active': settingsService.settings.readingMode == 'all'}">{{ 'toolbar.all' | translate }}</button>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-read-order">
<a type="button" class="btn btn-default" ng-click="toggleOrder()" title="{{ 'toolbar.sort_by_asc_desc' | translate }}"> <a type="button" class="btn btn-default" ng-click="toggleOrder()" title="{{ 'toolbar.sort_by_asc_desc' | translate }}">
<i <i
ng-class="{'icon-arrow-up' : settingsService.settings.readingOrder == 'asc', 'icon-arrow-down': settingsService.settings.readingOrder == 'desc'}"></i> ng-class="{'icon-arrow-up' : settingsService.settings.readingOrder == 'asc', 'icon-arrow-down': settingsService.settings.readingOrder == 'desc'}"></i>
</a> </a>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-read-view-settings">
<a type="button" class="btn btn-default" ng-click="settingsService.settings.viewMode = 'title'" <a type="button" class="btn btn-default" ng-click="settingsService.settings.viewMode = 'title'"
ng-class="{'active': settingsService.settings.viewMode == 'title'}" title="{{ 'toolbar.titles_only' | translate }}"> ng-class="{'active': settingsService.settings.viewMode == 'title'}" title="{{ 'toolbar.titles_only' | translate }}">
<i class="icon-list"></i> <i class="icon-list"></i>
@@ -84,7 +84,7 @@
</a> </a>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-settings">
<a class="btn btn-default" ng-click="toSettings()" title="{{ 'toolbar.settings' | translate }}"> <a class="btn btn-default" ng-click="toSettings()" title="{{ 'toolbar.settings' | translate }}">
<i class="icon-cog"></i> <i class="icon-cog"></i>
</a> </a>
@@ -124,13 +124,13 @@
</form> </form>
</div> </div>
<div class="btn-group donate"> <div class="btn-group donate">
<a class="btn btn-success" type="button" ng-click="toHelp()" title="{{ 'toolbar.about' | translate }} / {{ 'toolbar.donate' | translate }}"> <button class="btn btn-success" type="button" ng-click="toHelp()" title="{{ 'toolbar.about' | translate }} / {{ 'toolbar.donate' | translate }}">
<i class="icon-info-sign"></i> <i class="icon-info-sign"></i>
</a> </button>
</div> </div>
</div> </div>
</div> </div>
<span ng-if="!MobileService.mobile" ng-bind-html="ServerService.announcement | trustHtml"></span> <span ng-if="!MobileService.mobile" ng-bind-html="ServerService.announcement | trustHtml"></span>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,9 @@
package com.commafeed; package com.commafeed;
import io.dropwizard.Application;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.hibernate.HibernateBundle;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.servlets.CacheBustingFilter;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import java.io.IOException; import java.io.IOException;
import java.util.Date; import java.util.Date;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
@@ -22,6 +14,7 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.server.session.SessionHandler;
import org.hibernate.cfg.AvailableSettings;
import com.commafeed.backend.feed.FeedRefreshTaskGiver; import com.commafeed.backend.feed.FeedRefreshTaskGiver;
import com.commafeed.backend.feed.FeedRefreshUpdater; import com.commafeed.backend.feed.FeedRefreshUpdater;
@@ -39,10 +32,8 @@ import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.StartupService; import com.commafeed.backend.service.StartupService;
import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.UserService;
import com.commafeed.backend.task.OldStatusesCleanupTask; import com.commafeed.backend.task.ScheduledTask;
import com.commafeed.backend.task.OrphansCleanupTask; import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
import com.commafeed.frontend.auth.SecurityCheckProvider;
import com.commafeed.frontend.auth.SecurityCheckProvider.SecurityCheckUserServiceProvider;
import com.commafeed.frontend.resource.AdminREST; import com.commafeed.frontend.resource.AdminREST;
import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.frontend.resource.CategoryREST;
import com.commafeed.frontend.resource.EntryREST; import com.commafeed.frontend.resource.EntryREST;
@@ -54,19 +45,22 @@ import com.commafeed.frontend.servlet.AnalyticsServlet;
import com.commafeed.frontend.servlet.CustomCssServlet; import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.LogoutServlet; import com.commafeed.frontend.servlet.LogoutServlet;
import com.commafeed.frontend.servlet.NextUnreadServlet; import com.commafeed.frontend.servlet.NextUnreadServlet;
import com.commafeed.frontend.session.SessionHelperProvider; import com.commafeed.frontend.session.SessionHelperFactoryProvider;
import com.google.inject.Guice; import com.google.inject.Guice;
import com.google.inject.Injector; import com.google.inject.Injector;
import com.sun.jersey.api.core.ResourceConfig; import com.google.inject.Key;
import com.wordnik.swagger.config.ConfigFactory; import com.google.inject.TypeLiteral;
import com.wordnik.swagger.config.ScannerFactory;
import com.wordnik.swagger.config.SwaggerConfig; import io.dropwizard.Application;
import com.wordnik.swagger.jaxrs.config.DefaultJaxrsScanner; import io.dropwizard.assets.AssetsBundle;
import com.wordnik.swagger.jaxrs.listing.ApiDeclarationProvider; import io.dropwizard.db.DataSourceFactory;
import com.wordnik.swagger.jaxrs.listing.ApiListingResourceJSON; import io.dropwizard.forms.MultiPartBundle;
import com.wordnik.swagger.jaxrs.listing.ResourceListingProvider; import io.dropwizard.hibernate.HibernateBundle;
import com.wordnik.swagger.jaxrs.reader.DefaultJaxrsApiReader; import io.dropwizard.migrations.MigrationsBundle;
import com.wordnik.swagger.reader.ClassReaders; import io.dropwizard.server.DefaultServerFactory;
import io.dropwizard.servlets.CacheBustingFilter;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
public class CommaFeedApplication extends Application<CommaFeedConfiguration> { public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@@ -89,36 +83,44 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
FeedSubscription.class, User.class, UserRole.class, UserSettings.class) { FeedSubscription.class, User.class, UserRole.class, UserSettings.class) {
@Override @Override
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
return configuration.getDatabase(); DataSourceFactory factory = configuration.getDataSourceFactory();
// keep using old id generator for backward compatibility
factory.getProperties().put(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, "false");
factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");
factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
return factory;
} }
}); });
bootstrap.addBundle(new MigrationsBundle<CommaFeedConfiguration>() { bootstrap.addBundle(new MigrationsBundle<CommaFeedConfiguration>() {
@Override @Override
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
return configuration.getDatabase(); return configuration.getDataSourceFactory();
} }
}); });
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html")); bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
bootstrap.addBundle(new MultiPartBundle());
} }
@Override @Override
public void run(CommaFeedConfiguration config, Environment environment) throws Exception { public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
// configure context path
environment.getApplicationContext().setContextPath(config.getApplicationSettings().getContextPath());
// guice init // guice init
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics())); Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
// Auth/session management // session management
environment.servlets().setSessionHandler(new SessionHandler(config.getSessionManagerFactory().build())); environment.servlets().setSessionHandler(new SessionHandler(config.getSessionManagerFactory().build()));
environment.jersey().register(new SecurityCheckUserServiceProvider(injector.getInstance(UserService.class)));
environment.jersey().register(SecurityCheckProvider.class); // support for "@SecurityCheck User user" injection
environment.jersey().register(SessionHelperProvider.class); environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class)));
// support for "@Context SessionHelper sessionHelper" injection
environment.jersey().register(new SessionHelperFactoryProvider.Binder());
// REST resources // REST resources
environment.jersey().setUrlPattern("/rest/*"); environment.jersey().setUrlPattern("/rest/*");
((DefaultServerFactory) config.getServerFactory()).setJerseyRootPath("/rest/*");
environment.jersey().register(injector.getInstance(AdminREST.class)); environment.jersey().register(injector.getInstance(AdminREST.class));
environment.jersey().register(injector.getInstance(CategoryREST.class)); environment.jersey().register(injector.getInstance(CategoryREST.class));
environment.jersey().register(injector.getInstance(EntryREST.class)); environment.jersey().register(injector.getInstance(EntryREST.class));
@@ -134,9 +136,13 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js"); environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js");
// Scheduled tasks // Scheduled tasks
ScheduledExecutorService executor = environment.lifecycle().scheduledExecutorService("task-scheduler").build(); Set<ScheduledTask> tasks = injector.getInstance(Key.get(new TypeLiteral<Set<ScheduledTask>>() {
injector.getInstance(OldStatusesCleanupTask.class).register(executor); }));
injector.getInstance(OrphansCleanupTask.class).register(executor); ScheduledExecutorService executor = environment.lifecycle().scheduledExecutorService("task-scheduler", true).threads(tasks.size())
.build();
for (ScheduledTask task : tasks) {
task.register(executor);
}
// database init/changelogs // database init/changelogs
environment.lifecycle().manage(injector.getInstance(StartupService.class)); environment.lifecycle().manage(injector.getInstance(StartupService.class));
@@ -146,16 +152,6 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.lifecycle().manage(injector.getInstance(FeedRefreshWorker.class)); environment.lifecycle().manage(injector.getInstance(FeedRefreshWorker.class));
environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class)); environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class));
// Swagger
environment.jersey().register(new ApiListingResourceJSON());
environment.jersey().register(new ApiDeclarationProvider());
environment.jersey().register(new ResourceListingProvider());
ScannerFactory.setScanner(new DefaultJaxrsScanner());
ClassReaders.setReader(new DefaultJaxrsApiReader());
SwaggerConfig swaggerConfig = ConfigFactory.config();
swaggerConfig.setApiVersion("1");
swaggerConfig.setBasePath("/rest");
// cache configuration // cache configuration
// prevent caching on REST resources, except for favicons // prevent caching on REST resources, except for favicons
environment.servlets().addFilter("cache-filter", new CacheBustingFilter() { environment.servlets().addFilter("cache-filter", new CacheBustingFilter() {
@@ -170,8 +166,6 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
} }
}).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*"); }).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*");
// enable wadl
environment.jersey().disable(ResourceConfig.FEATURE_DISABLE_WADL);
} }
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {

View File

@@ -12,7 +12,7 @@ import javax.validation.constraints.NotNull;
import lombok.Getter; import lombok.Getter;
import org.apache.commons.lang.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.NotBlank;
import com.commafeed.backend.cache.RedisPoolFactory; import com.commafeed.backend.cache.RedisPoolFactory;
@@ -35,7 +35,7 @@ public class CommaFeedConfiguration extends Configuration {
@Valid @Valid
@NotNull @NotNull
@JsonProperty("database") @JsonProperty("database")
private DataSourceFactory database = new DataSourceFactory(); private DataSourceFactory dataSourceFactory = new DataSourceFactory();
@Valid @Valid
@NotNull @NotNull
@@ -64,60 +64,76 @@ public class CommaFeedConfiguration extends Configuration {
public static class ApplicationSettings { public static class ApplicationSettings {
@NotNull @NotNull
@NotBlank @NotBlank
private String contextPath; @Valid
@NotNull
@NotBlank
private String publicUrl; private String publicUrl;
@NotNull @NotNull
private boolean allowRegistrations; @Valid
private Boolean allowRegistrations;
@NotNull
@Valid
private Boolean createDemoAccount;
private String googleAnalyticsTrackingCode; private String googleAnalyticsTrackingCode;
@NotNull private String googleAuthKey;
@Min(1)
private int backgroundThreads;
@NotNull @NotNull
@Min(1) @Min(1)
private int databaseUpdateThreads; @Valid
private Integer backgroundThreads;
@NotNull
@Min(1)
@Valid
private Integer databaseUpdateThreads;
private String smtpHost; private String smtpHost;
private int smtpPort; private int smtpPort;
private boolean smtpTls; private boolean smtpTls;
private String smtpUserName; private String smtpUserName;
private String smtpPassword; private String smtpPassword;
private String smtpFromAddress;
@NotNull @NotNull
private boolean heavyLoad; @Valid
private Boolean heavyLoad;
@NotNull @NotNull
private boolean pubsubhubbub; @Valid
private Boolean pubsubhubbub;
@NotNull @NotNull
private boolean imageProxyEnabled; @Valid
private Boolean imageProxyEnabled;
@NotNull @NotNull
@Min(0) @Min(0)
private int queryTimeout; @Valid
private Integer queryTimeout;
@NotNull @NotNull
@Min(0) @Min(0)
private int keepStatusDays; @Valid
private Integer keepStatusDays;
@NotNull @NotNull
@Min(0) @Min(0)
private int refreshIntervalMinutes; @Valid
private Integer maxFeedCapacity;
@NotNull @NotNull
@Min(0)
@Valid
private Integer refreshIntervalMinutes;
@NotNull
@Valid
private CacheType cache; private CacheType cache;
@NotNull @NotNull
@Valid
private String announcement; private String announcement;
public Date getUnreadThreshold() { public Date getUnreadThreshold() {

View File

@@ -11,9 +11,15 @@ import com.commafeed.CommaFeedConfiguration.CacheType;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.cache.NoopCacheService; import com.commafeed.backend.cache.NoopCacheService;
import com.commafeed.backend.cache.RedisCacheService; import com.commafeed.backend.cache.RedisCacheService;
import com.commafeed.backend.favicon.DefaultFaviconFetcher;
import com.commafeed.backend.favicon.AbstractFaviconFetcher; import com.commafeed.backend.favicon.AbstractFaviconFetcher;
import com.commafeed.backend.favicon.DefaultFaviconFetcher;
import com.commafeed.backend.favicon.FacebookFaviconFetcher;
import com.commafeed.backend.favicon.YoutubeFaviconFetcher; import com.commafeed.backend.favicon.YoutubeFaviconFetcher;
import com.commafeed.backend.task.OldEntriesCleanupTask;
import com.commafeed.backend.task.OldStatusesCleanupTask;
import com.commafeed.backend.task.OrphanedContentsCleanupTask;
import com.commafeed.backend.task.OrphanedFeedsCleanupTask;
import com.commafeed.backend.task.ScheduledTask;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides; import com.google.inject.Provides;
import com.google.inject.multibindings.Multibinder; import com.google.inject.multibindings.Multibinder;
@@ -38,8 +44,15 @@ public class CommaFeedModule extends AbstractModule {
log.info("using cache {}", cacheService.getClass()); log.info("using cache {}", cacheService.getClass());
bind(CacheService.class).toInstance(cacheService); bind(CacheService.class).toInstance(cacheService);
Multibinder<AbstractFaviconFetcher> multibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class); Multibinder<AbstractFaviconFetcher> faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
multibinder.addBinding().to(YoutubeFaviconFetcher.class); faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class);
multibinder.addBinding().to(DefaultFaviconFetcher.class); faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class);
faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class);
Multibinder<ScheduledTask> taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class);
taskMultibinder.addBinding().to(OldStatusesCleanupTask.class);
taskMultibinder.addBinding().to(OldEntriesCleanupTask.class);
taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class);
taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class);
} }
} }

View File

@@ -16,7 +16,7 @@ import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpContext;
class ContentEncodingInterceptor implements HttpResponseInterceptor { class ContentEncodingInterceptor implements HttpResponseInterceptor {
private static final Set<String> ALLOWED_CONTENT_ENCODINGS = new HashSet<>(Arrays.asList("gzip", "x-gzip", "deflate", "identity")); private static final Set<String> ALLOWED_CONTENT_ENCODINGS = new HashSet<>(Arrays.asList("gzip", "x-gzip", "deflate", "identity"));
@Override @Override
@@ -28,17 +28,17 @@ class ContentEncodingInterceptor implements HttpResponseInterceptor {
} }
} }
} }
private boolean containsUnsupportedEncodings(Header contentEncodingHeader) { private boolean containsUnsupportedEncodings(Header contentEncodingHeader) {
HeaderElement[] codecs = contentEncodingHeader.getElements(); HeaderElement[] codecs = contentEncodingHeader.getElements();
for (final HeaderElement codec : codecs) { for (final HeaderElement codec : codecs) {
String codecName = codec.getName().toLowerCase(Locale.US); String codecName = codec.getName().toLowerCase(Locale.US);
if (!ALLOWED_CONTENT_ENCODINGS.contains(codecName)) { if (!ALLOWED_CONTENT_ENCODINGS.contains(codecName)) {
return true; return true;
} }
} }
return false; return false;
} }
@@ -47,9 +47,9 @@ class ContentEncodingInterceptor implements HttpResponseInterceptor {
@Override @Override
public Header getContentEncoding() { public Header getContentEncoding() {
return null; return null;
}; }
}; };
response.setEntity(wrapped); response.setEntity(wrapped);
} }

View File

@@ -17,7 +17,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts; import org.apache.http.Consts;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
@@ -34,7 +34,7 @@ import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig; import org.apache.http.config.ConnectionConfig;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
@@ -53,7 +53,7 @@ public class HttpGetter {
private static final String ACCEPT_LANGUAGE = "en"; private static final String ACCEPT_LANGUAGE = "en";
private static final String PRAGMA_NO_CACHE = "No-cache"; private static final String PRAGMA_NO_CACHE = "No-cache";
private static final String CACHE_CONTROL_NO_CACHE = "no-cache"; private static final String CACHE_CONTROL_NO_CACHE = "no-cache";
private static final HttpResponseInterceptor REMOVE_INCORRECT_CONTENT_ENCODING = new ContentEncodingInterceptor(); private static final HttpResponseInterceptor REMOVE_INCORRECT_CONTENT_ENCODING = new ContentEncodingInterceptor();
private static SSLContext SSL_CONTEXT = null; private static SSLContext SSL_CONTEXT = null;
@@ -70,7 +70,7 @@ public class HttpGetter {
@Inject @Inject
public HttpGetter(CommaFeedConfiguration config) { public HttpGetter(CommaFeedConfiguration config) {
this.userAgent = String.format("CommaFeed/%s (https://www.commafeed.com)", config.getVersion()); this.userAgent = String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", config.getVersion());
} }
public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException { public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
@@ -181,8 +181,8 @@ public class HttpGetter {
builder.addInterceptorFirst(REMOVE_INCORRECT_CONTENT_ENCODING); builder.addInterceptorFirst(REMOVE_INCORRECT_CONTENT_ENCODING);
builder.disableAutomaticRetries(); builder.disableAutomaticRetries();
builder.setSslcontext(SSL_CONTEXT); builder.setSSLContext(SSL_CONTEXT);
builder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
RequestConfig.Builder configBuilder = RequestConfig.custom(); RequestConfig.Builder configBuilder = RequestConfig.custom();
configBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES); configBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES);

View File

@@ -1,5 +1,6 @@
package com.commafeed.backend.cache; package com.commafeed.backend.cache;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -18,7 +19,6 @@ import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.UnreadCount; import com.commafeed.frontend.model.UnreadCount;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -30,7 +30,7 @@ public class RedisCacheService extends CacheService {
@Override @Override
public List<String> getLastEntries(Feed feed) { public List<String> getLastEntries(Feed feed) {
List<String> list = Lists.newArrayList(); List<String> list = new ArrayList<>();
try (Jedis jedis = pool.getResource()) { try (Jedis jedis = pool.getResource()) {
String key = buildRedisEntryKey(feed); String key = buildRedisEntryKey(feed);
Set<String> members = jedis.smembers(key); Set<String> members = jedis.smembers(key);

View File

@@ -2,7 +2,7 @@ package com.commafeed.backend.cache;
import lombok.Getter; import lombok.Getter;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.JedisPoolConfig;

View File

@@ -1,19 +1,19 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.commons.lang.ObjectUtils;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.QFeedCategory; import com.commafeed.backend.model.QFeedCategory;
import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.google.common.collect.Lists; import com.querydsl.core.types.Predicate;
import com.mysema.query.types.Predicate;
@Singleton @Singleton
public class FeedCategoryDAO extends GenericDAO<FeedCategory> { public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
@@ -26,11 +26,11 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} }
public List<FeedCategory> findAll(User user) { public List<FeedCategory> findAll(User user) {
return newQuery().from(category).where(category.user.eq(user)).join(category.user, QUser.user).fetch().list(category); return query().selectFrom(category).where(category.user.eq(user)).join(category.user, QUser.user).fetchJoin().fetch();
} }
public FeedCategory findById(User user, Long id) { public FeedCategory findById(User user, Long id) {
return newQuery().from(category).where(category.user.eq(user), category.id.eq(id)).uniqueResult(category); return query().selectFrom(category).where(category.user.eq(user), category.id.eq(id)).fetchOne();
} }
public FeedCategory findByName(User user, String name, FeedCategory parent) { public FeedCategory findByName(User user, String name, FeedCategory parent) {
@@ -40,7 +40,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} else { } else {
parentPredicate = category.parent.eq(parent); parentPredicate = category.parent.eq(parent);
} }
return newQuery().from(category).where(category.user.eq(user), category.name.eq(name), parentPredicate).uniqueResult(category); return query().selectFrom(category).where(category.user.eq(user), category.name.eq(name), parentPredicate).fetchOne();
} }
public List<FeedCategory> findByParent(User user, FeedCategory parent) { public List<FeedCategory> findByParent(User user, FeedCategory parent) {
@@ -50,18 +50,11 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} else { } else {
parentPredicate = category.parent.eq(parent); parentPredicate = category.parent.eq(parent);
} }
return newQuery().from(category).where(category.user.eq(user), parentPredicate).list(category); return query().selectFrom(category).where(category.user.eq(user), parentPredicate).fetch();
} }
public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) { public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {
List<FeedCategory> list = Lists.newArrayList(); return findAll(user).stream().filter(c -> isChild(c, parent)).collect(Collectors.toList());
List<FeedCategory> all = findAll(user);
for (FeedCategory cat : all) {
if (isChild(cat, parent)) {
list.add(cat);
}
}
return list;
} }
private boolean isChild(FeedCategory child, FeedCategory parent) { private boolean isChild(FeedCategory child, FeedCategory parent) {
@@ -70,7 +63,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} }
boolean isChild = false; boolean isChild = false;
while (child != null) { while (child != null) {
if (ObjectUtils.equals(child.getId(), parent.getId())) { if (Objects.equals(child.getId(), parent.getId())) {
isChild = true; isChild = true;
break; break;
} }

View File

@@ -7,7 +7,7 @@ import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
@@ -15,8 +15,9 @@ import com.commafeed.backend.model.QFeed;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.QUser;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.mysema.query.BooleanBuilder; import com.querydsl.jpa.JPAExpressions;
import com.mysema.query.jpa.hibernate.HibernateQuery; import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.hibernate.HibernateQuery;
@Singleton @Singleton
public class FeedDAO extends GenericDAO<Feed> { public class FeedDAO extends GenericDAO<Feed> {
@@ -29,24 +30,23 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) { public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
BooleanBuilder disabledDatePredicate = new BooleanBuilder(); HibernateQuery<Feed> query = query().selectFrom(feed);
disabledDatePredicate.or(feed.disabledUntil.isNull()); query.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())));
disabledDatePredicate.or(feed.disabledUntil.lt(new Date()));
HibernateQuery query = newQuery().from(feed);
if (lastLoginThreshold != null) { if (lastLoginThreshold != null) {
QFeedSubscription subs = QFeedSubscription.feedSubscription; QFeedSubscription subs = QFeedSubscription.feedSubscription;
QUser user = QUser.user; QUser user = QUser.user;
query.join(feed.subscriptions, subs).join(subs.user, user).where(disabledDatePredicate, user.lastLogin.gt(lastLoginThreshold));
} else { JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(subs);
query.where(disabledDatePredicate); subQuery.join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold));
query.where(subQuery.exists());
} }
return query.orderBy(feed.disabledUntil.asc()).limit(count).distinct().list(feed); return query.orderBy(feed.disabledUntil.asc()).limit(count).distinct().fetch();
} }
public Feed findByUrl(String normalizedUrl) { public Feed findByUrl(String normalizedUrl) {
List<Feed> feeds = newQuery().from(feed).where(feed.normalizedUrlHash.eq(DigestUtils.sha1Hex(normalizedUrl))).list(feed); List<Feed> feeds = query().selectFrom(feed).where(feed.normalizedUrlHash.eq(DigestUtils.sha1Hex(normalizedUrl))).fetch();
Feed feed = Iterables.getFirst(feeds, null); Feed feed = Iterables.getFirst(feeds, null);
if (feed != null && StringUtils.equals(normalizedUrl, feed.getNormalizedUrl())) { if (feed != null && StringUtils.equals(normalizedUrl, feed.getNormalizedUrl())) {
return feed; return feed;
@@ -55,11 +55,12 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public List<Feed> findByTopic(String topic) { public List<Feed> findByTopic(String topic) {
return newQuery().from(feed).where(feed.pushTopicHash.eq(DigestUtils.sha1Hex(topic))).list(feed); return query().selectFrom(feed).where(feed.pushTopicHash.eq(DigestUtils.sha1Hex(topic))).fetch();
} }
public List<Feed> findWithoutSubscriptions(int max) { public List<Feed> findWithoutSubscriptions(int max) {
QFeedSubscription sub = QFeedSubscription.feedSubscription; QFeedSubscription sub = QFeedSubscription.feedSubscription;
return newQuery().from(feed).leftJoin(feed.subscriptions, sub).where(sub.id.isNull()).limit(max).list(feed); return query().selectFrom(feed).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(feed)).notExists()).limit(max)
.fetch();
} }
} }

View File

@@ -10,12 +10,14 @@ import org.hibernate.SessionFactory;
import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryContent; import com.commafeed.backend.model.QFeedEntryContent;
import com.google.common.collect.Iterables; import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery;
@Singleton @Singleton
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> { public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
private QFeedEntryContent content = QFeedEntryContent.feedEntryContent; private QFeedEntryContent content = QFeedEntryContent.feedEntryContent;
private QFeedEntry entry = QFeedEntry.feedEntry;
@Inject @Inject
public FeedEntryContentDAO(SessionFactory sessionFactory) { public FeedEntryContentDAO(SessionFactory sessionFactory) {
@@ -23,15 +25,15 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
} }
public Long findExisting(String contentHash, String titleHash) { public Long findExisting(String contentHash, String titleHash) {
List<Long> list = newQuery().from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)).limit(1) return query().select(content.id).from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash))
.list(content.id); .fetchFirst();
return Iterables.getFirst(list, null);
} }
public int deleteWithoutEntries(int max) { public int deleteWithoutEntries(int max) {
QFeedEntry entry = QFeedEntry.feedEntry;
List<FeedEntryContent> list = newQuery().from(content).leftJoin(content.entries, entry).where(entry.id.isNull()).limit(max) JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id));
.list(content); List<FeedEntryContent> list = query().selectFrom(content).where(subQuery.notExists()).limit(max).fetch();
int deleted = list.size(); int deleted = list.size();
delete(list); delete(list);
return deleted; return deleted;

View File

@@ -1,7 +1,7 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -11,10 +11,12 @@ import org.hibernate.SessionFactory;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.QFeed;
import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedSubscription; import com.querydsl.core.Tuple;
import com.google.common.collect.Iterables; import com.querydsl.core.types.dsl.NumberExpression;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Singleton @Singleton
public class FeedEntryDAO extends GenericDAO<FeedEntry> { public class FeedEntryDAO extends GenericDAO<FeedEntry> {
@@ -27,22 +29,32 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
} }
public Long findExisting(String guid, Feed feed) { public Long findExisting(String guid, Feed feed) {
List<Long> list = newQuery().from(entry).where(entry.guidHash.eq(DigestUtils.sha1Hex(guid)), entry.feed.eq(feed)).limit(1) return query().select(entry.id).from(entry).where(entry.guidHash.eq(DigestUtils.sha1Hex(guid)), entry.feed.eq(feed)).limit(1)
.list(entry.id); .fetchOne();
return Iterables.getFirst(list, null);
} }
public List<FeedEntry> findWithoutSubscriptions(int max) { public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
QFeed feed = QFeed.feed; NumberExpression<Long> count = entry.id.count();
QFeedSubscription sub = QFeedSubscription.feedSubscription; List<Tuple> tuples = query().select(entry.feed.id, count).from(entry).groupBy(entry.feed).having(count.gt(maxCapacity)).limit(max)
return newQuery().from(entry).join(entry.feed, feed).leftJoin(feed.subscriptions, sub).where(sub.id.isNull()).limit(max) .fetch();
.list(entry); return tuples.stream().map(t -> new FeedCapacity(t.get(entry.feed.id), t.get(count))).collect(Collectors.toList());
} }
public int delete(Date olderThan, int max) { public int delete(Long feedId, long max) {
List<FeedEntry> list = newQuery().from(entry).where(entry.inserted.lt(olderThan)).limit(max).list(entry);
int deleted = list.size(); List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch();
delete(list); return delete(list);
return deleted; }
public int deleteOldEntries(Long feedId, long max) {
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).orderBy(entry.updated.asc()).limit(max).fetch();
return delete(list);
}
@AllArgsConstructor
@Getter
public static class FeedCapacity {
private Long id;
private Long capacity;
} }
} }

View File

@@ -1,5 +1,6 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -7,12 +8,14 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.commons.lang.StringUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.builder.CompareToBuilder; import org.apache.commons.lang3.builder.CompareToBuilder;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.FixedSizeSortedSet; import com.commafeed.backend.FixedSizeSortedSet;
import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedEntryTag;
@@ -26,11 +29,10 @@ import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.model.UnreadCount; import com.commafeed.frontend.model.UnreadCount;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
import com.mysema.query.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import com.mysema.query.Tuple; import com.querydsl.core.Tuple;
import com.mysema.query.jpa.hibernate.HibernateQuery; import com.querydsl.jpa.hibernate.HibernateQuery;
@Singleton @Singleton
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> { public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
@@ -60,13 +62,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
builder.append(o2.getEntryUpdated(), o1.getEntryUpdated()); builder.append(o2.getEntryUpdated(), o1.getEntryUpdated());
builder.append(o2.getId(), o1.getId()); builder.append(o2.getId(), o1.getId());
return builder.toComparison(); return builder.toComparison();
}; }
}; };
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse(); private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse();
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) { public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
List<FeedEntryStatus> statuses = newQuery().from(status).where(status.entry.eq(entry), status.subscription.eq(sub)).list(status); List<FeedEntryStatus> statuses = query().selectFrom(status).where(status.entry.eq(entry), status.subscription.eq(sub)).fetch();
FeedEntryStatus status = Iterables.getFirst(statuses, null); FeedEntryStatus status = Iterables.getFirst(statuses, null);
return handleStatus(user, status, sub, entry); return handleStatus(user, status, sub, entry);
} }
@@ -91,7 +93,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
} }
public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) { public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) {
HibernateQuery query = newQuery().from(status).where(status.user.eq(user), status.starred.isTrue()); HibernateQuery<FeedEntryStatus> query = query().selectFrom(status).where(status.user.eq(user), status.starred.isTrue());
if (newerThan != null) { if (newerThan != null) {
query.where(status.entryInserted.gt(newerThan)); query.where(status.entryInserted.gt(newerThan));
} }
@@ -102,9 +104,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
query.orderBy(status.entryUpdated.desc(), status.id.desc()); query.orderBy(status.entryUpdated.desc(), status.id.desc());
} }
query.offset(offset).limit(limit).setTimeout(config.getApplicationSettings().getQueryTimeout()); query.offset(offset).limit(limit);
int timeout = config.getApplicationSettings().getQueryTimeout();
if (timeout > 0) {
query.setTimeout(timeout / 1000);
}
List<FeedEntryStatus> statuses = query.list(status); List<FeedEntryStatus> statuses = query.fetch();
for (FeedEntryStatus status : statuses) { for (FeedEntryStatus status : statuses) {
status = handleStatus(user, status, status.getSubscription(), status.getEntry()); status = handleStatus(user, status, status.getSubscription(), status.getEntry());
fetchTags(user, status); fetchTags(user, status);
@@ -112,18 +118,21 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return lazyLoadContent(includeContent, statuses); return lazyLoadContent(includeContent, statuses);
} }
private HibernateQuery buildQuery(User user, FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset, private HibernateQuery<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords,
int limit, ReadingOrder order, Date last, String tag) { Date newerThan, int offset, int limit, ReadingOrder order, Date last, String tag) {
HibernateQuery query = newQuery().from(entry).where(entry.feed.eq(sub.getFeed())); HibernateQuery<FeedEntry> query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed()));
if (keywords != null) { if (CollectionUtils.isNotEmpty(keywords)) {
query.join(entry.content, content); query.join(entry.content, content);
for (String keyword : StringUtils.split(keywords)) { for (FeedEntryKeyword keyword : keywords) {
BooleanBuilder or = new BooleanBuilder(); BooleanBuilder or = new BooleanBuilder();
or.or(content.content.containsIgnoreCase(keyword)); or.or(content.content.containsIgnoreCase(keyword.getKeyword()));
or.or(content.title.containsIgnoreCase(keyword)); or.or(content.title.containsIgnoreCase(keyword.getKeyword()));
if (keyword.getMode() == Mode.EXCLUDE) {
or.not();
}
query.where(or); query.where(or);
} }
} }
@@ -180,15 +189,16 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return query; return query;
} }
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly, String keywords, public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds, String tag) { List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
boolean onlyIds, String tag) {
int capacity = offset + limit; int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC; Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator); FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator);
for (FeedSubscription sub : subs) { for (FeedSubscription sub : subs) {
Date last = (order != null && set.isFull()) ? set.last().getEntryUpdated() : null; Date last = (order != null && set.isFull()) ? set.last().getEntryUpdated() : null;
HibernateQuery query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag); HibernateQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag);
List<Tuple> tuples = query.list(entry.id, entry.updated, status.id); List<Tuple> tuples = query.select(entry.id, entry.updated, status.id).fetch();
for (Tuple tuple : tuples) { for (Tuple tuple : tuples) {
Long id = tuple.get(entry.id); Long id = tuple.get(entry.id);
Date updated = tuple.get(entry.updated); Date updated = tuple.get(entry.updated);
@@ -211,7 +221,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
List<FeedEntryStatus> placeholders = set.asList(); List<FeedEntryStatus> placeholders = set.asList();
int size = placeholders.size(); int size = placeholders.size();
if (size < offset) { if (size < offset) {
return Lists.newArrayList(); return new ArrayList<>();
} }
placeholders = placeholders.subList(Math.max(offset, 0), size); placeholders = placeholders.subList(Math.max(offset, 0), size);
@@ -219,7 +229,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
if (onlyIds) { if (onlyIds) {
statuses = placeholders; statuses = placeholders;
} else { } else {
statuses = Lists.newArrayList(); statuses = new ArrayList<>();
for (FeedEntryStatus placeholder : placeholders) { for (FeedEntryStatus placeholder : placeholders) {
Long statusId = placeholder.getId(); Long statusId = placeholder.getId();
FeedEntry entry = feedEntryDAO.findById(placeholder.getEntry().getId()); FeedEntry entry = feedEntryDAO.findById(placeholder.getEntry().getId());
@@ -235,8 +245,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) { public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
UnreadCount uc = null; UnreadCount uc = null;
HibernateQuery query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null); HibernateQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null);
List<Tuple> tuples = query.list(entry.count(), entry.updated.max()); List<Tuple> tuples = query.select(entry.count(), entry.updated.max()).fetch();
for (Tuple tuple : tuples) { for (Tuple tuple : tuples) {
Long count = tuple.get(entry.count()); Long count = tuple.get(entry.count());
Date updated = tuple.get(entry.updated.max()); Date updated = tuple.get(entry.updated.max());
@@ -256,7 +266,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
} }
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) { public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) {
return newQuery().from(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).list(status); return query().selectFrom(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).fetch();
} }
} }

View File

@@ -23,10 +23,10 @@ public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
} }
public List<String> findByUser(User user) { public List<String> findByUser(User user) {
return newQuery().from(tag).where(tag.user.eq(user)).distinct().list(tag.name); return query().selectDistinct(tag.name).from(tag).where(tag.user.eq(user)).fetch();
} }
public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) { public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) {
return newQuery().from(tag).where(tag.user.eq(user), tag.entry.eq(entry)).list(tag); return query().selectFrom(tag).where(tag.user.eq(user), tag.entry.eq(entry)).fetch();
} }
} }

View File

@@ -1,6 +1,8 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -13,10 +15,8 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models; import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.google.common.base.Function;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.querydsl.jpa.hibernate.HibernateQuery;
import com.mysema.query.jpa.hibernate.HibernateQuery;
@Singleton @Singleton
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> { public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
@@ -29,57 +29,44 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
} }
public FeedSubscription findById(User user, Long id) { public FeedSubscription findById(User user, Long id) {
List<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user), sub.id.eq(id)).leftJoin(sub.feed).fetch() List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user), sub.id.eq(id)).leftJoin(sub.feed).fetchJoin()
.leftJoin(sub.category).fetch().list(sub); .leftJoin(sub.category).fetchJoin().fetch();
return initRelations(Iterables.getFirst(subs, null)); return initRelations(Iterables.getFirst(subs, null));
} }
public List<FeedSubscription> findByFeed(Feed feed) { public List<FeedSubscription> findByFeed(Feed feed) {
return newQuery().from(sub).where(sub.feed.eq(feed)).list(sub); return query().selectFrom(sub).where(sub.feed.eq(feed)).fetch();
} }
public FeedSubscription findByFeed(User user, Feed feed) { public FeedSubscription findByFeed(User user, Feed feed) {
List<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user), sub.feed.eq(feed)).list(sub); List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user), sub.feed.eq(feed)).fetch();
return initRelations(Iterables.getFirst(subs, null)); return initRelations(Iterables.getFirst(subs, null));
} }
public List<FeedSubscription> findAll(User user) { public List<FeedSubscription> findAll(User user) {
List<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user)).leftJoin(sub.feed).fetch().leftJoin(sub.category) List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user)).leftJoin(sub.feed).fetchJoin()
.fetch().list(sub); .leftJoin(sub.category).fetchJoin().fetch();
return initRelations(subs); return initRelations(subs);
} }
public List<FeedSubscription> findByCategory(User user, FeedCategory category) { public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
HibernateQuery query = newQuery().from(sub).where(sub.user.eq(user)); HibernateQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user));
if (category == null) { if (category == null) {
query.where(sub.category.isNull()); query.where(sub.category.isNull());
} else { } else {
query.where(sub.category.eq(category)); query.where(sub.category.eq(category));
} }
return initRelations(query.list(sub)); return initRelations(query.fetch());
} }
public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) { public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {
List<Long> categoryIds = Lists.transform(categories, new Function<FeedCategory, Long>() { Set<Long> categoryIds = categories.stream().map(c -> c.getId()).collect(Collectors.toSet());
@Override return findAll(user).stream().filter(s -> s.getCategory() != null && categoryIds.contains(s.getCategory().getId()))
public Long apply(FeedCategory input) { .collect(Collectors.toList());
return input.getId();
}
});
List<FeedSubscription> subscriptions = Lists.newArrayList();
for (FeedSubscription sub : findAll(user)) {
if (sub.getCategory() != null && categoryIds.contains(sub.getCategory().getId())) {
subscriptions.add(sub);
}
}
return subscriptions;
} }
private List<FeedSubscription> initRelations(List<FeedSubscription> list) { private List<FeedSubscription> initRelations(List<FeedSubscription> list) {
for (FeedSubscription sub : list) { list.forEach(s -> initRelations(s));
initRelations(sub);
}
return list; return list;
} }

View File

@@ -1,22 +1,25 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import io.dropwizard.hibernate.AbstractDAO;
import java.util.Collection; import java.util.Collection;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.mysema.query.jpa.hibernate.HibernateQuery; import com.querydsl.jpa.hibernate.HibernateQueryFactory;
import io.dropwizard.hibernate.AbstractDAO;
public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> { public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> {
private HibernateQueryFactory factory;
protected GenericDAO(SessionFactory sessionFactory) { protected GenericDAO(SessionFactory sessionFactory) {
super(sessionFactory); super(sessionFactory);
this.factory = new HibernateQueryFactory(() -> currentSession());
} }
protected HibernateQuery newQuery() { protected HibernateQueryFactory query() {
return new HibernateQuery(currentSession()); return factory;
} }
public void saveOrUpdate(T model) { public void saveOrUpdate(T model) {
@@ -24,19 +27,7 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
} }
public void saveOrUpdate(Collection<T> models) { public void saveOrUpdate(Collection<T> models) {
for (T model : models) { models.forEach(m -> persist(m));
persist(model);
}
}
public void merge(T model) {
currentSession().merge(model);
}
public void merge(Collection<T> models) {
for (T model : models) {
merge(model);
}
} }
public T findById(Long id) { public T findById(Long id) {
@@ -50,9 +41,7 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
} }
public int delete(Collection<T> objects) { public int delete(Collection<T> objects) {
for (T object : objects) { objects.forEach(o -> delete(o));
delete(object);
}
return objects.size(); return objects.size();
} }

View File

@@ -5,17 +5,26 @@ import org.hibernate.SessionFactory;
import org.hibernate.Transaction; import org.hibernate.Transaction;
import org.hibernate.context.internal.ManagedSessionContext; import org.hibernate.context.internal.ManagedSessionContext;
public abstract class UnitOfWork<T> { public class UnitOfWork {
private SessionFactory sessionFactory; @FunctionalInterface
public static interface SessionRunner {
public UnitOfWork(SessionFactory sessionFactory) { public void runInSession();
this.sessionFactory = sessionFactory;
} }
protected abstract T runInSession() throws Exception; @FunctionalInterface
public static interface SessionRunnerReturningValue<T> {
public T runInSession();
}
public T run() { public static void run(SessionFactory sessionFactory, SessionRunner sessionRunner) {
call(sessionFactory, () -> {
sessionRunner.runInSession();
return null;
});
}
public static <T> T call(SessionFactory sessionFactory, SessionRunnerReturningValue<T> sessionRunner) {
final Session session = sessionFactory.openSession(); final Session session = sessionFactory.openSession();
if (ManagedSessionContext.hasBind(sessionFactory)) { if (ManagedSessionContext.hasBind(sessionFactory)) {
throw new IllegalStateException("Already in a unit of work!"); throw new IllegalStateException("Already in a unit of work!");
@@ -25,11 +34,11 @@ public abstract class UnitOfWork<T> {
ManagedSessionContext.bind(session); ManagedSessionContext.bind(session);
session.beginTransaction(); session.beginTransaction();
try { try {
t = runInSession(); t = sessionRunner.runInSession();
commitTransaction(session); commitTransaction(session);
} catch (Exception e) { } catch (Exception e) {
rollbackTransaction(session); rollbackTransaction(session);
this.<RuntimeException> rethrow(e); UnitOfWork.<RuntimeException> rethrow(e);
} }
} finally { } finally {
session.close(); session.close();
@@ -38,14 +47,14 @@ public abstract class UnitOfWork<T> {
return t; return t;
} }
private void rollbackTransaction(Session session) { private static void rollbackTransaction(Session session) {
final Transaction txn = session.getTransaction(); final Transaction txn = session.getTransaction();
if (txn != null && txn.isActive()) { if (txn != null && txn.isActive()) {
txn.rollback(); txn.rollback();
} }
} }
private void commitTransaction(Session session) { private static void commitTransaction(Session session) {
final Transaction txn = session.getTransaction(); final Transaction txn = session.getTransaction();
if (txn != null && txn.isActive()) { if (txn != null && txn.isActive()) {
txn.commit(); txn.commit();
@@ -53,7 +62,7 @@ public abstract class UnitOfWork<T> {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <E extends Exception> void rethrow(Exception e) throws E { private static <E extends Exception> void rethrow(Exception e) throws E {
throw (E) e; throw (E) e;
} }

View File

@@ -6,7 +6,6 @@ import javax.inject.Singleton;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.QUserRole;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
@Singleton @Singleton
@@ -20,21 +19,18 @@ public class UserDAO extends GenericDAO<User> {
} }
public User findByName(String name) { public User findByName(String name) {
return newQuery().from(user).where(user.name.equalsIgnoreCase(name)).leftJoin(user.roles, QUserRole.userRole).fetch() return query().selectFrom(user).where(user.name.equalsIgnoreCase(name)).fetchOne();
.uniqueResult(user);
} }
public User findByApiKey(String key) { public User findByApiKey(String key) {
return newQuery().from(user).where(user.apiKey.equalsIgnoreCase(key)).leftJoin(user.roles, QUserRole.userRole).fetch() return query().selectFrom(user).where(user.apiKey.equalsIgnoreCase(key)).fetchOne();
.uniqueResult(user);
} }
public User findByEmail(String email) { public User findByEmail(String email) {
return newQuery().from(user).where(user.email.equalsIgnoreCase(email)).leftJoin(user.roles, QUserRole.userRole).fetch() return query().selectFrom(user).where(user.email.equalsIgnoreCase(email)).fetchOne();
.uniqueResult(user);
} }
public long count() { public long count() {
return newQuery().from(user).count(); return query().selectFrom(user).fetchCount();
} }
} }

View File

@@ -2,6 +2,7 @@ package com.commafeed.backend.dao;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -12,7 +13,6 @@ import com.commafeed.backend.model.QUserRole;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.google.common.collect.Sets;
@Singleton @Singleton
public class UserRoleDAO extends GenericDAO<UserRole> { public class UserRoleDAO extends GenericDAO<UserRole> {
@@ -25,18 +25,14 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
} }
public List<UserRole> findAll() { public List<UserRole> findAll() {
return newQuery().from(role).leftJoin(role.user).fetch().distinct().list(role); return query().selectFrom(role).leftJoin(role.user).fetchJoin().distinct().fetch();
} }
public List<UserRole> findAll(User user) { public List<UserRole> findAll(User user) {
return newQuery().from(role).where(role.user.eq(user)).distinct().list(role); return query().selectFrom(role).where(role.user.eq(user)).distinct().fetch();
} }
public Set<Role> findRoles(User user) { public Set<Role> findRoles(User user) {
Set<Role> list = Sets.newHashSet(); return findAll(user).stream().map(r -> r.getRole()).collect(Collectors.toSet());
for (UserRole role : findAll(user)) {
list.add(role.getRole());
}
return list;
} }
} }

View File

@@ -20,6 +20,6 @@ public class UserSettingsDAO extends GenericDAO<UserSettings> {
} }
public UserSettings findByUser(User user) { public UserSettings findByUser(User user) {
return newQuery().from(settings).where(settings.user.eq(user)).uniqueResult(settings); return query().selectFrom(settings).where(settings.user.eq(user)).fetchFirst();
} }
} }

View File

@@ -3,9 +3,11 @@ package com.commafeed.backend.favicon;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
@@ -18,7 +20,7 @@ public abstract class AbstractFaviconFetcher {
protected static int TIMEOUT = 4000; protected static int TIMEOUT = 4000;
public abstract byte[] fetch(Feed feed); public abstract Favicon fetch(Feed feed);
protected boolean isValidIconResponse(byte[] content, String contentType) { protected boolean isValidIconResponse(byte[] content, String contentType) {
if (content == null) { if (content == null) {
@@ -48,4 +50,11 @@ public abstract class AbstractFaviconFetcher {
return true; return true;
} }
@RequiredArgsConstructor
@Getter
public static class Favicon {
private final byte[] icon;
private final String mediaType;
}
} }

View File

@@ -6,7 +6,7 @@ import javax.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
@@ -28,9 +28,15 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
private final HttpGetter getter; private final HttpGetter getter;
@Override @Override
public byte[] fetch(Feed feed) { public Favicon fetch(Feed feed) {
String url = feed.getLink() != null ? feed.getLink() : feed.getUrl(); Favicon icon = fetch(feed.getLink());
if (icon == null) {
icon = fetch(feed.getUrl());
}
return icon;
}
private Favicon fetch(String url) {
if (url == null) { if (url == null) {
log.debug("url is null"); log.debug("url is null");
return null; return null;
@@ -47,7 +53,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
url = url.substring(0, firstSlash); url = url.substring(0, firstSlash);
} }
byte[] icon = getIconAtRoot(url); Favicon icon = getIconAtRoot(url);
if (icon == null) { if (icon == null) {
icon = getIconInPage(url); icon = getIconInPage(url);
@@ -56,7 +62,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
return icon; return icon;
} }
private byte[] getIconAtRoot(String url) { private Favicon getIconAtRoot(String url) {
byte[] bytes = null; byte[] bytes = null;
String contentType = null; String contentType = null;
@@ -67,23 +73,25 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
bytes = result.getContent(); bytes = result.getContent();
contentType = result.getContentType(); contentType = result.getContentType();
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to retrieve iconAtRoot for url {}: ", url, e); log.debug("Failed to retrieve iconAtRoot for url {}: ", url);
log.trace("Failed to retrieve iconAtRoot for url {}: ", url, e);
} }
if (!isValidIconResponse(bytes, contentType)) { if (!isValidIconResponse(bytes, contentType)) {
bytes = null; return null;
} }
return bytes; return new Favicon(bytes, contentType);
} }
private byte[] getIconInPage(String url) { private Favicon getIconInPage(String url) {
Document doc = null; Document doc = null;
try { try {
HttpResult result = getter.getBinary(url, TIMEOUT); HttpResult result = getter.getBinary(url, TIMEOUT);
doc = Jsoup.parse(new String(result.getContent()), url); doc = Jsoup.parse(new String(result.getContent()), url);
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to retrieve page to find icon", e); log.debug("Failed to retrieve page to find icon");
log.trace("Failed to retrieve page to find icon", e);
return null; return null;
} }
@@ -109,7 +117,8 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
bytes = result.getContent(); bytes = result.getContent();
contentType = result.getContentType(); contentType = result.getContentType();
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to retrieve icon found in page {}", href, e); log.debug("Failed to retrieve icon found in page {}", href);
log.trace("Failed to retrieve icon found in page {}", href, e);
return null; return null;
} }
@@ -118,6 +127,6 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
return null; return null;
} }
return bytes; return new Favicon(bytes, contentType);
} }
} }

View File

@@ -0,0 +1,79 @@
package com.commafeed.backend.favicon;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class FacebookFaviconFetcher extends AbstractFaviconFetcher {
private final HttpGetter getter;
@Override
public Favicon fetch(Feed feed) {
String url = feed.getUrl();
if (!url.toLowerCase().contains("www.facebook.com")) {
return null;
}
String userName = extractUserName(url);
if (userName == null) {
return null;
}
String iconUrl = String.format("https://graph.facebook.com/%s/picture?type=square&height=16", userName);
byte[] bytes = null;
String contentType = null;
try {
log.debug("Getting Facebook user's icon, {}", url);
HttpResult iconResult = getter.getBinary(iconUrl, TIMEOUT);
bytes = iconResult.getContent();
contentType = iconResult.getContentType();
} catch (Exception e) {
log.debug("Failed to retrieve Facebook icon", e);
}
if (!isValidIconResponse(bytes, contentType)) {
return null;
}
return new Favicon(bytes, contentType);
}
private String extractUserName(String url) {
URI uri = null;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
log.debug("could not parse url", e);
return null;
}
List<NameValuePair> params = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8.name());
for (NameValuePair param : params) {
if ("id".equals(param.getName())) {
return param.getValue();
}
}
return null;
}
}

View File

@@ -1,66 +1,91 @@
package com.commafeed.backend.favicon; package com.commafeed.backend.favicon;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.RequiredArgsConstructor; import org.apache.http.NameValuePair;
import lombok.extern.slf4j.Slf4j; import org.apache.http.client.utils.URLEncodedUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.youtube.YouTube;
import com.google.api.services.youtube.model.Channel;
import com.google.api.services.youtube.model.ChannelListResponse;
import com.google.api.services.youtube.model.Thumbnail;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }) )
@Singleton @Singleton
public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
private final HttpGetter getter; private final HttpGetter getter;
private final CommaFeedConfiguration config;
@Override @Override
public byte[] fetch(Feed feed) { public Favicon fetch(Feed feed) {
String url = feed.getUrl(); String url = feed.getUrl();
if (!url.toLowerCase().contains("://gdata.youtube.com/")) { if (!url.toLowerCase().contains("youtube.com/feeds/videos.xml")) {
return null; return null;
} }
String userName = extractUserName(url); String googleAuthKey = config.getApplicationSettings().getGoogleAuthKey();
if (userName == null) { if (googleAuthKey == null) {
log.debug("no google auth key configured");
return null; return null;
} }
String profileUrl = "https://gdata.youtube.com/feeds/users/" + userName;
byte[] bytes = null; byte[] bytes = null;
String contentType = null; String contentType = null;
try { try {
log.debug("Getting YouTube user's icon, {}", url); List<NameValuePair> params = URLEncodedUtils.parse(url.substring(url.indexOf("?") + 1), StandardCharsets.UTF_8);
Optional<NameValuePair> userId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("user")).findFirst();
// initial get to translate username to obscure user thumbnail URL Optional<NameValuePair> channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id")).findFirst();
HttpResult profileResult = getter.getBinary(profileUrl, TIMEOUT); if (!userId.isPresent() && !channelId.isPresent()) {
Document doc = Jsoup.parse(new String(profileResult.getContent()), profileUrl);
Elements thumbnails = doc.select("media|thumbnail");
if (thumbnails.isEmpty()) {
return null; return null;
} }
String thumbnailUrl = thumbnails.get(0).attr("abs:url"); YouTube youtube = new YouTube.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(),
new HttpRequestInitializer() {
@Override
public void initialize(HttpRequest request) throws IOException {
}
}).setApplicationName("CommaFeed").build();
int thumbnailStart = thumbnailUrl.indexOf("<media:thumbnail url='"); YouTube.Channels.List list = youtube.channels().list("snippet");
int thumbnailEnd = thumbnailUrl.indexOf("'/>", thumbnailStart); list.setKey(googleAuthKey);
if (thumbnailStart != -1) { if (userId.isPresent()) {
thumbnailUrl = thumbnailUrl.substring(thumbnailStart + "<media:thumbnail url='".length(), thumbnailEnd); list.setForUsername(userId.get().getValue());
} else {
list.setId(channelId.get().getValue());
} }
// final get to actually retrieve the thumbnail log.debug("contacting youtube api");
HttpResult iconResult = getter.getBinary(thumbnailUrl, TIMEOUT); ChannelListResponse response = list.execute();
if (response.getItems().isEmpty()) {
log.debug("youtube api returned no items");
return null;
}
Channel channel = response.getItems().get(0);
Thumbnail thumbnail = channel.getSnippet().getThumbnails().getDefault();
log.debug("fetching favicon");
HttpResult iconResult = getter.getBinary(thumbnail.getUrl(), TIMEOUT);
bytes = iconResult.getContent(); bytes = iconResult.getContent();
contentType = iconResult.getContentType(); contentType = iconResult.getContentType();
} catch (Exception e) { } catch (Exception e) {
@@ -68,23 +93,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
} }
if (!isValidIconResponse(bytes, contentType)) { if (!isValidIconResponse(bytes, contentType)) {
bytes = null;
}
return bytes;
}
private String extractUserName(String url) {
int apiOrBase = url.indexOf("/users/");
if (apiOrBase == -1) {
return null; return null;
} }
return new Favicon(bytes, contentType);
int userEndSlash = url.indexOf('/', apiOrBase + "/users/".length());
if (userEndSlash == -1) {
return null;
}
return url.substring(apiOrBase + "/users/".length(), userEndSlash);
} }
} }

View File

@@ -0,0 +1,39 @@
package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
/**
* A keyword used in a search query
*/
@Getter
@RequiredArgsConstructor
public class FeedEntryKeyword {
public static enum Mode {
INCLUDE, EXCLUDE;
}
private final String keyword;
private final Mode mode;
public static List<FeedEntryKeyword> fromQueryString(String keywords) {
List<FeedEntryKeyword> list = new ArrayList<>();
if (keywords != null) {
for (String keyword : StringUtils.split(keywords)) {
boolean not = false;
if (keyword.startsWith("-") || keyword.startsWith("!")) {
not = true;
keyword = keyword.substring(1);
}
list.add(new FeedEntryKeyword(keyword, not ? Mode.EXCLUDE : Mode.INCLUDE));
}
}
return list;
}
}

View File

@@ -41,16 +41,16 @@ public class FeedFetcher {
byte[] content = result.getContent(); byte[] content = result.getContent();
try { try {
fetchedFeed = parser.parse(feedUrl, content); fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
} catch (FeedException e) { } catch (FeedException e) {
if (extractFeedUrlFromHtml) { if (extractFeedUrlFromHtml) {
String extractedUrl = extractFeedUrl(StringUtils.newStringUtf8(result.getContent()), feedUrl); String extractedUrl = extractFeedUrl(StringUtils.newStringUtf8(result.getContent()), feedUrl);
if (org.apache.commons.lang.StringUtils.isNotBlank(extractedUrl)) { if (org.apache.commons.lang3.StringUtils.isNotBlank(extractedUrl)) {
feedUrl = extractedUrl; feedUrl = extractedUrl;
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout); result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
content = result.getContent(); content = result.getContent();
fetchedFeed = parser.parse(feedUrl, content); fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
} else { } else {
throw e; throw e;
} }

View File

@@ -1,9 +1,11 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.io.StringReader; import java.io.StringReader;
import java.nio.charset.Charset;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -11,8 +13,7 @@ import javax.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang.SystemUtils;
import org.jdom2.Element; import org.jdom2.Element;
import org.jdom2.Namespace; import org.jdom2.Namespace;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
@@ -20,10 +21,7 @@ import org.xml.sax.InputSource;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.rometools.rome.feed.synd.SyndContent;
import com.rometools.rome.feed.synd.SyndEnclosure; import com.rometools.rome.feed.synd.SyndEnclosure;
import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.feed.synd.SyndFeed;
@@ -43,20 +41,13 @@ public class FeedParser {
private static final Date START = new Date(86400000); private static final Date START = new Date(86400000);
private static final Date END = new Date(1000l * Integer.MAX_VALUE - 86400000); private static final Date END = new Date(1000l * Integer.MAX_VALUE - 86400000);
private static final Function<SyndContent, String> CONTENT_TO_STRING = new Function<SyndContent, String>() {
@Override
public String apply(SyndContent content) {
return content.getValue();
}
};
public FetchedFeed parse(String feedUrl, byte[] xml) throws FeedException { public FetchedFeed parse(String feedUrl, byte[] xml) throws FeedException {
FetchedFeed fetchedFeed = new FetchedFeed(); FetchedFeed fetchedFeed = new FetchedFeed();
Feed feed = fetchedFeed.getFeed(); Feed feed = fetchedFeed.getFeed();
List<FeedEntry> entries = fetchedFeed.getEntries(); List<FeedEntry> entries = fetchedFeed.getEntries();
try { try {
String encoding = FeedUtils.guessEncoding(xml); Charset encoding = FeedUtils.guessEncoding(xml);
String xmlString = FeedUtils.trimInvalidXmlCharacters(new String(xml, encoding)); String xmlString = FeedUtils.trimInvalidXmlCharacters(new String(xml, encoding));
if (xmlString == null) { if (xmlString == null) {
throw new FeedException("Input string is null for url " + feedUrl); throw new FeedException("Input string is null for url " + feedUrl);
@@ -86,7 +77,7 @@ public class FeedParser {
} }
entry.setGuid(FeedUtils.truncate(guid, 2048)); entry.setGuid(FeedUtils.truncate(guid, 2048));
entry.setUpdated(validateDate(getEntryUpdateDate(item), true)); entry.setUpdated(validateDate(getEntryUpdateDate(item), true));
entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink(), feed.getUrlAfterRedirect()), 2048)); entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink(), feedUrl), 2048));
// if link is empty but guid is used as url // if link is empty but guid is used as url
if (StringUtils.isBlank(entry.getUrl()) && StringUtils.startsWith(entry.getGuid(), "http")) { if (StringUtils.isBlank(entry.getUrl()) && StringUtils.startsWith(entry.getGuid(), "http")) {
@@ -95,6 +86,8 @@ public class FeedParser {
FeedEntryContent content = new FeedEntryContent(); FeedEntryContent content = new FeedEntryContent();
content.setContent(getContent(item)); content.setContent(getContent(item));
content.setCategories(FeedUtils.truncate(
item.getCategories().stream().map(c -> c.getName()).collect(Collectors.joining(", ")), 4096));
content.setTitle(getTitle(item)); content.setTitle(getTitle(item));
content.setAuthor(StringUtils.trimToNull(item.getAuthor())); content.setAuthor(StringUtils.trimToNull(item.getAuthor()));
SyndEnclosure enclosure = Iterables.getFirst(item.getEnclosures(), null); SyndEnclosure enclosure = Iterables.getFirst(item.getEnclosures(), null);
@@ -173,7 +166,7 @@ public class FeedParser {
if (item.getContents().isEmpty()) { if (item.getContents().isEmpty()) {
content = item.getDescription() == null ? null : item.getDescription().getValue(); content = item.getDescription() == null ? null : item.getDescription().getValue();
} else { } else {
content = StringUtils.join(Collections2.transform(item.getContents(), CONTENT_TO_STRING), SystemUtils.LINE_SEPARATOR); content = item.getContents().stream().map(c -> c.getValue()).collect(Collectors.joining(System.lineSeparator()));
} }
return StringUtils.trimToNull(content); return StringUtils.trimToNull(content);
} }

View File

@@ -1,40 +1,45 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory;
import com.codahale.metrics.Gauge; import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
@Singleton @Singleton
public class FeedQueues { public class FeedQueues {
private SessionFactory sessionFactory;
private final FeedDAO feedDAO; private final FeedDAO feedDAO;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
private Queue<FeedRefreshContext> addQueue = Queues.newConcurrentLinkedQueue(); private Queue<FeedRefreshContext> addQueue = new ConcurrentLinkedQueue<>();
private Queue<FeedRefreshContext> takeQueue = Queues.newConcurrentLinkedQueue(); private Queue<FeedRefreshContext> takeQueue = new ConcurrentLinkedQueue<>();
private Queue<Feed> giveBackQueue = Queues.newConcurrentLinkedQueue(); private Queue<Feed> giveBackQueue = new ConcurrentLinkedQueue<>();
private Meter refill; private Meter refill;
@Inject @Inject
public FeedQueues(FeedDAO feedDAO, CommaFeedConfiguration config, MetricRegistry metrics) { public FeedQueues(SessionFactory sessionFactory, FeedDAO feedDAO, CommaFeedConfiguration config, MetricRegistry metrics) {
this.sessionFactory = sessionFactory;
this.config = config; this.config = config;
this.feedDAO = feedDAO; this.feedDAO = feedDAO;
@@ -78,13 +83,7 @@ public class FeedQueues {
public void add(Feed feed, boolean urgent) { public void add(Feed feed, boolean urgent) {
int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes(); int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes();
if (feed.getLastUpdated() == null || feed.getLastUpdated().before(DateUtils.addMinutes(new Date(), -1 * refreshInterval))) { if (feed.getLastUpdated() == null || feed.getLastUpdated().before(DateUtils.addMinutes(new Date(), -1 * refreshInterval))) {
boolean alreadyQueued = false; boolean alreadyQueued = addQueue.stream().anyMatch(c -> c.getFeed().getId().equals(feed.getId()));
for (FeedRefreshContext context : addQueue) {
if (context.getFeed().getId().equals(feed.getId())) {
alreadyQueued = true;
break;
}
}
if (!alreadyQueued) { if (!alreadyQueued) {
addQueue.add(new FeedRefreshContext(feed, urgent)); addQueue.add(new FeedRefreshContext(feed, urgent));
} }
@@ -97,7 +96,7 @@ public class FeedQueues {
private void refill() { private void refill() {
refill.mark(); refill.mark();
List<FeedRefreshContext> contexts = Lists.newArrayList(); List<FeedRefreshContext> contexts = new ArrayList<>();
int batchSize = Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads()); int batchSize = Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
// add feeds we got from the add() method // add feeds we got from the add() method
@@ -109,7 +108,7 @@ public class FeedQueues {
// add feeds that are up to refresh from the database // add feeds that are up to refresh from the database
int count = batchSize - contexts.size(); int count = batchSize - contexts.size();
if (count > 0) { if (count > 0) {
List<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold()); List<Feed> feeds = UnitOfWork.call(sessionFactory, () -> feedDAO.findNextUpdatable(count, getLastLoginThreshold()));
for (Feed feed : feeds) { for (Feed feed : feeds) {
contexts.add(new FeedRefreshContext(feed, false)); contexts.add(new FeedRefreshContext(feed, false));
} }
@@ -117,7 +116,7 @@ public class FeedQueues {
// set the disabledDate as we use it in feedDAO to decide what to refresh next. We also use a map to remove // set the disabledDate as we use it in feedDAO to decide what to refresh next. We also use a map to remove
// duplicates. // duplicates.
Map<Long, FeedRefreshContext> map = Maps.newLinkedHashMap(); Map<Long, FeedRefreshContext> map = new LinkedHashMap<>();
for (FeedRefreshContext context : contexts) { for (FeedRefreshContext context : contexts) {
Feed feed = context.getFeed(); Feed feed = context.getFeed();
feed.setDisabledUntil(DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes())); feed.setDisabledUntil(DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes()));
@@ -135,11 +134,8 @@ public class FeedQueues {
} }
// update all feeds in the database // update all feeds in the database
List<Feed> feeds = Lists.newArrayList(); List<Feed> feeds = map.values().stream().map(c -> c.getFeed()).collect(Collectors.toList());
for (FeedRefreshContext context : map.values()) { UnitOfWork.run(sessionFactory, () -> feedDAO.saveOrUpdate(feeds));
feeds.add(context.getFeed());
}
feedDAO.merge(feeds);
} }
/** /**
@@ -154,7 +150,7 @@ public class FeedQueues {
} }
private Date getLastLoginThreshold() { private Date getLastLoginThreshold() {
if (config.getApplicationSettings().isHeavyLoad()) { if (config.getApplicationSettings().getHeavyLoad()) {
return DateUtils.addDays(new Date(), -30); return DateUtils.addDays(new Date(), -30);
} else { } else {
return null; return null;

View File

@@ -2,9 +2,14 @@ package com.commafeed.backend.feed;
import java.util.List; import java.util.List;
import lombok.Getter;
import lombok.Setter;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
@Getter
@Setter
public class FeedRefreshContext { public class FeedRefreshContext {
private Feed feed; private Feed feed;
private List<FeedEntry> entries; private List<FeedEntry> entries;
@@ -14,29 +19,4 @@ public class FeedRefreshContext {
this.feed = feed; this.feed = feed;
this.urgent = isUrgent; this.urgent = isUrgent;
} }
public Feed getFeed() {
return feed;
}
public void setFeed(Feed feed) {
this.feed = feed;
}
public boolean isUrgent() {
return urgent;
}
public void setUrgent(boolean urgent) {
this.urgent = urgent;
}
public List<FeedEntry> getEntries() {
return entries;
}
public void setEntries(List<FeedEntry> entries) {
this.entries = entries;
}
} }

View File

@@ -5,11 +5,11 @@ import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import com.codahale.metrics.Gauge; import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import lombok.extern.slf4j.Slf4j;
/** /**
* Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using * 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} * {@link Task} instead of {@link Runnable}
@@ -37,7 +37,14 @@ public class FeedRefreshExecutor {
return offerLast(r); return offerLast(r);
} }
} }
}); }) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (t != null) {
log.error("thread from pool {} threw a runtime exception", poolName, t);
}
}
};
pool.setRejectedExecutionHandler(new RejectedExecutionHandler() { pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override @Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {

View File

@@ -10,13 +10,10 @@ import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.UnitOfWork;
/** /**
* Infinite loop fetching feeds from @FeedQueues and queuing them to the {@link FeedRefreshWorker} pool. * Infinite loop fetching feeds from @FeedQueues and queuing them to the {@link FeedRefreshWorker} pool.
@@ -26,7 +23,6 @@ import com.commafeed.backend.dao.UnitOfWork;
@Singleton @Singleton
public class FeedRefreshTaskGiver implements Managed { public class FeedRefreshTaskGiver implements Managed {
private final SessionFactory sessionFactory;
private final FeedQueues queues; private final FeedQueues queues;
private final FeedRefreshWorker worker; private final FeedRefreshWorker worker;
@@ -36,9 +32,8 @@ public class FeedRefreshTaskGiver implements Managed {
private Meter threadWaited; private Meter threadWaited;
@Inject @Inject
public FeedRefreshTaskGiver(SessionFactory sessionFactory, FeedQueues queues, FeedDAO feedDAO, FeedRefreshWorker worker, public FeedRefreshTaskGiver(FeedQueues queues, FeedDAO feedDAO, FeedRefreshWorker worker, CommaFeedConfiguration config,
CommaFeedConfiguration config, MetricRegistry metrics) { MetricRegistry metrics) {
this.sessionFactory = sessionFactory;
this.queues = queues; this.queues = queues;
this.worker = worker; this.worker = worker;
@@ -68,12 +63,7 @@ public class FeedRefreshTaskGiver implements Managed {
public void run() { public void run() {
while (!executor.isShutdown()) { while (!executor.isShutdown()) {
try { try {
FeedRefreshContext context = new UnitOfWork<FeedRefreshContext>(sessionFactory) { FeedRefreshContext context = queues.take();
@Override
protected FeedRefreshContext runInSession() throws Exception {
return queues.take();
}
}.run();
if (context != null) { if (context != null) {
feedRefreshed.mark(); feedRefreshed.mark();
worker.updateFeed(context); worker.updateFeed(context);

View File

@@ -1,23 +1,21 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import io.dropwizard.lifecycle.Managed; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
@@ -35,9 +33,11 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedUpdateService; import com.commafeed.backend.service.FeedUpdateService;
import com.commafeed.backend.service.PubSubService; import com.commafeed.backend.service.PubSubService;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Striped; import com.google.common.util.concurrent.Striped;
import io.dropwizard.lifecycle.Managed;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@Singleton @Singleton
public class FeedRefreshUpdater implements Managed { public class FeedRefreshUpdater implements Managed {
@@ -112,7 +112,7 @@ public class FeedRefreshUpdater implements Managed {
feed.setMessage("Feed has no entries"); feed.setMessage("Feed has no entries");
} else { } else {
List<String> lastEntries = cache.getLastEntries(feed); List<String> lastEntries = cache.getLastEntries(feed);
List<String> currentEntries = Lists.newArrayList(); List<String> currentEntries = new ArrayList<>();
List<FeedSubscription> subscriptions = null; List<FeedSubscription> subscriptions = null;
for (FeedEntry entry : entries) { for (FeedEntry entry : entries) {
@@ -120,12 +120,7 @@ public class FeedRefreshUpdater implements Managed {
if (!lastEntries.contains(cacheKey)) { if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl()); log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) { if (subscriptions == null) {
subscriptions = new UnitOfWork<List<FeedSubscription>>(sessionFactory) { subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed));
@Override
protected List<FeedSubscription> runInSession() throws Exception {
return feedSubscriptionDAO.findByFeed(feed);
}
}.run();
} }
ok &= addEntry(feed, entry, subscriptions); ok &= addEntry(feed, entry, subscriptions);
entryCacheMiss.mark(); entryCacheMiss.mark();
@@ -143,16 +138,13 @@ public class FeedRefreshUpdater implements Managed {
} }
if (CollectionUtils.isNotEmpty(subscriptions)) { if (CollectionUtils.isNotEmpty(subscriptions)) {
List<User> users = Lists.newArrayList(); List<User> users = subscriptions.stream().map(s -> s.getUser()).collect(Collectors.toList());
for (FeedSubscription sub : subscriptions) {
users.add(sub.getUser());
}
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(users.toArray(new User[0])); cache.invalidateUserRootCategory(users.toArray(new User[0]));
} }
} }
if (config.getApplicationSettings().isPubsubhubbub()) { if (config.getApplicationSettings().getPubsubhubbub()) {
handlePubSub(feed); handlePubSub(feed);
} }
if (!ok) { if (!ok) {
@@ -190,12 +182,7 @@ public class FeedRefreshUpdater implements Managed {
locked1 = lock1.tryLock(1, TimeUnit.MINUTES); locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
locked2 = lock2.tryLock(1, TimeUnit.MINUTES); locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) { if (locked1 && locked2) {
boolean inserted = new UnitOfWork<Boolean>(sessionFactory) { boolean inserted = UnitOfWork.call(sessionFactory, () -> feedUpdateService.addEntry(feed, entry, subscriptions));
@Override
protected Boolean runInSession() throws Exception {
return feedUpdateService.addEntry(feed, entry);
}
}.run();
if (inserted) { if (inserted) {
entryInserted.mark(); entryInserted.mark();
} }

View File

@@ -4,6 +4,8 @@ import io.dropwizard.lifecycle.Managed;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -11,8 +13,8 @@ import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
@@ -20,7 +22,6 @@ import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.feed.FeedRefreshExecutor.Task; import com.commafeed.backend.feed.FeedRefreshExecutor.Task;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.google.common.base.Optional;
/** /**
* Calls {@link FeedFetcher} and handles its outcome * Calls {@link FeedFetcher} and handles its outcome
@@ -84,13 +85,18 @@ public class FeedRefreshWorker implements Managed {
int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes(); int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes();
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval); Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
try { try {
String url = Optional.fromNullable(feed.getUrlAfterRedirect()).or(feed.getUrl()); String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl());
FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(), FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
feed.getLastPublishedDate(), feed.getLastContentHash()); feed.getLastPublishedDate(), feed.getLastContentHash());
// stops here if NotModifiedException or any other exception is thrown // stops here if NotModifiedException or any other exception is thrown
List<FeedEntry> entries = fetchedFeed.getEntries(); List<FeedEntry> entries = fetchedFeed.getEntries();
if (config.getApplicationSettings().isHeavyLoad()) { Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
if (maxFeedCapacity > 0) {
entries = entries.stream().limit(maxFeedCapacity).collect(Collectors.toList());
}
if (config.getApplicationSettings().getHeavyLoad()) {
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed() disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
.getAverageEntryInterval(), disabledUntil); .getAverageEntryInterval(), disabledUntil);
} }
@@ -118,7 +124,7 @@ public class FeedRefreshWorker implements Managed {
} catch (NotModifiedException e) { } catch (NotModifiedException e) {
log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage()); log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
if (config.getApplicationSettings().isHeavyLoad()) { if (config.getApplicationSettings().getHeavyLoad()) {
disabledUntil = FeedUtils.buildDisabledUntil(feed.getLastEntryDate(), feed.getAverageEntryInterval(), disabledUntil); disabledUntil = FeedUtils.buildDisabledUntil(feed.getLastEntryDate(), feed.getAverageEntryInterval(), disabledUntil);
} }
feed.setErrorCount(0); feed.setErrorCount(0);

View File

@@ -3,19 +3,20 @@ package com.commafeed.backend.feed;
import java.io.StringReader; import java.io.StringReader;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics; import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
@@ -25,17 +26,19 @@ import org.jsoup.nodes.Entities.EscapeMode;
import org.jsoup.safety.Cleaner; import org.jsoup.safety.Cleaner;
import org.jsoup.safety.Whitelist; import org.jsoup.safety.Whitelist;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
import org.mozilla.universalchardet.UniversalDetector;
import org.w3c.css.sac.InputSource; import org.w3c.css.sac.InputSource;
import org.w3c.dom.css.CSSStyleDeclaration; import org.w3c.dom.css.CSSStyleDeclaration;
import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.frontend.model.Entry; import com.commafeed.frontend.model.Entry;
import com.google.common.collect.Lists; import com.ibm.icu.text.CharsetDetector;
import com.ibm.icu.text.CharsetMatch;
import com.steadystate.css.parser.CSSOMParser; import com.steadystate.css.parser.CSSOMParser;
import edu.uci.ics.crawler4j.url.URLCanonicalizer; import edu.uci.ics.crawler4j.url.URLCanonicalizer;
import lombok.extern.slf4j.Slf4j;
/** /**
* Utility methods related to feed handling * Utility methods related to feed handling
@@ -83,7 +86,7 @@ public class FeedUtils {
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width"); whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
whitelist.addAttributes("ul", "type"); whitelist.addAttributes("ul", "type");
whitelist.addProtocols("a", "href", "ftp", "http", "https", "mailto"); whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
whitelist.addProtocols("blockquote", "cite", "http", "https"); whitelist.addProtocols("blockquote", "cite", "http", "https");
whitelist.addProtocols("img", "src", "http", "https"); whitelist.addProtocols("img", "src", "http", "https");
whitelist.addProtocols("q", "cite", "http", "https"); whitelist.addProtocols("q", "cite", "http", "https");
@@ -97,14 +100,14 @@ public class FeedUtils {
* feed * feed
* *
*/ */
public static String guessEncoding(byte[] bytes) { public static Charset guessEncoding(byte[] bytes) {
String extracted = extractDeclaredEncoding(bytes); String extracted = extractDeclaredEncoding(bytes);
if (StringUtils.startsWithIgnoreCase(extracted, "iso-8859-")) { if (StringUtils.startsWithIgnoreCase(extracted, "iso-8859-")) {
if (StringUtils.endsWith(extracted, "1") == false) { if (StringUtils.endsWith(extracted, "1") == false) {
return extracted; return Charset.forName(extracted);
} }
} else if (StringUtils.startsWithIgnoreCase(extracted, "windows-")) { } else if (StringUtils.startsWithIgnoreCase(extracted, "windows-")) {
return extracted; return Charset.forName(extracted);
} }
return detectEncoding(bytes); return detectEncoding(bytes);
} }
@@ -112,27 +115,23 @@ public class FeedUtils {
/** /**
* Detect encoding by analyzing characters in the array * Detect encoding by analyzing characters in the array
*/ */
public static String detectEncoding(byte[] bytes) { public static Charset detectEncoding(byte[] bytes) {
String DEFAULT_ENCODING = "UTF-8"; String encoding = "UTF-8";
UniversalDetector detector = new UniversalDetector(null);
detector.handleData(bytes, 0, bytes.length); CharsetDetector detector = new CharsetDetector();
detector.dataEnd(); detector.setText(bytes);
String encoding = detector.getDetectedCharset(); CharsetMatch match = detector.detect();
detector.reset(); if (match != null) {
if (encoding == null) { encoding = match.getName();
encoding = DEFAULT_ENCODING; }
} else if (encoding.equalsIgnoreCase("ISO-8859-1")) { if (encoding.equalsIgnoreCase("ISO-8859-1")) {
encoding = "windows-1252"; encoding = "windows-1252";
} }
return encoding; return Charset.forName(encoding);
} }
public static String replaceHtmlEntitiesWithNumericEntities(String source) { public static String replaceHtmlEntitiesWithNumericEntities(String source) {
String result = source; return StringUtils.replaceEach(source, HtmlEntities.HTML_ENTITIES, HtmlEntities.NUMERIC_ENTITIES);
for (String entity : HtmlEntities.NUMERIC_MAPPING.keySet()) {
result = StringUtils.replace(result, entity, HtmlEntities.NUMERIC_MAPPING.get(entity));
}
return result;
} }
/** /**
@@ -182,7 +181,7 @@ public class FeedUtils {
return null; return null;
} }
String pi = new String(ArrayUtils.subarray(bytes, 0, index + 1)); String pi = new String(ArrayUtils.subarray(bytes, 0, index + 1)).replace('\'', '"');
index = StringUtils.indexOf(pi, "encoding=\""); index = StringUtils.indexOf(pi, "encoding=\"");
if (index == -1) { if (index == -1) {
return null; return null;
@@ -227,7 +226,7 @@ public class FeedUtils {
String rule = ""; String rule = "";
CSSOMParser parser = new CSSOMParser(); CSSOMParser parser = new CSSOMParser();
try { try {
List<String> rules = Lists.newArrayList(); List<String> rules = new ArrayList<>();
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig))); CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) { for (int i = 0; i < decl.getLength(); i++) {
@@ -252,7 +251,7 @@ public class FeedUtils {
String rule = ""; String rule = "";
CSSOMParser parser = new CSSOMParser(); CSSOMParser parser = new CSSOMParser();
try { try {
List<String> rules = Lists.newArrayList(); List<String> rules = new ArrayList<>();
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig))); CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) { for (int i = 0; i < decl.getLength(); i++) {
@@ -386,13 +385,7 @@ public class FeedUtils {
} }
public static List<Long> getSortedTimestamps(List<FeedEntry> entries) { public static List<Long> getSortedTimestamps(List<FeedEntry> entries) {
List<Long> timestamps = Lists.newArrayList(); return entries.stream().map(t -> t.getUpdated().getTime()).sorted(Collections.reverseOrder()).collect(Collectors.toList());
for (FeedEntry entry : entries) {
timestamps.add(entry.getUpdated().getTime());
}
Collections.sort(timestamps);
Collections.reverse(timestamps);
return timestamps;
} }
public static String removeTrailingSlash(String url) { public static String removeTrailingSlash(String url) {
@@ -436,22 +429,15 @@ public class FeedUtils {
} }
public static boolean isRelative(final String url) { public static boolean isRelative(final String url) {
// the regex means "doesn't start with 'scheme://'" // the regex means "start with 'scheme://'"
if ((url != null) && (url.startsWith("/") == false) && (!url.matches("^\\w+\\:\\/\\/.*")) && !(url.startsWith("#"))) { return url.startsWith("/") || url.startsWith("#") || !url.matches("^\\w+\\:\\/\\/.*");
return true;
} else {
return false;
}
} }
public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) { public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) {
return removeTrailingSlash(publicUrl) + "/rest/feed/favicon/" + subscription.getId(); return removeTrailingSlash(publicUrl) + "/rest/feed/favicon/" + subscription.getId();
} }
public static String proxyImages(String content, String publicUrl, boolean proxyImages) { public static String proxyImages(String content, String publicUrl) {
if (!proxyImages) {
return content;
}
if (StringUtils.isBlank(content)) { if (StringUtils.isBlank(content)) {
return content; return content;
} }
@@ -461,7 +447,7 @@ public class FeedUtils {
for (Element element : elements) { for (Element element : elements) {
String href = element.attr("src"); String href = element.attr("src");
if (href != null) { if (href != null) {
String proxy = removeTrailingSlash(publicUrl) + "/rest/server/proxy?u=" + imageProxyEncoder(href); String proxy = proxyImage(href, publicUrl);
element.attr("src", proxy); element.attr("src", proxy);
} }
} }
@@ -469,6 +455,13 @@ public class FeedUtils {
return doc.body().html(); return doc.body().html();
} }
public static String proxyImage(String url, String publicUrl) {
if (StringUtils.isBlank(url)) {
return url;
}
return removeTrailingSlash(publicUrl) + "/rest/server/proxy?u=" + imageProxyEncoder(url);
}
public static String rot13(String msg) { public static String rot13(String msg) {
StringBuilder message = new StringBuilder(); StringBuilder message = new StringBuilder();
@@ -495,19 +488,20 @@ public class FeedUtils {
return rot13(new String(Base64.decodeBase64(code))); return rot13(new String(Base64.decodeBase64(code)));
} }
public static void removeUnwantedFromSearch(List<Entry> entries, String keywords) { public static void removeUnwantedFromSearch(List<Entry> entries, List<FeedEntryKeyword> keywords) {
if (StringUtils.isBlank(keywords)) {
return;
}
Iterator<Entry> it = entries.iterator(); Iterator<Entry> it = entries.iterator();
while (it.hasNext()) { while (it.hasNext()) {
Entry entry = it.next(); Entry entry = it.next();
boolean keep = true; boolean keep = true;
for (String keyword : keywords.split(" ")) { for (FeedEntryKeyword keyword : keywords) {
String title = Jsoup.parse(entry.getTitle()).text(); String title = entry.getTitle() == null ? null : Jsoup.parse(entry.getTitle()).text();
String content = Jsoup.parse(entry.getContent()).text(); String content = entry.getContent() == null ? null : Jsoup.parse(entry.getContent()).text();
if (!StringUtils.containsIgnoreCase(content, keyword) && !StringUtils.containsIgnoreCase(title, keyword)) { boolean condition = !StringUtils.containsIgnoreCase(content, keyword.getKeyword())
&& !StringUtils.containsIgnoreCase(title, keyword.getKeyword());
if (keyword.getMode() == Mode.EXCLUDE) {
condition = !condition;
}
if (condition) {
keep = false; keep = false;
break; break;
} }

View File

@@ -1,58 +1,23 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.Getter;
import lombok.Setter;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.google.common.collect.Lists;
@Getter
@Setter
public class FetchedFeed { public class FetchedFeed {
private Feed feed = new Feed(); private Feed feed = new Feed();
private List<FeedEntry> entries = Lists.newArrayList(); private List<FeedEntry> entries = new ArrayList<>();
private String title; private String title;
private String urlAfterRedirect; private String urlAfterRedirect;
private long fetchDuration; private long fetchDuration;
public Feed getFeed() {
return feed;
}
public void setFeed(Feed feed) {
this.feed = feed;
}
public List<FeedEntry> getEntries() {
return entries;
}
public void setEntries(List<FeedEntry> entries) {
this.entries = entries;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public long getFetchDuration() {
return fetchDuration;
}
public void setFetchDuration(long fetchDuration) {
this.fetchDuration = fetchDuration;
}
public String getUrlAfterRedirect() {
return urlAfterRedirect;
}
public void setUrlAfterRedirect(String urlAfterRedirect) {
this.urlAfterRedirect = urlAfterRedirect;
}
} }

View File

@@ -1,15 +1,14 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.util.Collections; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import com.google.common.collect.Maps;
public class HtmlEntities { public class HtmlEntities {
public static final Map<String, String> NUMERIC_MAPPING = Collections.unmodifiableMap(loadMap()); public static final String[] HTML_ENTITIES;
public static final String[] NUMERIC_ENTITIES;
private static synchronized Map<String, String> loadMap() { static {
Map<String, String> map = Maps.newLinkedHashMap(); Map<String, String> map = new LinkedHashMap<>();
map.put("&Aacute;", "&#193;"); map.put("&Aacute;", "&#193;");
map.put("&aacute;", "&#225;"); map.put("&aacute;", "&#225;");
map.put("&Acirc;", "&#194;"); map.put("&Acirc;", "&#194;");
@@ -261,6 +260,7 @@ public class HtmlEntities {
map.put("&zwj;", "&#8205;"); map.put("&zwj;", "&#8205;");
map.put("&zwnj;", "&#8204;"); map.put("&zwnj;", "&#8204;");
return map; HTML_ENTITIES = map.keySet().toArray(new String[map.size()]);
NUMERIC_ENTITIES = map.values().toArray(new String[map.size()]);
} }
} }

View File

@@ -1,12 +1,9 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.util.Date; import java.util.Date;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
@@ -103,12 +100,6 @@ public class Feed extends AbstractModel {
@Column(length = 40) @Column(length = 40)
private String lastContentHash; private String lastContentHash;
@OneToMany(mappedBy = "feed", cascade = CascadeType.REMOVE)
private Set<FeedEntry> entries;
@OneToMany(mappedBy = "feed")
private Set<FeedSubscription> subscriptions;
/** /**
* detected hub for pubsubhubbub * detected hub for pubsubhubbub
*/ */

View File

@@ -11,6 +11,8 @@ import javax.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.Type;
@Entity @Entity
@Table(name = "FEEDENTRYCONTENTS") @Table(name = "FEEDENTRYCONTENTS")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@@ -26,6 +28,7 @@ public class FeedEntryContent extends AbstractModel {
@Lob @Lob
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
@Type(type = "org.hibernate.type.StringClobType")
private String content; private String content;
@Column(length = 40) @Column(length = 40)
@@ -40,6 +43,9 @@ public class FeedEntryContent extends AbstractModel {
@Column(length = 255) @Column(length = 255)
private String enclosureType; private String enclosureType;
@Column(length = 4096)
private String categories;
@OneToMany(mappedBy = "content") @OneToMany(mappedBy = "content")
private Set<FeedEntry> entries; private Set<FeedEntry> entries;

View File

@@ -1,5 +1,6 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -16,8 +17,6 @@ import javax.persistence.Transient;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import com.google.common.collect.Lists;
@Entity @Entity
@Table(name = "FEEDENTRYSTATUSES") @Table(name = "FEEDENTRYSTATUSES")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@@ -41,7 +40,7 @@ public class FeedEntryStatus extends AbstractModel {
private boolean markable; private boolean markable;
@Transient @Transient
private List<FeedEntryTag> tags = Lists.newArrayList(); private List<FeedEntryTag> tags = new ArrayList<>();
/** /**
* Denormalization starts here * Denormalization starts here

View File

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

View File

@@ -1,13 +1,9 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.util.Date; import java.util.Date;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
@@ -15,11 +11,7 @@ import javax.persistence.TemporalType;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.apache.commons.lang.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.annotations.Cascade;
import com.commafeed.backend.model.UserRole.Role;
import com.google.common.collect.Sets;
@Entity @Entity
@Table(name = "USERS") @Table(name = "USERS")
@@ -58,27 +50,10 @@ public class User extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date recoverPasswordTokenDate; private Date recoverPasswordTokenDate;
@OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, CascadeType.REMOVE })
@Cascade({ org.hibernate.annotations.CascadeType.PERSIST, org.hibernate.annotations.CascadeType.SAVE_UPDATE,
org.hibernate.annotations.CascadeType.REMOVE })
private Set<UserRole> roles = Sets.newHashSet();
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private Set<FeedSubscription> subscriptions;
@Column(name = "last_full_refresh") @Column(name = "last_full_refresh")
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date lastFullRefresh; private Date lastFullRefresh;
public boolean hasRole(Role role) {
for (UserRole userRole : getRoles()) {
if (userRole.getRole() == role) {
return true;
}
}
return false;
}
public boolean shouldRefreshFeedsAt(Date when) { public boolean shouldRefreshFeedsAt(Date when) {
return (lastFullRefresh == null || lastFullRefreshMoreThan30MinutesBefore(when)); return (lastFullRefresh == null || lastFullRefreshMoreThan30MinutesBefore(when));
} }

View File

@@ -13,6 +13,8 @@ import javax.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.Type;
@Entity @Entity
@Table(name = "USERSETTINGS") @Table(name = "USERSETTINGS")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@@ -59,6 +61,7 @@ public class UserSettings extends AbstractModel {
@Lob @Lob
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
@Type(type = "org.hibernate.type.StringClobType")
private String customCss; private String customCss;
@Column(name = "scroll_speed") @Column(name = "scroll_speed")

View File

@@ -1,7 +1,9 @@
package com.commafeed.backend.opml; package com.commafeed.backend.opml;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -13,6 +15,7 @@ import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.google.common.base.MoreObjects;
import com.rometools.opml.feed.opml.Attribute; import com.rometools.opml.feed.opml.Attribute;
import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Opml;
import com.rometools.opml.feed.opml.Outline; import com.rometools.opml.feed.opml.Outline;
@@ -31,39 +34,40 @@ public class OPMLExporter {
opml.setCreated(new Date()); opml.setCreated(new Date());
List<FeedCategory> categories = feedCategoryDAO.findAll(user); List<FeedCategory> categories = feedCategoryDAO.findAll(user);
Collections.sort(categories,
(e1, e2) -> MoreObjects.firstNonNull(e1.getPosition(), 0) - MoreObjects.firstNonNull(e2.getPosition(), 0));
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user); List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
Collections.sort(subscriptions,
(e1, e2) -> MoreObjects.firstNonNull(e1.getPosition(), 0) - MoreObjects.firstNonNull(e2.getPosition(), 0));
// export root categories // export root categories
for (FeedCategory cat : categories) { for (FeedCategory cat : categories.stream().filter(c -> c.getParent() == null).collect(Collectors.toList())) {
if (cat.getParent() == null) { opml.getOutlines().add(buildCategoryOutline(cat, categories, subscriptions));
opml.getOutlines().add(buildCategoryOutline(cat, subscriptions));
}
} }
// export root subscriptions // export root subscriptions
for (FeedSubscription sub : subscriptions) { for (FeedSubscription sub : subscriptions.stream().filter(s -> s.getCategory() == null).collect(Collectors.toList())) {
if (sub.getCategory() == null) { opml.getOutlines().add(buildSubscriptionOutline(sub));
opml.getOutlines().add(buildSubscriptionOutline(sub));
}
} }
return opml; return opml;
} }
private Outline buildCategoryOutline(FeedCategory cat, List<FeedSubscription> subscriptions) { private Outline buildCategoryOutline(FeedCategory cat, List<FeedCategory> categories, List<FeedSubscription> subscriptions) {
Outline outline = new Outline(); Outline outline = new Outline();
outline.setText(cat.getName()); outline.setText(cat.getName());
outline.setTitle(cat.getName()); outline.setTitle(cat.getName());
for (FeedCategory child : cat.getChildren()) { for (FeedCategory child : categories.stream().filter(c -> c.getParent() != null && c.getParent().getId().equals(cat.getId()))
outline.getChildren().add(buildCategoryOutline(child, subscriptions)); .collect(Collectors.toList())) {
outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions));
} }
for (FeedSubscription sub : subscriptions) { for (FeedSubscription sub : subscriptions.stream()
if (sub.getCategory() != null && sub.getCategory().getId().equals(cat.getId())) { .filter(s -> s.getCategory() != null && s.getCategory().getId().equals(cat.getId())).collect(Collectors.toList())) {
outline.getChildren().add(buildSubscriptionOutline(sub)); outline.getChildren().add(buildSubscriptionOutline(sub));
}
} }
return outline; return outline;
} }

View File

@@ -10,7 +10,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
@@ -38,8 +38,8 @@ public class OPMLImporter {
try { try {
Opml feed = (Opml) input.build(new StringReader(xml)); Opml feed = (Opml) input.build(new StringReader(xml));
List<Outline> outlines = feed.getOutlines(); List<Outline> outlines = feed.getOutlines();
for (Outline outline : outlines) { for (int i = 0; i < outlines.size(); i++) {
handleOutline(user, outline, null); handleOutline(user, outlines.get(i), null, i);
} }
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
@@ -47,7 +47,7 @@ public class OPMLImporter {
} }
private void handleOutline(User user, Outline outline, FeedCategory parent) { private void handleOutline(User user, Outline outline, FeedCategory parent, int position) {
List<Outline> children = outline.getChildren(); List<Outline> children = outline.getChildren();
if (CollectionUtils.isNotEmpty(children)) { if (CollectionUtils.isNotEmpty(children)) {
String name = FeedUtils.truncate(outline.getText(), 128); String name = FeedUtils.truncate(outline.getText(), 128);
@@ -64,11 +64,12 @@ public class OPMLImporter {
category.setName(name); category.setName(name);
category.setParent(parent); category.setParent(parent);
category.setUser(user); category.setUser(user);
category.setPosition(position);
feedCategoryDAO.saveOrUpdate(category); feedCategoryDAO.saveOrUpdate(category);
} }
for (Outline child : children) { for (int i = 0; i < children.size(); i++) {
handleOutline(user, child, category); handleOutline(user, children.get(i), category, i);
} }
} else { } else {
String name = FeedUtils.truncate(outline.getText(), 128); String name = FeedUtils.truncate(outline.getText(), 128);
@@ -80,7 +81,7 @@ public class OPMLImporter {
} }
// make sure we continue with the import process even if a feed failed // make sure we continue with the import process even if a feed failed
try { try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent); feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position);
} catch (FeedSubscriptionException e) { } catch (FeedSubscriptionException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {

View File

@@ -1,9 +1,13 @@
package com.commafeed.backend.rome; package com.commafeed.backend.rome;
import java.util.Locale;
import org.jdom2.Document; import org.jdom2.Document;
import org.jdom2.Element; import org.jdom2.Element;
import com.rometools.opml.io.impl.OPML10Parser; import com.rometools.opml.io.impl.OPML10Parser;
import com.rometools.rome.feed.WireFeed;
import com.rometools.rome.io.FeedException;
/** /**
* Support for OPML 1.1 parsing * Support for OPML 1.1 parsing
@@ -19,12 +23,17 @@ public class OPML11Parser extends OPML10Parser {
public boolean isMyType(Document document) { public boolean isMyType(Document document) {
Element e = document.getRootElement(); Element e = document.getRootElement();
if (e.getName().equals("opml") && (e.getChild("head") == null || e.getChild("head").getChild("docs") == null) if (e.getName().equals("opml")) {
&& (e.getAttributeValue("version") == null || e.getAttributeValue("version").equals("1.1"))) {
return true; return true;
} }
return false; return false;
}; }
@Override
public WireFeed parse(Document document, boolean validate, Locale locale) throws IllegalArgumentException, FeedException {
document.getRootElement().getChildren().add(new Element("head"));
return super.parse(document, validate, locale);
}
} }

View File

@@ -1,32 +1,30 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryDAO.FeedCapacity;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntryStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/** /**
* Contains utility methods for cleaning the database * Contains utility methods for cleaning the database
* *
*/ */
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }) )
@Singleton @Singleton
public class DatabaseCleaningService { public class DatabaseCleaningService {
@@ -42,14 +40,18 @@ public class DatabaseCleaningService {
log.info("cleaning feeds without subscriptions"); log.info("cleaning feeds without subscriptions");
long total = 0; long total = 0;
int deleted = 0; int deleted = 0;
long entriesTotal = 0;
do { do {
deleted = new UnitOfWork<Integer>(sessionFactory) { List<Feed> feeds = UnitOfWork.call(sessionFactory, () -> feedDAO.findWithoutSubscriptions(1));
@Override for (Feed feed : feeds) {
protected Integer runInSession() throws Exception { int entriesDeleted = 0;
List<Feed> feeds = feedDAO.findWithoutSubscriptions(1); do {
return feedDAO.delete(feeds); entriesDeleted = UnitOfWork.call(sessionFactory, () -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE));
}; entriesTotal += entriesDeleted;
}.run(); log.info("removed {} entries for feeds without subscriptions", entriesTotal);
} while (entriesDeleted > 0);
}
deleted = UnitOfWork.call(sessionFactory, () -> feedDAO.delete(feeds));
total += deleted; total += deleted;
log.info("removed {} feeds without subscriptions", total); log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
@@ -62,12 +64,7 @@ public class DatabaseCleaningService {
long total = 0; long total = 0;
int deleted = 0; int deleted = 0;
do { do {
deleted = new UnitOfWork<Integer>(sessionFactory) { deleted = UnitOfWork.call(sessionFactory, () -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE));
@Override
protected Integer runInSession() throws Exception {
return feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE);
}
}.run();
total += deleted; total += deleted;
log.info("removed {} contents without entries", total); log.info("removed {} contents without entries", total);
} while (deleted != 0); } while (deleted != 0);
@@ -75,23 +72,28 @@ public class DatabaseCleaningService {
return total; return total;
} }
public long cleanEntriesOlderThan(long value, TimeUnit unit) { public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) {
final Calendar cal = Calendar.getInstance();
cal.add(Calendar.MINUTE, -1 * (int) unit.toMinutes(value));
long total = 0; long total = 0;
int deleted = 0; while (true) {
do { List<FeedCapacity> feeds = UnitOfWork.call(sessionFactory,
deleted = new UnitOfWork<Integer>(sessionFactory) { () -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE));
@Override if (feeds.isEmpty()) {
protected Integer runInSession() throws Exception { break;
return feedEntryDAO.delete(cal.getTime(), BATCH_SIZE); }
}
}.run(); for (final FeedCapacity feed : feeds) {
total += deleted; long remaining = feed.getCapacity() - maxFeedCapacity;
log.info("removed {} entries", total); do {
} while (deleted != 0); final long rem = remaining;
log.info("cleanup done: {} entries deleted", total); int deleted = UnitOfWork.call(sessionFactory,
() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(BATCH_SIZE, rem)));
total += deleted;
remaining -= deleted;
log.info("removed {} entries for feeds exceeding capacity", total);
} while (remaining > 0);
}
}
log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total);
return total; return total;
} }
@@ -100,15 +102,10 @@ public class DatabaseCleaningService {
long total = 0; long total = 0;
int deleted = 0; int deleted = 0;
do { do {
deleted = new UnitOfWork<Integer>(sessionFactory) { deleted = UnitOfWork.call(sessionFactory,
@Override () -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE)));
protected Integer runInSession() throws Exception {
List<FeedEntryStatus> list = feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE);
return feedEntryStatusDAO.delete(list);
}
}.run();
total += deleted; total += deleted;
log.info("cleaned {} old read statuses", total); log.info("removed {} old read statuses", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} old read statuses deleted", total); log.info("cleanup done: {} old read statuses deleted", total);
return total; return total;

View File

@@ -6,7 +6,7 @@ import javax.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;

View File

@@ -0,0 +1,119 @@
package com.commafeed.backend.service;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.RequiredArgsConstructor;
import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.JexlEngine;
import org.apache.commons.jexl2.JexlException;
import org.apache.commons.jexl2.JexlInfo;
import org.apache.commons.jexl2.MapContext;
import org.apache.commons.jexl2.Script;
import org.apache.commons.jexl2.introspection.JexlMethod;
import org.apache.commons.jexl2.introspection.JexlPropertyGet;
import org.apache.commons.jexl2.introspection.Uberspect;
import org.apache.commons.jexl2.introspection.UberspectImpl;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.LogFactory;
import org.jsoup.Jsoup;
import com.commafeed.backend.model.FeedEntry;
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class FeedEntryFilteringService {
private static final JexlEngine ENGINE = initEngine();
private static JexlEngine initEngine() {
// classloader that prevents object creation
ClassLoader cl = new ClassLoader() {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return null;
}
};
// uberspect that prevents access to .class and .getClass()
Uberspect uberspect = new UberspectImpl(LogFactory.getLog(JexlEngine.class)) {
@Override
public JexlPropertyGet getPropertyGet(Object obj, Object identifier, JexlInfo info) {
if ("class".equals(identifier)) {
return null;
}
return super.getPropertyGet(obj, identifier, info);
}
@Override
public JexlMethod getMethod(Object obj, String method, Object[] args, JexlInfo info) {
if ("getClass".equals(method)) {
return null;
}
return super.getMethod(obj, method, args, info);
}
};
JexlEngine engine = new JexlEngine(uberspect, null, null, null);
engine.setStrict(true);
engine.setClassLoader(cl);
return engine;
}
private ExecutorService executor = Executors.newCachedThreadPool();
public boolean filterMatchesEntry(String filter, FeedEntry entry) throws FeedEntryFilterException {
if (StringUtils.isBlank(filter)) {
return true;
}
Script script = null;
try {
script = ENGINE.createScript(filter);
} catch (JexlException e) {
throw new FeedEntryFilterException("Exception while parsing expression " + filter, e);
}
JexlContext context = new MapContext();
context.set("title", entry.getContent().getTitle() == null ? "" : Jsoup.parse(entry.getContent().getTitle()).text().toLowerCase());
context.set("author", entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor().toLowerCase());
context.set("content", entry.getContent().getContent() == null ? "" : Jsoup.parse(entry.getContent().getContent()).text()
.toLowerCase());
context.set("url", entry.getUrl() == null ? "" : entry.getUrl().toLowerCase());
context.set("categories", entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories().toLowerCase());
Callable<Object> callable = script.callable(context);
Future<Object> future = executor.submit(callable);
Object result = null;
try {
result = future.get(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new FeedEntryFilterException("interrupted while evaluating expression " + filter, e);
} catch (ExecutionException e) {
throw new FeedEntryFilterException("Exception while evaluating expression " + filter, e);
} catch (TimeoutException e) {
throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e);
}
try {
return (boolean) result;
} catch (ClassCastException e) {
throw new FeedEntryFilterException(e.getMessage(), e);
}
}
@SuppressWarnings("serial")
public static class FeedEntryFilterException extends Exception {
public FeedEntryFilterException(String message, Throwable t) {
super(message, t);
}
}
}

View File

@@ -1,5 +1,6 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -12,11 +13,11 @@ import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.google.common.collect.Lists;
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton @Singleton
@@ -65,9 +66,9 @@ public class FeedEntryService {
feedEntryStatusDAO.saveOrUpdate(status); feedEntryStatusDAO.saveOrUpdate(status);
} }
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) { public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan, List<FeedEntryKeyword> keywords) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, -1, -1, null, false, List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null,
false, null); false, false, null);
markList(statuses, olderThan); markList(statuses, olderThan);
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(user); cache.invalidateUserRootCategory(user);
@@ -79,7 +80,7 @@ public class FeedEntryService {
} }
private void markList(List<FeedEntryStatus> statuses, Date olderThan) { private void markList(List<FeedEntryStatus> statuses, Date olderThan) {
List<FeedEntryStatus> list = Lists.newArrayList(); List<FeedEntryStatus> list = new ArrayList<>();
for (FeedEntryStatus status : statuses) { for (FeedEntryStatus status : statuses) {
if (!status.isRead()) { if (!status.isRead()) {
Date inserted = status.getEntry().getInserted(); Date inserted = status.getEntry().getInserted();

View File

@@ -1,7 +1,8 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -13,9 +14,6 @@ import com.commafeed.backend.dao.FeedEntryTagDAO;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton @Singleton
@@ -30,29 +28,12 @@ public class FeedEntryTagService {
return; return;
} }
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, entry); List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
Map<String, FeedEntryTag> tagMap = Maps.uniqueIndex(tags, new Function<FeedEntryTag, String>() { Set<String> existingTagNames = existingTags.stream().map(t -> t.getName()).collect(Collectors.toSet());
@Override
public String apply(FeedEntryTag input) {
return input.getName();
}
});
List<FeedEntryTag> addList = Lists.newArrayList(); List<FeedEntryTag> addList = tagNames.stream().filter(name -> !existingTagNames.contains(name))
List<FeedEntryTag> removeList = Lists.newArrayList(); .map(name -> new FeedEntryTag(user, entry, name)).collect(Collectors.toList());
List<FeedEntryTag> removeList = existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).collect(Collectors.toList());
for (String tagName : tagNames) {
FeedEntryTag tag = tagMap.get(tagName);
if (tag == null) {
addList.add(new FeedEntryTag(user, entry, tagName));
}
}
for (FeedEntryTag tag : tags) {
if (!tagNames.contains(tag.getName())) {
removeList.add(tag);
}
}
feedEntryTagDAO.saveOrUpdate(addList); feedEntryTagDAO.saveOrUpdate(addList);
feedEntryTagDAO.delete(removeList); feedEntryTagDAO.delete(removeList);

View File

@@ -12,6 +12,7 @@ import org.apache.commons.io.IOUtils;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.favicon.AbstractFaviconFetcher; import com.commafeed.backend.favicon.AbstractFaviconFetcher;
import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
@@ -21,7 +22,7 @@ public class FeedService {
private final FeedDAO feedDAO; private final FeedDAO feedDAO;
private final Set<AbstractFaviconFetcher> faviconFetchers; private final Set<AbstractFaviconFetcher> faviconFetchers;
private byte[] defaultFavicon; private Favicon defaultFavicon;
@Inject @Inject
public FeedService(FeedDAO feedDAO, Set<AbstractFaviconFetcher> faviconFetchers) { public FeedService(FeedDAO feedDAO, Set<AbstractFaviconFetcher> faviconFetchers) {
@@ -29,7 +30,7 @@ public class FeedService {
this.faviconFetchers = faviconFetchers; this.faviconFetchers = faviconFetchers;
try { try {
defaultFavicon = IOUtils.toByteArray(getClass().getResource("/images/default_favicon.gif")); defaultFavicon = new Favicon(IOUtils.toByteArray(getClass().getResource("/images/default_favicon.gif")), "image/gif");
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("could not load default favicon", e); throw new RuntimeException("could not load default favicon", e);
} }
@@ -49,9 +50,9 @@ public class FeedService {
return feed; return feed;
} }
public byte[] fetchFavicon(Feed feed) { public Favicon fetchFavicon(Feed feed) {
byte[] icon = null; Favicon icon = null;
for (AbstractFaviconFetcher faviconFetcher : faviconFetchers) { for (AbstractFaviconFetcher faviconFetcher : faviconFetchers) {
icon = faviconFetcher.fetch(feed); icon = faviconFetcher.fetch(feed);
if (icon != null) { if (icon != null) {

View File

@@ -2,6 +2,7 @@ package com.commafeed.backend.service;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -9,7 +10,7 @@ import javax.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
@@ -23,7 +24,6 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models; import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.UnreadCount; import com.commafeed.frontend.model.UnreadCount;
import com.google.common.collect.Maps;
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@@ -44,7 +44,15 @@ public class FeedSubscriptionService {
private final CacheService cache; private final CacheService cache;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
public Feed subscribe(User user, String url, String title, FeedCategory category) { public Feed subscribe(User user, String url, String title) {
return subscribe(user, url, title, null, 0);
}
public Feed subscribe(User user, String url, String title, FeedCategory parent) {
return subscribe(user, url, title, parent, 0);
}
public Feed subscribe(User user, String url, String title, FeedCategory category, int position) {
final String pubUrl = config.getApplicationSettings().getPublicUrl(); final String pubUrl = config.getApplicationSettings().getPublicUrl();
if (StringUtils.isBlank(pubUrl)) { if (StringUtils.isBlank(pubUrl)) {
@@ -63,7 +71,7 @@ public class FeedSubscriptionService {
sub.setUser(user); sub.setUser(user);
} }
sub.setCategory(category); sub.setCategory(category);
sub.setPosition(0); sub.setPosition(position);
sub.setTitle(FeedUtils.truncate(title, 128)); sub.setTitle(FeedUtils.truncate(title, 128));
feedSubscriptionDAO.saveOrUpdate(sub); feedSubscriptionDAO.saveOrUpdate(sub);
@@ -92,12 +100,7 @@ public class FeedSubscriptionService {
} }
public Map<Long, UnreadCount> getUnreadCount(User user) { public Map<Long, UnreadCount> getUnreadCount(User user) {
Map<Long, UnreadCount> map = Maps.newHashMap(); return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(s -> s.getId(), s -> getUnreadCount(user, s)));
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) {
map.put(sub.getId(), getUnreadCount(user, sub));
}
return map;
} }
private UnreadCount getUnreadCount(User user, FeedSubscription sub) { private UnreadCount getUnreadCount(User user, FeedSubscription sub) {

View File

@@ -1,30 +1,39 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.Date; import java.util.Date;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton @Singleton
public class FeedUpdateService { public class FeedUpdateService {
private final FeedEntryDAO feedEntryDAO; private final FeedEntryDAO feedEntryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedEntryContentService feedEntryContentService; private final FeedEntryContentService feedEntryContentService;
private final FeedEntryFilteringService feedEntryFilteringService;
/** /**
* this is NOT thread-safe * this is NOT thread-safe
*/ */
public boolean addEntry(Feed feed, FeedEntry entry) { public boolean addEntry(Feed feed, FeedEntry entry, List<FeedSubscription> subscriptions) {
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed); Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
if (existing != null) { if (existing != null) {
@@ -36,8 +45,23 @@ public class FeedUpdateService {
entry.setContent(content); entry.setContent(content);
entry.setInserted(new Date()); entry.setInserted(new Date());
entry.setFeed(feed); entry.setFeed(feed);
feedEntryDAO.saveOrUpdate(entry); feedEntryDAO.saveOrUpdate(entry);
// if filter does not match the entry, mark it as read
for (FeedSubscription sub : subscriptions) {
boolean matches = true;
try {
matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry);
} catch (FeedEntryFilterException e) {
log.error("could not evaluate filter {}", sub.getFilter(), e);
}
if (!matches) {
FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry);
status.setRead(true);
feedEntryStatusDAO.saveOrUpdate(status);
}
}
return true; return true;
} }
} }

View File

@@ -1,5 +1,6 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.Optional;
import java.util.Properties; import java.util.Properties;
import javax.inject.Inject; import javax.inject.Inject;
@@ -34,6 +35,7 @@ public class MailService {
final String username = settings.getSmtpUserName(); final String username = settings.getSmtpUserName();
final String password = settings.getSmtpPassword(); final String password = settings.getSmtpPassword();
final String fromAddress = Optional.ofNullable(settings.getSmtpFromAddress()).orElse(settings.getSmtpUserName());
String dest = user.getEmail(); String dest = user.getEmail();
@@ -51,7 +53,7 @@ public class MailService {
}); });
Message message = new MimeMessage(session); Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(username, "CommaFeed")); message.setFrom(new InternetAddress(fromAddress, "CommaFeed"));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest)); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest));
message.setSubject("CommaFeed - " + subject); message.setSubject("CommaFeed - " + subject);
message.setContent(content, "text/html; charset=utf-8"); message.setContent(content, "text/html; charset=utf-8");

View File

@@ -15,7 +15,7 @@ import javax.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
// taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html // taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
@SuppressWarnings("serial") @SuppressWarnings("serial")

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