Compare commits

...

204 Commits
1.0.0 ... 1.3.0

Author SHA1 Message Date
Athou
1f4d62ab47 1.3.0 release 2013-09-25 13:47:13 +02:00
Athou
a7b826bd4f prevent unintentional entry list reset 2013-09-20 08:11:28 +02:00
Athou
407481faa6 delete operation does not support limit. limit on select and delete afterwards 2013-09-18 09:24:31 +02:00
Athou
305b68546c create a new transaction for each delete chunk 2013-09-18 09:24:05 +02:00
Athou
136c41c6aa delete old read statuses by chunks in order to avoid large transactions 2013-09-17 13:01:27 +02:00
Athou
587b25b18b Merge pull request #510 from Cymrodor/patch-1
Update cy.properties
2013-09-16 19:40:29 -07:00
Cymrodor
beaa40ad65 Update cy.properties
Ychwanegu, cwtogi, cywiro a thacluso.
2013-09-16 23:21:39 +01:00
Athou
1389a5a238 readme update 2013-09-16 20:32:37 +02:00
Athou
2f34ff8a9f prevent NPE if session does not exist 2013-09-16 07:01:47 +02:00
Athou
d3626b0e7c reduce blockquotes font size 2013-09-10 19:07:17 +02:00
Athou
bb4529b6f1 improve scrolling performance by registering events only once instead of once per entry 2013-09-10 16:12:39 +02:00
Athou
dd94125d52 remove unneeded synchronization locks on settings 2013-09-08 19:08:26 +02:00
Athou
a7149e3740 don't start a new reporter every time the registry is injected 2013-09-05 16:30:14 +02:00
Athou
b64d041385 Merge pull request #505 from ekovi/patch-3
Update _svetla.scss
2013-09-01 01:36:17 -07:00
Athou
cc04bdfbc5 Merge pull request #504 from LpSamuelm/patch-17
Translated new labels to Swedish
2013-09-01 01:34:53 -07:00
Athou
d8c772ed5e compact forms 2013-09-01 10:33:36 +02:00
Athou
dfcc4eeebd return an error message when feed/category is not found instead of returning an empty feed/category 2013-09-01 10:33:35 +02:00
ekovi
e491841d4a Update _svetla.scss
some changes and fixes
2013-08-30 20:37:36 +02:00
LpSamuelm
ccb72837b3 Translated new labels to Swedish 2013-08-30 09:24:47 +02:00
Athou
6560fc9d05 display gauges as well 2013-08-23 14:12:13 +02:00
Athou
14d5879735 fix issue where only the first directive was shown 2013-08-23 13:40:23 +02:00
Athou
7fa8bef3de initial metrics page setup 2013-08-23 12:58:24 +02:00
Athou
966caae727 store and use urlAfterRedirect if different than the actual url 2013-08-22 15:55:05 +02:00
Athou
a14484ee03 retrieve the final url after potential http 30x redirect 2013-08-22 15:36:04 +02:00
Athou
fb9b42ab12 added log4j entry for metrics 2013-08-22 15:27:24 +02:00
Athou
6974abdb95 don't compare strings with == 2013-08-22 12:04:00 +02:00
Athou
65efdeb1df wicket update 2013-08-22 09:13:56 +02:00
Athou
54a39ea0a9 fix scrolling issues on some mobile devices (#482) 2013-08-22 09:13:56 +02:00
Athou
641350cbde detect categories in opml files by checking if they have children 2013-08-22 06:20:44 +02:00
Athou
06ece8f5ee Merge pull request #497 from ekovi/patch-1
translation of additional entries
2013-08-21 20:44:32 -07:00
ekovi
ca87f1c47a translation of additional entries 2013-08-21 21:17:06 +02:00
Athou
c38ddb5d00 add a note about hsqldb data location (fix #496) 2013-08-21 13:04:12 +02:00
Athou
1acd7c4a01 set serialid 2013-08-20 09:47:08 +02:00
Athou
d92c2ebdf7 measure refill rate 2013-08-18 17:19:01 +02:00
Athou
8f19e9408e report through jmx 2013-08-18 17:13:45 +02:00
Athou
3ecb47da5a use timers instead of meters 2013-08-18 16:42:01 +02:00
Athou
ae03b42c6d pretty print response if method is annotated with @PrettyPrint or 'pretty' req param is set to true 2013-08-18 16:29:41 +02:00
Athou
ee4eb9bb07 use codahale metrics library instead of our own 2013-08-18 16:29:07 +02:00
Athou
a0be2e0879 added gmail social sharing button 2013-08-17 21:55:29 +02:00
Athou
a3414d7156 let's use snapshots 2013-08-17 13:47:14 +02:00
Athou
81a4b36c08 we don't need to set the default port manually as wicket does that for us 2013-08-16 21:33:59 +02:00
Athou
bf154cf83d set port too 2013-08-16 21:23:11 +02:00
Athou
0d1234ca4b use protocol from the publicUrl when rendering urls 2013-08-16 21:10:25 +02:00
Athou
a1c42f2709 preserve https across redirections if it was set 2013-08-16 19:27:13 +02:00
Athou
7608921684 readme update 2013-08-16 18:03:07 +02:00
Athou
24f2b17416 reduce session size and prevent potential session loss (and exceptions) when server restarts 2013-08-16 17:40:45 +02:00
Athou
33eb469520 silence swagger, generated warnings are distracting 2013-08-16 17:06:52 +02:00
Athou
90eef904f9 import cleanup 2013-08-16 16:40:04 +02:00
Athou
d1f72ee53a if we fail to parse the rule, return an empty rule instead 2013-08-16 16:31:43 +02:00
Athou
e0e212dfc4 trust encoding declared as windows codepages (fix #491) 2013-08-16 13:41:49 +02:00
Athou
ef0a03cb3b added link to news+ extension 2013-08-16 13:40:14 +02:00
Athou
221eeddab8 don't add already existing entries (#477) 2013-08-16 13:32:58 +02:00
Athou
1076527b62 make chrome think every bookmark click is a different url in order to create history entries (fix #488) 2013-08-14 10:43:10 +02:00
Athou
1e13c11061 prevent NPE 2013-08-13 15:35:02 +02:00
Athou
440922380d use jackson for json only 2013-08-13 12:49:57 +02:00
Athou
969a199a8e new label 2013-08-13 12:21:21 +02:00
Athou
21b0176a49 display message on details page 2013-08-13 12:15:30 +02:00
Athou
06a996cd81 added ability to exclude subscriptions from getEntries 2013-08-13 10:10:25 +02:00
Athou
e1fc33626e added ability to exclude subscriptions from markAll 2013-08-13 09:40:13 +02:00
Athou
b331626e8f don't send the OK int constant as entity 2013-08-13 09:25:55 +02:00
Athou
95d4f725f9 feed id not required anymore 2013-08-13 09:14:41 +02:00
Athou
45b54a75db annotations not needed anymore 2013-08-13 09:06:08 +02:00
Athou
e000bb05c4 don't handle byte arrays with jackson (feed icons, ...) 2013-08-12 20:53:19 +02:00
Athou
d66ca05dca don't wrap String responses with double quotes 2013-08-12 20:50:03 +02:00
Athou
321260b0a5 set disabledUntil to now instead of null when the error count threshold has not been reached yet 2013-08-12 16:20:55 +02:00
Athou
5ef8fd18ca annotations not needed anymore 2013-08-12 13:42:18 +02:00
Athou
5b5d5cca1c jackson update 2013-08-12 12:45:25 +02:00
Athou
51ac16a9e1 small refactoring 2013-08-12 10:03:34 +02:00
Athou
cf185c3877 only fetch status when we know it's there 2013-08-12 10:01:24 +02:00
Athou
71368fba62 fix reply message 2013-08-11 17:22:17 +02:00
Athou
6bae50a56a wicket update 2013-08-11 17:12:00 +02:00
Athou
27681603cd generate accessors only 2013-08-11 14:01:16 +02:00
Athou
e1be05711b use lombok data instead of getters and setters 2013-08-11 12:09:05 +02:00
Athou
7b1bb9072e modelgen upgrade 2013-08-11 12:01:08 +02:00
Athou
a58b0a0806 use lombok data instead of getters and setters 2013-08-11 11:59:24 +02:00
Athou
e26950671c clarify documentation: keywords are not required 2013-08-11 11:50:30 +02:00
Athou
0d730128f7 use lombok slf4j annotation 2013-08-11 11:45:32 +02:00
Athou
174664619b use lombok 2013-08-11 11:11:09 +02:00
Athou
f8738f10af added optional 'onlyIds' api parameter 2013-08-09 12:53:21 +02:00
Athou
677fb87f71 increase limit 2013-08-09 12:18:35 +02:00
Athou
c980e5dd67 return newly created category id 2013-08-09 09:36:06 +02:00
Athou
474995c8dd send date of the newest subscriptions item 2013-08-08 17:00:52 +02:00
Athou
74ee810757 Merge pull request #486 from swoga/patch-1
Update de.properties
2013-08-08 07:27:36 -07:00
swoga
c5b56b47ae Update de.properties 2013-08-08 16:26:47 +02:00
Athou
dccaca4972 file writing is now java vendor independent (#485) 2013-08-08 15:31:31 +02:00
Athou
92f53a0034 double click in the tree opens the edit page 2013-08-07 16:41:37 +02:00
Athou
15eb00b1ba new labels 2013-08-07 16:32:13 +02:00
Athou
041b5ad2c0 tweaking display for search results 2013-08-07 16:31:44 +02:00
Athou
4520ef4078 search is now context aware (will only search in selected category or feed) 2013-08-07 15:26:43 +02:00
Athou
701a1903ba move feedcount in its own file 2013-08-07 10:08:03 +02:00
Athou
ff7458dfc1 make sure statuses are unique 2013-08-06 16:20:50 +02:00
Athou
a72e08c0c6 try to parse given url before using embedded links 2013-08-06 13:49:03 +02:00
Athou
2bff335698 show exception in debug log 2013-08-06 13:07:50 +02:00
Athou
b8a256ac7d allow some css rules for images (#478) 2013-08-06 13:00:28 +02:00
Athou
2168c0039a Merge pull request #479 from Athou/disk-io
stay in indexes as long as possible
2013-08-06 03:48:58 -07:00
Athou
d225884ec3 stay in indexes as long as possible 2013-08-06 12:39:12 +02:00
Athou
9934c4a169 reset current entry tracking when reloading (#476) 2013-08-06 11:46:39 +02:00
Athou
9e75f23d8f display more of the feed name 2013-08-06 11:14:12 +02:00
Athou
f36471bbf3 in unread mode, use the actual number of unread items as offset instead of the size of the list of items (#462) 2013-08-03 16:32:18 +02:00
Athou
4664bef4d8 not needed anymore 2013-08-01 21:16:01 +02:00
Athou
71403d4174 if under heavy load, don't refresh feeds for users who logged in more than a month ago for the last time 2013-08-01 21:11:45 +02:00
Athou
cb1b99815c copy generated api docs even when prod flag is false 2013-08-01 18:26:10 +02:00
Athou
5668efc8a8 don't expose documentation class in the method signature as it's not available at runtime 2013-08-01 18:16:44 +02:00
Athou
d3223ec8b4 exclude api generator from war 2013-08-01 17:20:06 +02:00
Athou
bfbe39993f use an annotation processor instead of a groovy script because of classloading issues 2013-08-01 17:11:57 +02:00
Athou
e90747fd08 display the base url statically 2013-08-01 14:38:24 +02:00
Athou
8926f9784d documentation is now generated during build time and swagger is not needed for runtime anymore 2013-08-01 13:35:04 +02:00
Athou
0ff1d58dfb code formatting 2013-08-01 11:18:06 +02:00
Athou
8df587aaad don't display entries twice when refreshing during loading (fix #473) 2013-08-01 11:18:06 +02:00
Athou
10fdffc378 Merge pull request #475 from res87th/patch-1
Light changes in ru.properties
2013-07-31 23:11:35 -07:00
res87th
a7d7335970 Update ru.properties 2013-07-31 22:55:59 -07:00
Athou
bb5244c118 return headers on all requests 2013-08-01 05:33:23 +02:00
Athou
f20a5e92e2 extracted needed classes and remove crawler4j dependency and java7 requirement 2013-08-01 05:24:05 +02:00
Athou
5ce0428b15 Merge pull request #472 from gabrielrcp/translation
New labels for ptuguese translation
2013-07-31 09:56:21 -07:00
Athou
a43e738365 set default value 2013-07-31 18:47:20 +02:00
Gabriel Peixoto
f4a4eab32d New labels for portuguese translation. 2013-07-31 13:44:00 -03:00
Athou
7ffc58892a marker already present in the openshift repository, delete to avoid merge conflicts 2013-07-31 18:26:35 +02:00
Athou
8f85637bb8 invalidate cache once per feed instead of once per entry 2013-07-31 16:03:52 +02:00
Athou
a37925396a mobile view tweaks 2013-07-31 15:51:00 +02:00
Athou
b66749264a added http headers for cross origin resource sharing 2013-07-31 15:31:06 +02:00
Athou
6dcf2aabd1 pass a context object around instead of creating transient fields in model objects 2013-07-31 13:06:57 +02:00
Athou
71bb33d710 small cleanup 2013-07-31 12:07:29 +02:00
Athou
365c235e1f fix jslint warning 2013-07-31 11:35:53 +02:00
Athou
da65e85081 apply new js formatter 2013-07-31 11:16:50 +02:00
Athou
7497b88c26 added javascript formatter 2013-07-31 11:16:40 +02:00
Athou
5d5c955451 list filtering wasn't working correctly 2013-07-31 11:07:48 +02:00
Athou
c17cc5bd1c highlight search results 2013-07-31 11:02:39 +02:00
Athou
54e5621267 more infos in entry header in mobile view 2013-07-31 09:18:08 +02:00
Athou
13534b5f44 hide feedback button in mobile view 2013-07-31 08:59:30 +02:00
Athou
43a0c7be81 relationships are not needed for that query 2013-07-30 21:12:52 +02:00
Athou
8ec9705dd6 .gitkeep files not needed 2013-07-30 16:17:16 +02:00
Athou
a82e6f3402 hibernate timeout is in seconds 2013-07-30 14:38:50 +02:00
Athou
704081656e increase batch size again 2013-07-30 13:23:32 +02:00
Athou
201a3ae96f Merge pull request #468 from swoga/patch-1
Added translations to de.properties
2013-07-30 03:35:10 -07:00
swoga
2d54ec9efb Added translations to de.properties 2013-07-30 12:33:57 +02:00
Athou
c1ac273749 increase maximum allowed favicon size 2013-07-30 09:20:52 +02:00
Athou
9bc5fdf02f small batch size seems better than a large one 2013-07-30 06:18:22 +02:00
Athou
8d340e0f52 optimization not needed anymore 2013-07-29 21:31:08 +02:00
Athou
8628ac9e9a use ApplicationPropertiesService 2013-07-29 16:42:52 +02:00
Athou
737e24e7dc more push callback checks 2013-07-29 12:24:03 +02:00
Athou
f5943889ec increase batch size 2013-07-29 12:10:22 +02:00
Athou
61bdd484d3 Merge pull request #467 from yxd-works/patch-3
Update zh.properties
2013-07-29 01:44:57 -07:00
Athou
0ed901ffb6 fix error when emitting event without payload 2013-07-29 10:43:57 +02:00
Athou
5fe5b97130 configurable feed refresh interval 2013-07-29 10:40:55 +02:00
Athou
ef79cf1748 added comments for refill() 2013-07-29 09:47:21 +02:00
YANG Xudong
0f00161b93 Update zh.properties 2013-07-29 11:31:44 +09:00
Athou
0809021c25 Merge pull request #466 from JKakku/patch-1
Update fi.properties
2013-07-28 11:29:45 -07:00
JKakku
ad28b26e72 Update fi.properties 2013-07-28 21:02:22 +03:00
Athou
7827cf49d6 limit queue sizes 2013-07-28 16:36:21 +02:00
Athou
8ed58a8aa5 reduce cache size 2013-07-28 16:14:34 +02:00
Athou
068bb1a0d8 Merge pull request #465 from LpSamuelm/patch-16
Translated new labels to Swedish
2013-07-28 07:11:42 -07:00
LpSamuelm
4f1b458458 Translated new labels to Swedish
woop woop in da boop
2013-07-28 15:41:58 +02:00
Athou
7ad9c24879 fix feed cleanup 2013-07-28 14:35:10 +02:00
Athou
e3e476555a use default thread factory 2013-07-28 12:44:24 +02:00
Athou
223c2f464e index for entry lookup 2013-07-27 17:22:31 +02:00
Athou
6d396e1982 don't fetch the whole content, just return the id if it does 2013-07-27 16:42:50 +02:00
Athou
60bf96411c removed unused urlHash field 2013-07-27 15:45:15 +02:00
Athou
3dd4f140e2 refactored the way we handle feed refresh queue 2013-07-27 15:45:03 +02:00
Athou
1131d70645 move utility method to service (fix #463) 2013-07-27 13:01:28 +02:00
Athou
4b080510e7 new label 2013-07-27 11:22:04 +02:00
Athou
37437877e1 option to force refresh all feeds 2013-07-27 11:21:26 +02:00
Athou
da94880c53 added a little doc 2013-07-26 16:00:02 +02:00
Athou
68ad6d8b55 reuse existing random object 2013-07-26 15:34:14 +02:00
Athou
080c0b48d0 prevent possible NPE 2013-07-26 15:34:02 +02:00
Athou
e8bfecc07d Merge pull request #461 from Athou/statuses-revamp
Statuses revamp
2013-07-26 06:23:06 -07:00
Athou
8e43a7fa00 added update warning 2013-07-26 14:34:00 +02:00
Athou
fa45d1bfad version bump 2013-07-26 14:13:38 +02:00
Athou
9cdc364fde index title hash 2013-07-26 08:15:23 +02:00
Athou
6f29af1710 better locking mechanism 2013-07-25 16:41:48 +02:00
Athou
e77787e2cd ignore case 2013-07-25 14:37:21 +02:00
Athou
84159a3a2d filter out entries that were matched from html code 2013-07-25 14:15:30 +02:00
Athou
68e531ed0c log phrasing 2013-07-25 11:31:49 +02:00
Athou
72bdf2573c tests for fixedsizesortedset 2013-07-25 11:15:57 +02:00
Athou
00159ce1c5 moved methods around 2013-07-25 11:02:49 +02:00
Athou
d293e972f2 added logging 2013-07-25 10:55:19 +02:00
Athou
c618e22c52 revert part of caching mechanism 2013-07-25 10:48:52 +02:00
Athou
73f2871235 revamp cache service 2013-07-25 10:21:11 +02:00
Athou
bdb30a60c3 ignore utility method for (de)serialization 2013-07-25 09:27:09 +02:00
Athou
7da630ed6d run every day at midnight 2013-07-25 09:19:05 +02:00
Athou
8845c54d0c apply formatter 2013-07-25 09:17:33 +02:00
Athou
02f1090fe7 set parameters for delete query 2013-07-25 09:12:56 +02:00
Athou
9bac3f424f index for faster status deletes 2013-07-25 09:12:45 +02:00
Athou
db2264023f added code formatter 2013-07-25 09:12:33 +02:00
Athou
ec8eb4bd1f faster startup 2013-07-24 20:05:58 +02:00
Athou
ed5596636a rewrote with a backing arraylist 2013-07-24 19:41:29 +02:00
Athou
dd0fdfc89e prevent exception when hashing content 2013-07-24 19:19:50 +02:00
Athou
d212e96664 set initial capacity to minimum if unknown capacity 2013-07-24 17:34:46 +02:00
Athou
1a720e6a29 change underlying implementation of the fixedsizesortedset to priority queue 2013-07-24 17:28:10 +02:00
Athou
7316a6e07d revert comparators 2013-07-24 17:25:14 +02:00
Athou
84a75db464 treeset uses compareTo instead of equals 2013-07-24 16:57:01 +02:00
Athou
dde3d8e405 was not meant to be committed 2013-07-24 16:43:00 +02:00
Athou
9e0a39981f use capacity as limit 2013-07-24 16:42:43 +02:00
Athou
dab9f53743 fix metrics 2013-07-24 15:53:21 +02:00
Athou
645164997d move author to content 2013-07-24 15:50:05 +02:00
Athou
c2b53b117c remove many to many relationship between entries and feeds 2013-07-24 15:40:59 +02:00
Athou
6e52f60e85 Merge pull request #458 from JKakku/patch-4
Update fi.properties
2013-07-24 06:02:54 -07:00
JKakku
1498e24037 Update fi.properties
Added version label
2013-07-24 15:41:34 +03:00
Athou
fdacac74cc cleanup unneeded statuses 2013-07-24 12:13:18 +02:00
Athou
3defd982e7 only mark if markable 2013-07-24 12:13:06 +02:00
Athou
f4eb9e2a09 always join for statuses 2013-07-23 16:45:13 +02:00
Athou
08693e16f0 move method around 2013-07-23 16:32:20 +02:00
Athou
d95e1522d8 less database calls 2013-07-23 16:19:19 +02:00
Athou
150920e0c8 delete old statuses 2013-07-23 15:27:56 +02:00
Athou
074ecbf159 persist read status instead of unread status 2013-07-22 16:31:29 +02:00
191 changed files with 6182 additions and 6456 deletions

View File

@@ -6,15 +6,12 @@ Google Reader inspired self-hosted RSS reader, based on JAX-RS, Wicket and Angul
Deploy on your own server (using TomEE, a lightweight JavaEE6 container based on Tomcat) or even in the cloud for free on OpenShift. Deploy on your own server (using TomEE, a lightweight JavaEE6 container based on Tomcat) or even in the cloud for free on OpenShift.
[Android app](https://github.com/doomrobo/CommaFeed-Android-Reader) Related open-source projects
----------------------------
[Chrome extension](https://github.com/Athou/commafeed-chrome) Android apps: [News+ extension](https://github.com/Athou/commafeed-newsplus) - [Android app](https://github.com/doomrobo/CommaFeed-Android-Reader)
[Firefox extension](https://github.com/Athou/commafeed-firefox) 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)
[Opera extension](https://github.com/Athou/commafeed-opera)
[Safari extension](https://github.com/Athou/commafeed-safari)
Deployment on OpenShift Deployment on OpenShift
----------------------- -----------------------
@@ -77,12 +74,15 @@ It will generate a zip file at `target/commafeed.zip` with everything you need t
* If you don't use the embedded database, create a database in your external database instance, then uncomment the `Resource` element corresponding to the database engine you use from `conf/tomee.xml` and edit the default credentials. * If you don't use the embedded database, create a database in your external database instance, then uncomment the `Resource` element corresponding to the database engine you use from `conf/tomee.xml` and edit the default credentials.
* If you'd like to change the default port (8082), edit `conf/server.xml` and look for `<Connector port="8082" protocol="HTTP/1.1"`. Change the port to the value you'd like to use. * If you'd like to change the default port (8082), edit `conf/server.xml` and look for `<Connector port="8082" protocol="HTTP/1.1"`. Change the port to the value you'd like to use.
* CommaFeed will run on the `/commafeed` context. If you'd like to change the context, go to `webapps` and rename `commafeed.war`. Use the special name `ROOT.war` to deploy to the root context. * CommaFeed will run on the `/commafeed` context. If you'd like to change the context, go to `webapps` and rename `commafeed.war`. Use the special name `ROOT.war` to deploy to the root context.
* To start and stop the application, use `bin/startup.sh` and `bin/shutdown.sh` on Linux (you need to `chmod +x bin/*.sh`) or `bin\startup.bat` and `bin\shutdown.bat` on Windows. * To start and stop the application, use `bin/startup.sh` and `bin/shutdown.sh` on Linux (you need to `chmod +x bin/*.sh`) or `bin\startup.bat` and `bin\shutdown.bat` on Windows.
If you use the embedded database, note that the database file will be created in the current directory, so make sure you always start the app in the same directory. You can optionally set an absolute path instead of a relative one in `tomee.xml`.
* To update the application with a newer version, pull the latest changes and use the same command you used to build the complete TomEE package, but without the `tomee:build` part (keep `-Pprod -P<database>`). * To update the application with a newer version, pull the latest changes and use the same command you used to build the complete TomEE package, but without the `tomee:build` part (keep `-Pprod -P<database>`).
This will generate the file `target/commafeed.war`. Copy this file to your tomee `webapps/` directory. This will generate the file `target/commafeed.war`. Copy this file to your tomee `webapps/` directory.
* The application is online at [http://localhost:8082/commafeed](http://localhost:8082/commafeed). Don't forget to set the public URL in the admin settings. * The application is online at [http://localhost:8082/commafeed](http://localhost:8082/commafeed). Don't forget to set the public URL in the admin settings.
* The default user is `admin` and the password is `admin`. * The default user is `admin` and the password is `admin`.
You can use nginix or apache as a proxy http server. Note that when using apache, the `ProxyPreserveHost on` option should be `set in your config file.
Local development Local development
----------------- -----------------

View File

@@ -0,0 +1,283 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="12">
<profile kind="CodeFormatterProfile" name="Eclipse [built-in] 140 chars" version="12">
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
<setting id="org.eclipse.jdt.core.compiler.source" value="1.5"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="140"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="tab"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.5"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="140"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.5"/>
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
</profile>
</profiles>

View File

@@ -0,0 +1,267 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="11">
<profile kind="CodeFormatterProfile" name="Eclipse [built-in] 140 chars" version="11">
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_case" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_empty_lines" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_compact_if" value="52"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_after_annotation" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_header" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_block_comments" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_object_initializer" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_between_type_declarations" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_assignment" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.tabulation.size" value="4"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_default" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.align_type_members_on_columns" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.continuation_indentation_for_objlit_initializer" value="1"/>
<setting id="org.eclipse.wst.jsdt.core.compiler.compliance" value="1.5"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_closing_brace_in_objlit_initializer" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_source_code" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.wrap_before_binary_operator" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_after_package" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_after_comma_in_objlit_initializer" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_javadoc_comments" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indentation.size" value="4"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_objlit_initializer" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.compiler.source" value="1.5"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.continuation_indentation" value="2"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_conditional_expression" value="48"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.indent_parameter_description" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_after_imports" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="64"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.indent_root_tags" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_package" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_member_type" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_enum_constants" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_imports" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_binary_expression" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.lineSplit" value="140"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_html" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_method" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_superclass_in_type_declaration" value="64"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.compiler.codegen.targetPlatform" value="1.5"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.keep_empty_objlit_initializer_on_one_line" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_statements_compare_to_block" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_line_comments" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="64"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.compiler.problem.assertIdentifier" value="error"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_object_initializer" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.line_length" value="80"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_block" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_multiple_fields" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.indent_statements_compare_to_body" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_new_chunk" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.tabulation.char" value="tab"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.put_empty_statement_on_new_line" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_between_import_groups" value="1"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_field" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_after_opening_brace_in_objlit_initializer" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.compact_else_if" value="true"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
</profile>
</profiles>

112
pom.xml
View File

@@ -4,7 +4,7 @@
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>1.0.0</version> <version>1.3.0</version>
<packaging>war</packaging> <packaging>war</packaging>
<name>CommaFeed</name> <name>CommaFeed</name>
@@ -46,9 +46,8 @@
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version> <version>3.1</version>
<configuration> <configuration>
<source>1.7</source> <source>1.6</source>
<target>1.7</target> <target>1.6</target>
<compilerArgument>-proc:none</compilerArgument>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
@@ -65,6 +64,13 @@
<include>**/beans.xml</include> <include>**/beans.xml</include>
</includes> </includes>
</resource> </resource>
<resource>
<directory>target/generated-sources/api-docs/</directory>
<targetPath>api/api-docs</targetPath>
<includes>
<include>**/*</include>
</includes>
</resource>
</webResources> </webResources>
</configuration> </configuration>
</plugin> </plugin>
@@ -138,12 +144,25 @@
<outputDirectory>target/generated-sources/metamodel</outputDirectory> <outputDirectory>target/generated-sources/metamodel</outputDirectory>
</configuration> </configuration>
</execution> </execution>
<execution>
<id>doc</id>
<phase>process-classes</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<processors>
<processor>com.commafeed.frontend.APIGenerator</processor>
</processors>
<outputDirectory>target/generated-sources/api-docs</outputDirectory>
</configuration>
</execution>
</executions> </executions>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.hibernate</groupId> <groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId> <artifactId>hibernate-jpamodelgen</artifactId>
<version>1.2.0.Final</version> <version>1.3.0.Final</version>
</dependency> </dependency>
</dependencies> </dependencies>
</plugin> </plugin>
@@ -167,6 +186,12 @@
</build> </build>
<dependencies> <dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>0.12.0</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.jboss.spec</groupId> <groupId>org.jboss.spec</groupId>
<artifactId>jboss-javaee-6.0</artifactId> <artifactId>jboss-javaee-6.0</artifactId>
@@ -237,16 +262,6 @@
<artifactId>commons-fileupload</artifactId> <artifactId>commons-fileupload</artifactId>
<version>1.3</version> <version>1.3</version>
</dependency> </dependency>
<dependency>
<groupId>com.googlecode.lambdaj</groupId>
<artifactId>lambdaj</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.2</version>
</dependency>
<dependency> <dependency>
<groupId>net.java.dev.rome</groupId> <groupId>net.java.dev.rome</groupId>
@@ -270,11 +285,6 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>edu.uci.ics</groupId>
<artifactId>crawler4j</artifactId>
<version>3.5</version>
</dependency>
<dependency> <dependency>
<groupId>org.jdom</groupId> <groupId>org.jdom</groupId>
<artifactId>jdom</artifactId> <artifactId>jdom</artifactId>
@@ -296,17 +306,6 @@
<version>0.9.9</version> <version>0.9.9</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client-servlet</artifactId>
<version>1.14.1-beta</version>
</dependency>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client-jackson2</artifactId>
<version>1.14.1-beta</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.httpcomponents</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>
@@ -319,9 +318,9 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId> <artifactId>jackson-databind</artifactId>
<version>2.1.4</version> <version>2.2.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
@@ -337,23 +336,23 @@
<dependency> <dependency>
<groupId>org.apache.wicket</groupId> <groupId>org.apache.wicket</groupId>
<artifactId>wicket-core</artifactId> <artifactId>wicket-core</artifactId>
<version>6.8.0</version> <version>6.10.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.wicket</groupId> <groupId>org.apache.wicket</groupId>
<artifactId>wicket-auth-roles</artifactId> <artifactId>wicket-auth-roles</artifactId>
<version>6.8.0</version> <version>6.10.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.wicket</groupId> <groupId>org.apache.wicket</groupId>
<artifactId>wicket-extensions</artifactId> <artifactId>wicket-extensions</artifactId>
<version>6.8.0</version> <version>6.10.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.wicket</groupId> <groupId>org.apache.wicket</groupId>
<artifactId>wicket-cdi</artifactId> <artifactId>wicket-cdi</artifactId>
<version>6.8.0</version> <version>6.10.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ro.isdc.wro4j</groupId> <groupId>ro.isdc.wro4j</groupId>
@@ -361,28 +360,27 @@
<version>1.6.3</version> <version>1.6.3</version>
</dependency> </dependency>
<dependency>
<groupId>com.wordnik</groupId>
<artifactId>swagger-annotations_2.9.1</artifactId>
<version>1.2.5</version>
</dependency>
<dependency> <dependency>
<groupId>com.wordnik</groupId> <groupId>com.wordnik</groupId>
<artifactId>swagger-jaxrs_2.9.1</artifactId> <artifactId>swagger-jaxrs_2.9.1</artifactId>
<version>1.2.5</version> <version>1.2.5</version>
<exclusions> <scope>provided</scope>
<exclusion> </dependency>
<artifactId>jersey-server</artifactId>
<groupId>com.sun.jersey</groupId> <dependency>
</exclusion> <groupId>com.codahale.metrics</groupId>
<exclusion> <artifactId>metrics-core</artifactId>
<artifactId>jersey-servlet</artifactId> <version>3.0.1</version>
<groupId>com.sun.jersey</groupId> </dependency>
</exclusion> <dependency>
<exclusion> <groupId>com.codahale.metrics</groupId>
<artifactId>jersey-client</artifactId> <artifactId>metrics-json</artifactId>
<groupId>com.sun.jersey</groupId> <version>3.0.1</version>
</exclusion>
<exclusion>
<artifactId>jersey-core</artifactId>
<groupId>com.sun.jersey</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
@@ -518,7 +516,7 @@
</dependencies> </dependencies>
<executions> <executions>
<execution> <execution>
<phase>generate-sources</phase> <phase>process-classes</phase>
<goals> <goals>
<goal>execute</goal> <goal>execute</goal>
</goals> </goals>

View File

View File

@@ -1,62 +1,49 @@
package com.commafeed.backend; package com.commafeed.backend;
import java.util.Collection; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.TreeSet;
import org.apache.commons.collections.CollectionUtils; /**
* List wrapper that sorts its elements in the order provided by given comparator and ensure a maximum capacity.
*
*
*/
public class FixedSizeSortedSet<E> {
import com.google.common.collect.Lists; private List<E> inner;
public class FixedSizeSortedSet<E> extends TreeSet<E> {
private static final long serialVersionUID = 1L;
private final Comparator<? super E> comparator; private final Comparator<? super E> comparator;
private final int maxSize; private final int capacity;
public FixedSizeSortedSet(int maxSize, Comparator<? super E> comparator) { public FixedSizeSortedSet(int capacity, Comparator<? super E> comparator) {
super(comparator); this.inner = new ArrayList<E>(Math.max(0, capacity));
this.maxSize = maxSize; this.capacity = capacity < 0 ? Integer.MAX_VALUE : capacity;
this.comparator = comparator; this.comparator = comparator;
} }
@Override public void add(E e) {
public boolean add(E e) { int position = Math.abs(Collections.binarySearch(inner, e, comparator) + 1);
if (isFull()) { if (isFull()) {
E last = last(); if (position < inner.size()) {
int comparison = comparator.compare(e, last); inner.remove(inner.size() - 1);
if (comparison < 0) { inner.add(position, e);
remove(last);
return super.add(e);
} else {
return false;
} }
} else { } else {
return super.add(e); inner.add(position, e);
} }
} }
@Override public E last() {
public boolean addAll(Collection<? extends E> c) { return inner.get(inner.size() - 1);
if (CollectionUtils.isEmpty(c)) {
return false;
}
boolean success = true;
for (E e : c) {
success &= add(e);
}
return success;
} }
public boolean isFull() { public boolean isFull() {
return size() == maxSize; return inner.size() == capacity;
} }
@SuppressWarnings("unchecked")
public List<E> asList() { public List<E> asList() {
return (List<E>) Lists.newArrayList(toArray()); return inner;
} }
} }

View File

@@ -13,16 +13,20 @@ import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException; import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.CookiePolicy; import org.apache.http.client.params.CookiePolicy;
import org.apache.http.client.params.HttpClientParams; import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.ClientConnectionManager;
@@ -37,14 +41,18 @@ import org.apache.http.impl.client.SystemDefaultHttpClient;
import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams; import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams; import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Smart HTTP getter: handles gzip, ssl, last modified and etag headers
*
*/
@Slf4j
public class HttpGetter { public class HttpGetter {
private static Logger log = LoggerFactory.getLogger(HttpGetter.class);
private static final String USER_AGENT = "CommaFeed/1.0 (http://www.commafeed.com)"; private static final String USER_AGENT = "CommaFeed/1.0 (http://www.commafeed.com)";
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";
@@ -56,9 +64,7 @@ public class HttpGetter {
static { static {
try { try {
SSL_CONTEXT = SSLContext.getInstance("TLS"); SSL_CONTEXT = SSLContext.getInstance("TLS");
SSL_CONTEXT.init(new KeyManager[0], SSL_CONTEXT.init(new KeyManager[0], new TrustManager[] { new DefaultTrustManager() }, new SecureRandom());
new TrustManager[] { new DefaultTrustManager() },
new SecureRandom());
} catch (Exception e) { } catch (Exception e) {
log.error("Could not configure ssl context"); log.error("Could not configure ssl context");
} }
@@ -66,8 +72,7 @@ public class HttpGetter {
private static final X509HostnameVerifier VERIFIER = new DefaultHostnameVerifier(); private static final X509HostnameVerifier VERIFIER = new DefaultHostnameVerifier();
public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
IOException, NotModifiedException {
return getBinary(url, null, null, timeout); return getBinary(url, null, null, timeout);
} }
@@ -85,14 +90,16 @@ public class HttpGetter {
* @throws NotModifiedException * @throws NotModifiedException
* if the url hasn't changed since we asked for it last time * if the url hasn't changed since we asked for it last time
*/ */
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws ClientProtocolException, IOException,
throws ClientProtocolException, IOException, NotModifiedException { NotModifiedException {
HttpResult result = null; HttpResult result = null;
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
HttpClient client = newClient(timeout); HttpClient client = newClient(timeout);
try { try {
HttpGet httpget = new HttpGet(url); HttpGet httpget = new HttpGet(url);
HttpContext context = new BasicHttpContext();
httpget.addHeader(HttpHeaders.ACCEPT_LANGUAGE, ACCEPT_LANGUAGE); httpget.addHeader(HttpHeaders.ACCEPT_LANGUAGE, ACCEPT_LANGUAGE);
httpget.addHeader(HttpHeaders.PRAGMA, PRAGMA_NO_CACHE); httpget.addHeader(HttpHeaders.PRAGMA, PRAGMA_NO_CACHE);
httpget.addHeader(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); httpget.addHeader(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
@@ -107,50 +114,48 @@ public class HttpGetter {
HttpResponse response = null; HttpResponse response = null;
try { try {
response = client.execute(httpget); response = client.execute(httpget, context);
int code = response.getStatusLine().getStatusCode(); int code = response.getStatusLine().getStatusCode();
if (code == HttpStatus.SC_NOT_MODIFIED) { if (code == HttpStatus.SC_NOT_MODIFIED) {
throw new NotModifiedException("304 http code"); throw new NotModifiedException("'304 - not modified' http code received");
} else if (code >= 300) { } else if (code >= 300) {
throw new HttpResponseException(code, throw new HttpResponseException(code, "Server returned HTTP error code " + code);
"Server returned HTTP error code " + code);
} }
} catch (HttpResponseException e) { } catch (HttpResponseException e) {
if (e.getStatusCode() == HttpStatus.SC_NOT_MODIFIED) { if (e.getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
throw new NotModifiedException("304 http code"); throw new NotModifiedException("'304 - not modified' http code received");
} else { } else {
throw e; throw e;
} }
} }
Header lastModifiedHeader = response Header lastModifiedHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED);
.getFirstHeader(HttpHeaders.LAST_MODIFIED); String lastModifiedHeaderValue = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG); if (lastModifiedHeaderValue != null && StringUtils.equals(lastModified, lastModifiedHeaderValue)) {
String lastModifiedResponse = lastModifiedHeader == null ? null
: StringUtils.trimToNull(lastModifiedHeader.getValue());
if (lastModified != null
&& StringUtils.equals(lastModified, lastModifiedResponse)) {
throw new NotModifiedException("lastModifiedHeader is the same"); throw new NotModifiedException("lastModifiedHeader is the same");
} }
String eTagResponse = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue()); Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
if (eTag != null && StringUtils.equals(eTag, eTagResponse)) { String eTagHeaderValue = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue());
if (eTag != null && StringUtils.equals(eTag, eTagHeaderValue)) {
throw new NotModifiedException("eTagHeader is the same"); throw new NotModifiedException("eTagHeader is the same");
} }
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
byte[] content = null; byte[] content = null;
String contentType = null;
if (entity != null) { if (entity != null) {
content = EntityUtils.toByteArray(entity); content = EntityUtils.toByteArray(entity);
if (entity.getContentType() != null) {
contentType = entity.getContentType().getValue();
}
} }
HttpUriRequest req = (HttpUriRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST);
HttpHost host = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
String urlAfterRedirect = req.getURI().isAbsolute() ? req.getURI().toString() : host.toURI() + req.getURI();
long duration = System.currentTimeMillis() - start; long duration = System.currentTimeMillis() - start;
Header contentType = entity.getContentType(); result = new HttpResult(content, contentType, lastModifiedHeaderValue, eTagHeaderValue, duration, urlAfterRedirect);
result = new HttpResult(content, contentType == null ? null
: contentType.getValue(), lastModifiedHeader == null ? null
: lastModifiedHeader.getValue(), eTagHeader == null ? null
: eTagHeader.getValue(), duration);
} finally { } finally {
client.getConnectionManager().shutdown(); client.getConnectionManager().shutdown();
} }
@@ -164,14 +169,15 @@ public class HttpGetter {
private String lastModifiedSince; private String lastModifiedSince;
private String eTag; private String eTag;
private long duration; private long duration;
private String urlAfterRedirect;
public HttpResult(byte[] content, String contentType, public HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, long duration, String urlAfterRedirect) {
String lastModifiedSince, String eTag, long duration) {
this.content = content; this.content = content;
this.contentType = contentType; this.contentType = contentType;
this.lastModifiedSince = lastModifiedSince; this.lastModifiedSince = lastModifiedSince;
this.eTag = eTag; this.eTag = eTag;
this.duration = duration; this.duration = duration;
this.urlAfterRedirect = urlAfterRedirect;
} }
public byte[] getContent() { public byte[] getContent() {
@@ -194,6 +200,9 @@ public class HttpGetter {
return duration; return duration;
} }
public String getUrlAfterRedirect() {
return urlAfterRedirect;
}
} }
public static HttpClient newClient(int timeout) { public static HttpClient newClient(int timeout) {
@@ -209,8 +218,7 @@ public class HttpGetter {
HttpProtocolParams.setContentCharset(params, UTF8); HttpProtocolParams.setContentCharset(params, UTF8);
HttpConnectionParams.setConnectionTimeout(params, timeout); HttpConnectionParams.setConnectionTimeout(params, timeout);
HttpConnectionParams.setSoTimeout(params, timeout); HttpConnectionParams.setSoTimeout(params, timeout);
client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
false));
return new DecompressingHttpClient(client); return new DecompressingHttpClient(client);
} }
@@ -225,13 +233,11 @@ public class HttpGetter {
private static class DefaultTrustManager implements X509TrustManager { private static class DefaultTrustManager implements X509TrustManager {
@Override @Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
throws CertificateException {
} }
@Override @Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
throws CertificateException {
} }
@Override @Override
@@ -240,21 +246,18 @@ public class HttpGetter {
} }
} }
private static class DefaultHostnameVerifier implements private static class DefaultHostnameVerifier implements X509HostnameVerifier {
X509HostnameVerifier {
@Override @Override
public void verify(String string, SSLSocket ssls) throws IOException { public void verify(String string, SSLSocket ssls) throws IOException {
} }
@Override @Override
public void verify(String string, X509Certificate xc) public void verify(String string, X509Certificate xc) throws SSLException {
throws SSLException {
} }
@Override @Override
public void verify(String string, String[] strings, String[] strings1) public void verify(String string, String[] strings, String[] strings1) throws SSLException {
throws SSLException {
} }
@Override @Override

View File

@@ -1,190 +0,0 @@
package com.commafeed.backend;
import javax.ejb.Singleton;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
@Singleton
public class MetricsBean {
@PersistenceContext
EntityManager em;
private Metric lastMinute = new Metric();
private Metric thisMinute = new Metric();
private Metric lastHour = new Metric();
private Metric thisHour = new Metric();
private long minuteTimestamp;
private long hourTimestamp;
@AroundInvoke
private Object roll(InvocationContext context) throws Exception {
long now = System.currentTimeMillis();
if (now - minuteTimestamp > 60000) {
lastMinute = thisMinute;
thisMinute = new Metric();
minuteTimestamp = now;
}
if (now - hourTimestamp > 60000 * 60) {
lastHour = thisHour;
thisHour = new Metric();
hourTimestamp = now;
}
return context.proceed();
}
public void feedRefreshed() {
thisMinute.feedsRefreshed++;
thisHour.feedsRefreshed++;
}
public void feedUpdated() {
thisHour.feedsUpdated++;
thisMinute.feedsUpdated++;
}
public void entryUpdated(int statusesCount) {
thisHour.entriesInserted++;
thisMinute.entriesInserted++;
thisHour.statusesInserted += statusesCount;
thisMinute.statusesInserted += statusesCount;
}
public void entryCacheHit() {
thisHour.entryCacheHit++;
thisMinute.entryCacheHit++;
}
public void entryCacheMiss() {
thisHour.entryCacheMiss++;
thisMinute.entryCacheMiss++;
}
public void pushReceived(int feedCount) {
thisHour.pushNotificationsReceived++;
thisMinute.pushNotificationsReceived++;
thisHour.pushFeedsQueued += feedCount;
thisMinute.pushFeedsQueued += feedCount;
}
public void threadWaited() {
thisHour.threadWaited++;
thisMinute.threadWaited++;
}
public Metric getLastMinute() {
return lastMinute;
}
public Metric getLastHour() {
return lastHour;
}
public String getCacheStats() {
Session session = em.unwrap(Session.class);
SessionFactory sessionFactory = session.getSessionFactory();
Statistics statistics = sessionFactory.getStatistics();
return statistics.toString();
}
public static class Metric {
private int feedsRefreshed;
private int feedsUpdated;
private int entriesInserted;
private int statusesInserted;
private int threadWaited;
private int pushNotificationsReceived;
private int pushFeedsQueued;
private int entryCacheHit;
private int entryCacheMiss;
public int getFeedsRefreshed() {
return feedsRefreshed;
}
public void setFeedsRefreshed(int feedsRefreshed) {
this.feedsRefreshed = feedsRefreshed;
}
public int getFeedsUpdated() {
return feedsUpdated;
}
public void setFeedsUpdated(int feedsUpdated) {
this.feedsUpdated = feedsUpdated;
}
public int getEntriesInserted() {
return entriesInserted;
}
public void setEntriesInserted(int entriesInserted) {
this.entriesInserted = entriesInserted;
}
public int getStatusesInserted() {
return statusesInserted;
}
public void setStatusesInserted(int statusesInserted) {
this.statusesInserted = statusesInserted;
}
public int getThreadWaited() {
return threadWaited;
}
public void setThreadWaited(int threadWaited) {
this.threadWaited = threadWaited;
}
public int getPushNotificationsReceived() {
return pushNotificationsReceived;
}
public void setPushNotificationsReceived(int pushNotificationsReceived) {
this.pushNotificationsReceived = pushNotificationsReceived;
}
public int getPushFeedsQueued() {
return pushFeedsQueued;
}
public void setPushFeedsQueued(int pushFeedsQueued) {
this.pushFeedsQueued = pushFeedsQueued;
}
public int getEntryCacheHit() {
return entryCacheHit;
}
public void setEntryCacheHit(int entryCacheHit) {
this.entryCacheHit = entryCacheHit;
}
public int getEntryCacheMiss() {
return entryCacheMiss;
}
public void setEntryCacheMiss(int entryCacheMiss) {
this.entryCacheMiss = entryCacheMiss;
}
}
}

View File

@@ -0,0 +1,38 @@
package com.commafeed.backend;
import java.util.Date;
import javax.ejb.Schedule;
import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.inject.Inject;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.DatabaseCleaningService;
/**
* Contains all scheduled tasks
*
*/
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class ScheduledTasks {
@Inject
ApplicationSettingsService applicationSettingsService;
@Inject
DatabaseCleaningService cleaner;
/**
* clean old read statuses, runs every day at midnight
*/
@Schedule(hour = "0", persistent = false)
private void cleanupOldStatuses() {
Date threshold = applicationSettingsService.getUnreadThreshold();
if (threshold != null) {
cleaner.cleanStatusesOlderThan(threshold);
}
}
}

View File

@@ -1,34 +1,39 @@
package com.commafeed.backend.cache; package com.commafeed.backend.cache;
import java.util.List; import java.util.List;
import java.util.Map;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
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.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.UnreadCount;
public abstract class CacheService { public abstract class CacheService {
// feed entries for faster refresh
public abstract List<String> getLastEntries(Feed feed); public abstract List<String> getLastEntries(Feed feed);
public abstract void setLastEntries(Feed feed, List<String> entries); public abstract void setLastEntries(Feed feed, List<String> entries);
public String buildUniqueEntryKey(Feed feed, FeedEntry entry) { public String buildUniqueEntryKey(Feed feed, FeedEntry entry) {
return DigestUtils.sha1Hex(entry.getGuid() + return DigestUtils.sha1Hex(entry.getGuid() + entry.getUrl());
entry.getUrl());
} }
public abstract Category getRootCategory(User user); // user categories
public abstract Category getUserRootCategory(User user);
public abstract void setRootCategory(User user, Category category); public abstract void setUserRootCategory(User user, Category category);
public abstract Map<Long, Long> getUnreadCounts(User user);
public abstract void setUnreadCounts(User user, Map<Long, Long> map); public abstract void invalidateUserRootCategory(User... users);
public abstract void invalidateUserData(User... users); // unread count
public abstract UnreadCount getUnreadCount(FeedSubscription sub);
public abstract void setUnreadCount(FeedSubscription sub, UnreadCount count);
public abstract void invalidateUnreadCount(FeedSubscription... subs);
} }

View File

@@ -2,14 +2,15 @@ package com.commafeed.backend.cache;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative; import javax.enterprise.inject.Alternative;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.UnreadCount;
@Alternative @Alternative
@ApplicationScoped @ApplicationScoped
@@ -25,27 +26,32 @@ public class NoopCacheService extends CacheService {
} }
@Override @Override
public Category getRootCategory(User user) { public UnreadCount getUnreadCount(FeedSubscription sub) {
return null; return null;
} }
@Override @Override
public void setRootCategory(User user, Category category) { public void setUnreadCount(FeedSubscription sub, UnreadCount count) {
} }
@Override @Override
public Map<Long, Long> getUnreadCounts(User user) { public void invalidateUnreadCount(FeedSubscription... subs) {
}
@Override
public Category getUserRootCategory(User user) {
return null; return null;
} }
@Override @Override
public void setUnreadCounts(User user, Map<Long, Long> map) { public void setUserRootCategory(User user, Category category) {
} }
@Override @Override
public void invalidateUserData(User... users) { public void invalidateUserRootCategory(User... users) {
} }

View File

@@ -1,39 +1,36 @@
package com.commafeed.backend.cache; package com.commafeed.backend.cache;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative; import javax.enterprise.inject.Alternative;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis; import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Pipeline; import redis.clients.jedis.Pipeline;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
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.Category; import com.commafeed.frontend.model.Category;
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.fasterxml.jackson.databind.type.MapType; import com.google.common.collect.Lists;
import com.google.api.client.util.Lists;
@Alternative @Alternative
@ApplicationScoped @ApplicationScoped
@Slf4j
public class RedisCacheService extends CacheService { public class RedisCacheService extends CacheService {
private static final Logger log = LoggerFactory private static ObjectMapper mapper = new ObjectMapper();
.getLogger(RedisCacheService.class);
private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost"); private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
private ObjectMapper mapper = new ObjectMapper();
@Override @Override
public List<String> getLastEntries(Feed feed) { public List<String> getLastEntries(Feed feed) {
@@ -70,11 +67,11 @@ public class RedisCacheService extends CacheService {
} }
@Override @Override
public Category getRootCategory(User user) { public Category getUserRootCategory(User user) {
Category cat = null; Category cat = null;
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
String key = buildRedisRootCategoryKey(user); String key = buildRedisUserRootCategoryKey(user);
String json = jedis.get(key); String json = jedis.get(key);
if (json != null) { if (json != null) {
cat = mapper.readValue(json, Category.class); cat = mapper.readValue(json, Category.class);
@@ -88,10 +85,10 @@ public class RedisCacheService extends CacheService {
} }
@Override @Override
public void setRootCategory(User user, Category category) { public void setUserRootCategory(User user, Category category) {
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
String key = buildRedisRootCategoryKey(user); String key = buildRedisUserRootCategoryKey(user);
Pipeline pipe = jedis.pipelined(); Pipeline pipe = jedis.pipelined();
pipe.del(key); pipe.del(key);
@@ -106,37 +103,35 @@ public class RedisCacheService extends CacheService {
} }
@Override @Override
public Map<Long, Long> getUnreadCounts(User user) { public UnreadCount getUnreadCount(FeedSubscription sub) {
Map<Long, Long> map = null; UnreadCount count = null;
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
String key = buildRedisUnreadCountKey(user); String key = buildRedisUnreadCountKey(sub);
String json = jedis.get(key); String json = jedis.get(key);
if (json != null) { if (json != null) {
MapType type = mapper.getTypeFactory().constructMapType( count = mapper.readValue(json, UnreadCount.class);
Map.class, Long.class, Long.class);
map = mapper.readValue(json, type);
} }
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} finally { } finally {
pool.returnResource(jedis); pool.returnResource(jedis);
} }
return map; return count;
} }
@Override @Override
public void setUnreadCounts(User user, Map<Long, Long> map) { public void setUnreadCount(FeedSubscription sub, UnreadCount count) {
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
String key = buildRedisUnreadCountKey(user); String key = buildRedisUnreadCountKey(sub);
Pipeline pipe = jedis.pipelined(); Pipeline pipe = jedis.pipelined();
pipe.del(key); pipe.del(key);
pipe.set(key, mapper.writeValueAsString(map)); pipe.set(key, mapper.writeValueAsString(count));
pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30)); pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30));
pipe.sync(); pipe.sync();
} catch (JsonProcessingException e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} finally { } finally {
pool.returnResource(jedis); pool.returnResource(jedis);
@@ -144,15 +139,13 @@ public class RedisCacheService extends CacheService {
} }
@Override @Override
public void invalidateUserData(User... users) { public void invalidateUserRootCategory(User... users) {
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
Pipeline pipe = jedis.pipelined(); Pipeline pipe = jedis.pipelined();
if (users != null) { if (users != null) {
for (User user : users) { for (User user : users) {
String key = buildRedisRootCategoryKey(user); String key = buildRedisUserRootCategoryKey(user);
pipe.del(key);
key = buildRedisUnreadCountKey(user);
pipe.del(key); pipe.del(key);
} }
} }
@@ -162,16 +155,33 @@ public class RedisCacheService extends CacheService {
} }
} }
private String buildRedisRootCategoryKey(User user) { @Override
return "root_cat:" + Models.getId(user); public void invalidateUnreadCount(FeedSubscription... subs) {
} Jedis jedis = pool.getResource();
try {
private String buildRedisUnreadCountKey(User user) { Pipeline pipe = jedis.pipelined();
return "unread_count:" + Models.getId(user); if (subs != null) {
for (FeedSubscription sub : subs) {
String key = buildRedisUnreadCountKey(sub);
pipe.del(key);
}
}
pipe.sync();
} finally {
pool.returnResource(jedis);
}
} }
private String buildRedisEntryKey(Feed feed) { private String buildRedisEntryKey(Feed feed) {
return "feed:" + feed.getId(); return "f:" + Models.getId(feed);
}
private String buildRedisUserRootCategoryKey(User user) {
return "c:" + Models.getId(user);
}
private String buildRedisUnreadCountKey(FeedSubscription sub) {
return "u:" + Models.getId(sub);
} }
} }

View File

@@ -26,8 +26,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
CriteriaQuery<FeedCategory> query = builder.createQuery(getType()); CriteriaQuery<FeedCategory> query = builder.createQuery(getType());
Root<FeedCategory> root = query.from(getType()); Root<FeedCategory> root = query.from(getType());
Join<FeedCategory, User> userJoin = (Join<FeedCategory, User>) root Join<FeedCategory, User> userJoin = (Join<FeedCategory, User>) root.fetch(FeedCategory_.user);
.fetch(FeedCategory_.user);
query.where(builder.equal(userJoin.get(User_.id), user.getId())); query.where(builder.equal(userJoin.get(User_.id), user.getId()));
@@ -38,14 +37,12 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
CriteriaQuery<FeedCategory> query = builder.createQuery(getType()); CriteriaQuery<FeedCategory> query = builder.createQuery(getType());
Root<FeedCategory> root = query.from(getType()); Root<FeedCategory> root = query.from(getType());
Predicate p1 = builder.equal( Predicate p1 = builder.equal(root.get(FeedCategory_.user).get(User_.id), user.getId());
root.get(FeedCategory_.user).get(User_.id), user.getId());
Predicate p2 = builder.equal(root.get(FeedCategory_.id), id); Predicate p2 = builder.equal(root.get(FeedCategory_.id), id);
query.where(p1, p2); query.where(p1, p2);
return Iterables.getFirst(cache(em.createQuery(query)).getResultList(), return Iterables.getFirst(cache(em.createQuery(query)).getResultList(), null);
null);
} }
public FeedCategory findByName(User user, String name, FeedCategory parent) { public FeedCategory findByName(User user, String name, FeedCategory parent) {
@@ -60,8 +57,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
if (parent == null) { if (parent == null) {
predicates.add(builder.isNull(root.get(FeedCategory_.parent))); predicates.add(builder.isNull(root.get(FeedCategory_.parent)));
} else { } else {
predicates predicates.add(builder.equal(root.get(FeedCategory_.parent), parent));
.add(builder.equal(root.get(FeedCategory_.parent), parent));
} }
query.where(predicates.toArray(new Predicate[0])); query.where(predicates.toArray(new Predicate[0]));
@@ -85,8 +81,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
if (parent == null) { if (parent == null) {
predicates.add(builder.isNull(root.get(FeedCategory_.parent))); predicates.add(builder.isNull(root.get(FeedCategory_.parent)));
} else { } else {
predicates predicates.add(builder.equal(root.get(FeedCategory_.parent), parent));
.add(builder.equal(root.get(FeedCategory_.parent), parent));
} }
query.where(predicates.toArray(new Predicate[0])); query.where(predicates.toArray(new Predicate[0]));
@@ -94,8 +89,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
return em.createQuery(query).getResultList(); return em.createQuery(query).getResultList();
} }
public List<FeedCategory> findAllChildrenCategories(User user, public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {
FeedCategory parent) {
List<FeedCategory> list = Lists.newArrayList(); List<FeedCategory> list = Lists.newArrayList();
List<FeedCategory> all = findAll(user); List<FeedCategory> all = findAll(user);
for (FeedCategory cat : all) { for (FeedCategory cat : all) {

View File

@@ -4,19 +4,17 @@ import java.util.Date;
import java.util.List; import java.util.List;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.persistence.Query;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression; import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType; import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Path; import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin; import javax.persistence.criteria.SetJoin;
import javax.persistence.criteria.Subquery;
import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.SingularAttribute;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
@@ -26,59 +24,55 @@ import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.FeedSubscription_; import com.commafeed.backend.model.FeedSubscription_;
import com.commafeed.backend.model.Feed_; import com.commafeed.backend.model.Feed_;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.User_;
import com.commafeed.frontend.model.FeedCount;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@Stateless @Stateless
public class FeedDAO extends GenericDAO<Feed> { public class FeedDAO extends GenericDAO<Feed> {
@XmlRootElement private List<Predicate> getUpdatablePredicates(CriteriaQuery<?> query, Root<Feed> root, Date lastLoginThreshold) {
@XmlAccessorType(XmlAccessType.FIELD)
public static class FeedCount { List<Predicate> preds = Lists.newArrayList();
public String value; Predicate isNull = builder.isNull(root.get(Feed_.disabledUntil));
public List<Feed> feeds; Predicate lessThan = builder.lessThan(root.get(Feed_.disabledUntil), new Date());
preds.add(builder.or(isNull, lessThan));
if (lastLoginThreshold != null) {
Subquery<Long> subquery = query.subquery(Long.class);
Root<FeedSubscription> subroot = subquery.from(FeedSubscription.class);
subquery.select(builder.count(subroot.get(FeedSubscription_.id)));
Join<FeedSubscription, User> userJoin = subroot.join(FeedSubscription_.user);
Predicate p1 = builder.equal(subroot.get(FeedSubscription_.feed), root);
Predicate p2 = builder.greaterThanOrEqualTo(userJoin.get(User_.lastLogin), lastLoginThreshold);
subquery.where(p1, p2);
preds.add(builder.exists(subquery));
}
return preds;
} }
private List<Predicate> getUpdatablePredicates(Root<Feed> root, public Long getUpdatableCount(Date lastLoginThreshold) {
Date threshold) {
Predicate hasSubscriptions = builder.isNotEmpty(root
.get(Feed_.subscriptions));
Predicate neverUpdated = builder.isNull(root.get(Feed_.lastUpdated));
Predicate updatedBeforeThreshold = builder.lessThan(
root.get(Feed_.lastUpdated), threshold);
Predicate disabledDateIsNull = builder.isNull(root
.get(Feed_.disabledUntil));
Predicate disabledDateIsInPast = builder.lessThan(
root.get(Feed_.disabledUntil), new Date());
return Lists.newArrayList(hasSubscriptions,
builder.or(neverUpdated, updatedBeforeThreshold),
builder.or(disabledDateIsNull, disabledDateIsInPast));
}
public Long getUpdatableCount(Date threshold) {
CriteriaQuery<Long> query = builder.createQuery(Long.class); CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());
query.select(builder.count(root)); query.select(builder.count(root));
query.where(getUpdatablePredicates(root, threshold).toArray( query.where(getUpdatablePredicates(query, root, lastLoginThreshold).toArray(new Predicate[0]));
new Predicate[0]));
TypedQuery<Long> q = em.createQuery(query); TypedQuery<Long> q = em.createQuery(query);
return q.getSingleResult(); return q.getSingleResult();
} }
public List<Feed> findNextUpdatable(int count, Date threshold) { public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
CriteriaQuery<Feed> query = builder.createQuery(getType()); CriteriaQuery<Feed> query = builder.createQuery(getType());
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());
query.where(getUpdatablePredicates(root, threshold).toArray( query.where(getUpdatablePredicates(query, root, lastLoginThreshold).toArray(new Predicate[0]));
new Predicate[0])); query.orderBy(builder.asc(root.get(Feed_.disabledUntil)));
query.orderBy(builder.asc(root.get(Feed_.lastUpdated)));
TypedQuery<Feed> q = em.createQuery(query); TypedQuery<Feed> q = em.createQuery(query);
q.setMaxResults(count); q.setMaxResults(count);
@@ -87,18 +81,11 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public Feed findByUrl(String url) { public Feed findByUrl(String url) {
List<Feed> feeds = findByField(Feed_.urlHash, DigestUtils.sha1Hex(url));
Feed feed = Iterables.getFirst(feeds, null);
if (feed != null && StringUtils.equals(url, feed.getUrl())) {
return feed;
}
String normalized = FeedUtils.normalizeURL(url); String normalized = FeedUtils.normalizeURL(url);
feeds = findByField(Feed_.normalizedUrlHash, List<Feed> feeds = findByField(Feed_.normalizedUrlHash, DigestUtils.sha1Hex(normalized));
DigestUtils.sha1Hex(normalized)); Feed feed = Iterables.getFirst(feeds, null);
feed = Iterables.getFirst(feeds, null); if (feed != null && StringUtils.equals(normalized, feed.getNormalizedUrl())) {
if (feed != null
&& StringUtils.equals(normalized, feed.getNormalizedUrl())) {
return feed; return feed;
} }
@@ -109,19 +96,11 @@ public class FeedDAO extends GenericDAO<Feed> {
return findByField(Feed_.pushTopicHash, DigestUtils.sha1Hex(topic)); return findByField(Feed_.pushTopicHash, DigestUtils.sha1Hex(topic));
} }
public void deleteRelationships(Feed feed) {
Query relationshipDeleteQuery = em
.createNamedQuery("Feed.deleteEntryRelationships");
relationshipDeleteQuery.setParameter("feedId", feed.getId());
relationshipDeleteQuery.executeUpdate();
}
public int deleteWithoutSubscriptions(int max) { public int deleteWithoutSubscriptions(int max) {
CriteriaQuery<Feed> query = builder.createQuery(getType()); CriteriaQuery<Feed> query = builder.createQuery(getType());
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());
SetJoin<Feed, FeedSubscription> join = root.join(Feed_.subscriptions, SetJoin<Feed, FeedSubscription> join = root.join(Feed_.subscriptions, JoinType.LEFT);
JoinType.LEFT);
query.where(builder.isNull(join.get(FeedSubscription_.id))); query.where(builder.isNull(join.get(FeedSubscription_.id)));
TypedQuery<Feed> q = em.createQuery(query); TypedQuery<Feed> q = em.createQuery(query);
q.setMaxResults(max); q.setMaxResults(max);
@@ -129,17 +108,13 @@ public class FeedDAO extends GenericDAO<Feed> {
List<Feed> list = q.getResultList(); List<Feed> list = q.getResultList();
int deleted = list.size(); int deleted = list.size();
for (Feed feed : list) { delete(list);
deleteRelationships(feed);
delete(feed);
}
return deleted; return deleted;
} }
public static enum DuplicateMode { public static enum DuplicateMode {
NORMALIZED_URL(Feed_.normalizedUrlHash), LAST_CONTENT( NORMALIZED_URL(Feed_.normalizedUrlHash), LAST_CONTENT(Feed_.lastContentHash), PUSH_TOPIC(Feed_.pushTopicHash);
Feed_.lastContentHash), PUSH_TOPIC(Feed_.pushTopicHash);
private SingularAttribute<Feed, String> path; private SingularAttribute<Feed, String> path;
private DuplicateMode(SingularAttribute<Feed, String> path) { private DuplicateMode(SingularAttribute<Feed, String> path) {
@@ -151,8 +126,7 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
} }
public List<FeedCount> findDuplicates(DuplicateMode mode, int offset, public List<FeedCount> findDuplicates(DuplicateMode mode, int offset, int limit, long minCount) {
int limit, long minCount) {
CriteriaQuery<String> query = builder.createQuery(String.class); CriteriaQuery<String> query = builder.createQuery(String.class);
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());
@@ -170,14 +144,12 @@ public class FeedDAO extends GenericDAO<Feed> {
List<FeedCount> result = Lists.newArrayList(); List<FeedCount> result = Lists.newArrayList();
for (String pathValue : pathValues) { for (String pathValue : pathValues) {
FeedCount fc = new FeedCount(); FeedCount fc = new FeedCount(pathValue);
fc.value = pathValue;
fc.feeds = Lists.newArrayList();
for (Feed feed : findByField(mode.getPath(), pathValue)) { for (Feed feed : findByField(mode.getPath(), pathValue)) {
Feed f = new Feed(); Feed f = new Feed();
f.setId(feed.getId()); f.setId(feed.getId());
f.setUrl(feed.getUrl()); f.setUrl(feed.getUrl());
fc.feeds.add(f); fc.getFeeds().add(f);
} }
result.add(fc); result.add(fc);
} }

View File

@@ -0,0 +1,50 @@
package com.commafeed.backend.dao;
import java.util.List;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryContent_;
import com.commafeed.backend.model.FeedEntry_;
import com.google.common.collect.Iterables;
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
public Long findExisting(String contentHash, String titleHash) {
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<FeedEntryContent> root = query.from(getType());
query.select(root.get(FeedEntryContent_.id));
Predicate p1 = builder.equal(root.get(FeedEntryContent_.contentHash), contentHash);
Predicate p2 = builder.equal(root.get(FeedEntryContent_.titleHash), titleHash);
query.where(p1, p2);
TypedQuery<Long> q = em.createQuery(query);
limit(q, 0, 1);
return Iterables.getFirst(q.getResultList(), null);
}
public int deleteWithoutEntries(int max) {
CriteriaQuery<FeedEntryContent> query = builder.createQuery(getType());
Root<FeedEntryContent> root = query.from(getType());
Join<FeedEntryContent, FeedEntry> join = root.join(FeedEntryContent_.entries, JoinType.LEFT);
query.where(builder.isNull(join.get(FeedEntry_.id)));
TypedQuery<FeedEntryContent> q = em.createQuery(query);
q.setMaxResults(max);
List<FeedEntryContent> list = q.getResultList();
int deleted = list.size();
return deleted;
}
}

View File

@@ -4,77 +4,36 @@ import java.util.Date;
import java.util.List; import java.util.List;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntry_; import com.commafeed.backend.model.FeedEntry_;
import com.commafeed.backend.model.FeedFeedEntry; import com.commafeed.backend.model.Feed_;
import com.commafeed.backend.model.FeedFeedEntry_;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@Stateless @Stateless
public class FeedEntryDAO extends GenericDAO<FeedEntry> { public class FeedEntryDAO extends GenericDAO<FeedEntry> {
@Inject public Long findExisting(String guid, Long feedId) {
ApplicationSettingsService applicationSettingsService;
protected static final Logger log = LoggerFactory CriteriaQuery<Long> query = builder.createQuery(Long.class);
.getLogger(FeedEntryDAO.class);
public static class EntryWithFeed {
public FeedEntry entry;
public FeedFeedEntry ffe;
public EntryWithFeed(FeedEntry entry, FeedFeedEntry ffe) {
this.entry = entry;
this.ffe = ffe;
}
}
public EntryWithFeed findExisting(String guid, String url, Long feedId) {
TypedQuery<EntryWithFeed> q = em.createNamedQuery(
"EntryStatus.existing", EntryWithFeed.class);
q.setParameter("guidHash", DigestUtils.sha1Hex(guid));
q.setParameter("url", url);
q.setParameter("feedId", feedId);
EntryWithFeed result = null;
List<EntryWithFeed> list = q.getResultList();
for (EntryWithFeed ewf : list) {
if (ewf.entry != null && ewf.ffe != null) {
result = ewf;
break;
}
}
if (result == null) {
result = Iterables.getFirst(list, null);
}
return result;
}
public List<FeedEntry> findByFeed(Feed feed, int offset, int limit) {
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
Root<FeedEntry> root = query.from(getType()); Root<FeedEntry> root = query.from(getType());
SetJoin<FeedEntry, FeedFeedEntry> feedsJoin = root.join(FeedEntry_.feedRelationships); query.select(root.get(FeedEntry_.id));
query.where(builder.equal(feedsJoin.get(FeedFeedEntry_.feed), feed)); Predicate p1 = builder.equal(root.get(FeedEntry_.guidHash), DigestUtils.sha1Hex(guid));
query.orderBy(builder.desc(feedsJoin.get(FeedFeedEntry_.entryUpdated))); Predicate p2 = builder.equal(root.get(FeedEntry_.feed).get(Feed_.id), feedId);
TypedQuery<FeedEntry> q = em.createQuery(query);
limit(q, offset, limit); query.where(p1, p2);
setTimeout(q, applicationSettingsService.get().getQueryTimeout());
return q.getResultList(); TypedQuery<Long> q = em.createQuery(query);
limit(q, 0, 1);
List<Long> list = q.getResultList();
return Iterables.getFirst(list, null);
} }
public int delete(Date olderThan, int max) { public int delete(Date olderThan, int max) {
@@ -90,20 +49,4 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
delete(list); delete(list);
return deleted; return deleted;
} }
public int deleteWithoutFeeds(int max) {
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
Root<FeedEntry> root = query.from(getType());
SetJoin<FeedEntry, FeedFeedEntry> join = root.join(FeedEntry_.feedRelationships,
JoinType.LEFT);
query.where(builder.isNull(join.get(FeedFeedEntry_.feed)));
TypedQuery<FeedEntry> q = em.createQuery(query);
q.setMaxResults(max);
List<FeedEntry> list = q.getResultList();
int deleted = list.size();
delete(list);
return deleted;
}
} }

View File

@@ -10,53 +10,42 @@ import javax.inject.Inject;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Path; import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger; import org.hibernate.Criteria;
import org.slf4j.LoggerFactory; import org.hibernate.criterion.Disjunction;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.sql.JoinType;
import org.hibernate.transform.Transformers;
import com.commafeed.backend.FixedSizeSortedSet; import com.commafeed.backend.FixedSizeSortedSet;
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.FeedEntryContent_;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedEntryStatus_; import com.commafeed.backend.model.FeedEntryStatus_;
import com.commafeed.backend.model.FeedEntry_; import com.commafeed.backend.model.FeedEntry_;
import com.commafeed.backend.model.FeedFeedEntry;
import com.commafeed.backend.model.FeedFeedEntry_;
import com.commafeed.backend.model.FeedSubscription; 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.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
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.Lists;
import com.google.common.collect.Maps;
@Stateless @Stateless
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> { public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
protected static Logger log = LoggerFactory private static final String ALIAS_STATUS = "status";
.getLogger(FeedEntryStatusDAO.class); private static final String ALIAS_ENTRY = "entry";
private static final Comparator<FeedEntry> ENTRY_COMPARATOR_DESC = new Comparator<FeedEntry>() {
@Override
public int compare(FeedEntry o1, FeedEntry o2) {
return ObjectUtils.compare(o2.getUpdated(), o1.getUpdated());
};
};
private static final Comparator<FeedEntry> ENTRY_COMPARATOR_ASC = new Comparator<FeedEntry>() {
@Override
public int compare(FeedEntry o1, FeedEntry o2) {
return ObjectUtils.compare(o1.getUpdated(), o2.getUpdated());
};
};
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_DESC = new Comparator<FeedEntryStatus>() { private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_DESC = new Comparator<FeedEntryStatus>() {
@Override @Override
@@ -81,22 +70,30 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
Root<FeedEntryStatus> root = query.from(getType()); Root<FeedEntryStatus> root = query.from(getType());
Predicate p1 = builder.equal(root.get(FeedEntryStatus_.entry), entry); Predicate p1 = builder.equal(root.get(FeedEntryStatus_.entry), entry);
Predicate p2 = builder.equal(root.get(FeedEntryStatus_.subscription), Predicate p2 = builder.equal(root.get(FeedEntryStatus_.subscription), sub);
sub);
query.where(p1, p2); query.where(p1, p2);
List<FeedEntryStatus> statuses = em.createQuery(query).getResultList(); List<FeedEntryStatus> statuses = em.createQuery(query).getResultList();
FeedEntryStatus status = Iterables.getFirst(statuses, null); FeedEntryStatus status = Iterables.getFirst(statuses, null);
return handleStatus(status, sub, entry);
}
private FeedEntryStatus handleStatus(FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) { if (status == null) {
Date unreadThreshold = applicationSettingsService.getUnreadThreshold();
boolean read = unreadThreshold == null ? false : entry.getUpdated().before(unreadThreshold);
status = new FeedEntryStatus(sub.getUser(), sub, entry); status = new FeedEntryStatus(sub.getUser(), sub, entry);
status.setRead(true); status.setRead(read);
status.setMarkable(!read);
} else {
status.setMarkable(true);
} }
return status; return status;
} }
public List<FeedEntryStatus> findStarred(User user, Date newerThan, public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) {
int offset, int limit, ReadingOrder order, boolean includeContent) {
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType()); CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
Root<FeedEntryStatus> root = query.from(getType()); Root<FeedEntryStatus> root = query.from(getType());
@@ -108,8 +105,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
query.where(predicates.toArray(new Predicate[0])); query.where(predicates.toArray(new Predicate[0]));
if (newerThan != null) { if (newerThan != null) {
predicates.add(builder.greaterThanOrEqualTo( predicates.add(builder.greaterThanOrEqualTo(root.get(FeedEntryStatus_.entryInserted), newerThan));
root.get(FeedEntryStatus_.entryInserted), newerThan));
} }
orderStatusesBy(query, root, order); orderStatusesBy(query, root, order);
@@ -117,200 +113,156 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
TypedQuery<FeedEntryStatus> q = em.createQuery(query); TypedQuery<FeedEntryStatus> q = em.createQuery(query);
limit(q, offset, limit); limit(q, offset, limit);
setTimeout(q); setTimeout(q);
return lazyLoadContent(includeContent, q.getResultList()); List<FeedEntryStatus> statuses = q.getResultList();
for (FeedEntryStatus status : statuses) {
status = handleStatus(status, status.getSubscription(), status.getEntry());
}
return lazyLoadContent(includeContent, statuses);
} }
public List<FeedEntryStatus> findBySubscriptions( private Criteria buildSearchCriteria(FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset, int limit,
List<FeedSubscription> subscriptions, String keywords, ReadingOrder order, Date last) {
Date newerThan, int offset, int limit, ReadingOrder order, Criteria criteria = getSession().createCriteria(FeedEntry.class, ALIAS_ENTRY);
boolean includeContent) {
int capacity = offset + limit; criteria.add(Restrictions.eq(FeedEntry_.feed.getName(), sub.getFeed()));
Comparator<FeedEntry> comparator = order == ReadingOrder.desc ? ENTRY_COMPARATOR_DESC
: ENTRY_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntry> set = new FixedSizeSortedSet<FeedEntry>(
capacity < 0 ? Integer.MAX_VALUE : capacity, comparator);
for (FeedSubscription sub : subscriptions) {
CriteriaQuery<FeedEntry> query = builder
.createQuery(FeedEntry.class);
Root<FeedEntry> root = query.from(FeedEntry.class);
Join<FeedEntry, FeedFeedEntry> ffeJoin = root
.join(FeedEntry_.feedRelationships);
List<Predicate> predicates = Lists.newArrayList(); if (keywords != null) {
predicates.add(builder.equal(ffeJoin.get(FeedFeedEntry_.feed), Criteria contentJoin = criteria.createCriteria(FeedEntry_.content.getName(), "content", JoinType.INNER_JOIN);
sub.getFeed()));
if (newerThan != null) { for (String keyword : StringUtils.split(keywords)) {
predicates.add(builder.greaterThanOrEqualTo( Disjunction or = Restrictions.disjunction();
root.get(FeedEntry_.inserted), newerThan)); or.add(Restrictions.ilike(FeedEntryContent_.content.getName(), keyword, MatchMode.ANYWHERE));
or.add(Restrictions.ilike(FeedEntryContent_.title.getName(), keyword, MatchMode.ANYWHERE));
contentJoin.add(or);
} }
if (keywords != null) {
Join<FeedEntry, FeedEntryContent> contentJoin = root
.join(FeedEntry_.content);
String joinedKeywords = StringUtils.join(keywords.toLowerCase()
.split(" "), "%");
joinedKeywords = "%" + joinedKeywords + "%";
Predicate content = builder.like(builder.lower(contentJoin
.get(FeedEntryContent_.content)), joinedKeywords);
Predicate title = builder
.like(builder.lower(contentJoin
.get(FeedEntryContent_.title)), joinedKeywords);
predicates.add(builder.or(content, title));
}
if (order != null && !set.isEmpty() && set.isFull()) {
Predicate filter = null;
FeedEntry last = set.last();
if (order == ReadingOrder.desc) {
filter = builder.greaterThan(
ffeJoin.get(FeedFeedEntry_.entryUpdated),
last.getUpdated());
} else {
filter = builder.lessThan(
ffeJoin.get(FeedFeedEntry_.entryUpdated),
last.getUpdated());
}
predicates.add(filter);
}
query.where(predicates.toArray(new Predicate[0]));
orderEntriesBy(query, ffeJoin, order);
TypedQuery<FeedEntry> q = em.createQuery(query);
limit(q, 0, capacity);
setTimeout(q);
List<FeedEntry> list = q.getResultList();
for (FeedEntry entry : list) {
entry.setSubscription(sub);
}
set.addAll(list);
} }
Criteria statusJoin = criteria.createCriteria(FeedEntry_.statuses.getName(), ALIAS_STATUS, JoinType.LEFT_OUTER_JOIN,
Restrictions.eq(FeedEntryStatus_.subscription.getName(), sub));
List<FeedEntry> entries = set.asList(); if (unreadOnly) {
int size = entries.size();
if (size < offset) {
return Lists.newArrayList();
}
entries = entries.subList(Math.max(offset, 0), size); Disjunction or = Restrictions.disjunction();
or.add(Restrictions.isNull(FeedEntryStatus_.read.getName()));
or.add(Restrictions.eq(FeedEntryStatus_.read.getName(), false));
statusJoin.add(or);
List<FeedEntryStatus> results = Lists.newArrayList(); Date unreadThreshold = applicationSettingsService.getUnreadThreshold();
for (FeedEntry entry : entries) { if (unreadThreshold != null) {
FeedSubscription subscription = entry.getSubscription(); criteria.add(Restrictions.ge(FeedEntry_.updated.getName(), unreadThreshold));
results.add(getStatus(subscription, entry));
}
return lazyLoadContent(includeContent, results);
}
public List<FeedEntryStatus> findUnreadBySubscriptions(
List<FeedSubscription> subscriptions, Date newerThan, int offset,
int limit, ReadingOrder order, boolean includeContent) {
int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC
: STATUS_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(
capacity < 0 ? Integer.MAX_VALUE : capacity, comparator);
for (FeedSubscription sub : subscriptions) {
CriteriaQuery<FeedEntryStatus> query = builder
.createQuery(getType());
Root<FeedEntryStatus> root = query.from(getType());
List<Predicate> predicates = Lists.newArrayList();
predicates.add(builder.equal(
root.get(FeedEntryStatus_.subscription), sub));
predicates.add(builder.isFalse(root.get(FeedEntryStatus_.read)));
if (newerThan != null) {
predicates.add(builder.greaterThanOrEqualTo(
root.get(FeedEntryStatus_.entryInserted), newerThan));
} }
if (order != null && !set.isEmpty() && set.isFull()) {
Predicate filter = null;
FeedEntryStatus last = set.last();
if (order == ReadingOrder.desc) {
filter = builder.greaterThan(
root.get(FeedEntryStatus_.entryUpdated),
last.getEntryUpdated());
} else {
filter = builder.lessThan(
root.get(FeedEntryStatus_.entryUpdated),
last.getEntryUpdated());
}
predicates.add(filter);
}
query.where(predicates.toArray(new Predicate[0]));
orderStatusesBy(query, root, order);
TypedQuery<FeedEntryStatus> q = em.createQuery(query);
limit(q, -1, limit);
setTimeout(q);
List<FeedEntryStatus> list = q.getResultList();
set.addAll(list);
} }
List<FeedEntryStatus> entries = set.asList();
int size = entries.size();
if (size < offset) {
return Lists.newArrayList();
}
entries = entries.subList(Math.max(offset, 0), size);
return lazyLoadContent(includeContent, entries);
}
public List<FeedEntryStatus> findAllUnread(User user, Date newerThan,
int offset, int limit, ReadingOrder order, boolean includeContent) {
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
Root<FeedEntryStatus> root = query.from(getType());
List<Predicate> predicates = Lists.newArrayList();
predicates.add(builder.equal(root.get(FeedEntryStatus_.user), user));
predicates.add(builder.isFalse(root.get(FeedEntryStatus_.read)));
if (newerThan != null) { if (newerThan != null) {
predicates.add(builder.greaterThanOrEqualTo( criteria.add(Restrictions.ge(FeedEntry_.inserted.getName(), newerThan));
root.get(FeedEntryStatus_.entryInserted), newerThan));
} }
query.where(predicates.toArray(new Predicate[0])); if (last != null) {
orderStatusesBy(query, root, order); if (order == ReadingOrder.desc) {
criteria.add(Restrictions.gt(FeedEntry_.updated.getName(), last));
TypedQuery<FeedEntryStatus> q = em.createQuery(query); } else {
limit(q, offset, limit); criteria.add(Restrictions.lt(FeedEntry_.updated.getName(), last));
setTimeout(q); }
return lazyLoadContent(includeContent, q.getResultList());
}
/**
* Map between subscriptionId and unread count
*/
@SuppressWarnings("rawtypes")
public Map<Long, Long> getUnreadCount(User user) {
Map<Long, Long> map = Maps.newHashMap();
Query query = em.createNamedQuery("EntryStatus.unreadCounts");
query.setParameter("user", user);
setTimeout(query);
List resultList = query.getResultList();
for (Object o : resultList) {
Object[] array = (Object[]) o;
map.put((Long) array[0], (Long) array[1]);
} }
return map;
if (order != null) {
Order o = null;
if (order == ReadingOrder.asc) {
o = Order.asc(FeedEntry_.updated.getName());
} else {
o = Order.desc(FeedEntry_.updated.getName());
}
criteria.addOrder(o);
}
if (offset > -1) {
criteria.setFirstResult(offset);
}
if (limit > -1) {
criteria.setMaxResults(limit);
}
int timeout = applicationSettingsService.get().getQueryTimeout();
if (timeout > 0) {
// hibernate timeout is in seconds, jpa timeout is in millis
criteria.setTimeout(timeout / 1000);
}
return criteria;
} }
private List<FeedEntryStatus> lazyLoadContent(boolean includeContent, @SuppressWarnings("unchecked")
List<FeedEntryStatus> results) { public List<FeedEntryStatus> findBySubscriptions(List<FeedSubscription> subs, boolean unreadOnly, String keywords, Date newerThan,
int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds) {
int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator);
for (FeedSubscription sub : subs) {
Date last = (order != null && set.isFull()) ? set.last().getEntryUpdated() : null;
Criteria criteria = buildSearchCriteria(sub, unreadOnly, keywords, newerThan, -1, capacity, order, last);
ProjectionList projection = Projections.projectionList();
projection.add(Projections.property("id"), "id");
projection.add(Projections.property("updated"), "updated");
projection.add(Projections.property(ALIAS_STATUS + ".id"), "status_id");
criteria.setProjection(projection);
criteria.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
List<Map<String, Object>> list = criteria.list();
for (Map<String, Object> map : list) {
Long id = (Long) map.get("id");
Date updated = (Date) map.get("updated");
Long statusId = (Long) map.get("status_id");
FeedEntry entry = new FeedEntry();
entry.setId(id);
entry.setUpdated(updated);
FeedEntryStatus status = new FeedEntryStatus();
status.setId(statusId);
status.setEntryUpdated(updated);
status.setEntry(entry);
status.setSubscription(sub);
set.add(status);
}
}
List<FeedEntryStatus> placeholders = set.asList();
int size = placeholders.size();
if (size < offset) {
return Lists.newArrayList();
}
placeholders = placeholders.subList(Math.max(offset, 0), size);
List<FeedEntryStatus> statuses = null;
if (onlyIds) {
statuses = placeholders;
} else {
statuses = Lists.newArrayList();
for (FeedEntryStatus placeholder : placeholders) {
Long statusId = placeholder.getId();
FeedEntry entry = em.find(FeedEntry.class, placeholder.getEntry().getId());
statuses.add(handleStatus(statusId == null ? null : findById(statusId), placeholder.getSubscription(), entry));
}
statuses = lazyLoadContent(includeContent, statuses);
}
return statuses;
}
@SuppressWarnings("unchecked")
public UnreadCount getUnreadCount(FeedSubscription subscription) {
UnreadCount uc = null;
Criteria criteria = buildSearchCriteria(subscription, true, null, null, -1, -1, null, null);
ProjectionList projection = Projections.projectionList();
projection.add(Projections.rowCount(), "count");
projection.add(Projections.max(FeedEntry_.updated.getName()), "updated");
criteria.setProjection(projection);
criteria.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
List<Map<String, Object>> list = criteria.list();
for (Map<String, Object> row : list) {
Long count = (Long) row.get("count");
Date updated = (Date) row.get("updated");
uc = new UnreadCount(subscription.getId(), count, updated);
}
return uc;
}
private List<FeedEntryStatus> lazyLoadContent(boolean includeContent, List<FeedEntryStatus> results) {
if (includeContent) { if (includeContent) {
for (FeedEntryStatus status : results) { for (FeedEntryStatus status : results) {
Models.initialize(status.getSubscription().getFeed()); Models.initialize(status.getSubscription().getFeed());
@@ -320,18 +272,11 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return results; return results;
} }
private void orderEntriesBy(CriteriaQuery<?> query, private void orderStatusesBy(CriteriaQuery<?> query, Path<FeedEntryStatus> statusJoin, ReadingOrder order) {
Path<FeedFeedEntry> ffeJoin, ReadingOrder order) {
orderBy(query, ffeJoin.get(FeedFeedEntry_.entryUpdated), order);
}
private void orderStatusesBy(CriteriaQuery<?> query,
Path<FeedEntryStatus> statusJoin, ReadingOrder order) {
orderBy(query, statusJoin.get(FeedEntryStatus_.entryUpdated), order); orderBy(query, statusJoin.get(FeedEntryStatus_.entryUpdated), order);
} }
private void orderBy(CriteriaQuery<?> query, Path<Date> date, private void orderBy(CriteriaQuery<?> query, Path<Date> date, ReadingOrder order) {
ReadingOrder order) {
if (order != null) { if (order != null) {
if (order == ReadingOrder.asc) { if (order == ReadingOrder.asc) {
query.orderBy(builder.asc(date)); query.orderBy(builder.asc(date));
@@ -345,43 +290,17 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
setTimeout(query, applicationSettingsService.get().getQueryTimeout()); setTimeout(query, applicationSettingsService.get().getQueryTimeout());
} }
public void markAllEntries(User user, Date olderThan) { public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) {
List<FeedEntryStatus> statuses = findAllUnread(user, null, -1, -1, CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
null, false); Root<FeedEntryStatus> root = query.from(getType());
markList(statuses, olderThan);
}
public void markSubscriptionEntries(List<FeedSubscription> subscriptions, Predicate p1 = builder.lessThan(root.get(FeedEntryStatus_.entryInserted), olderThan);
Date olderThan) { Predicate p2 = builder.isFalse(root.get(FeedEntryStatus_.starred));
List<FeedEntryStatus> statuses = findUnreadBySubscriptions(
subscriptions, null, -1, -1, null, false);
markList(statuses, olderThan);
}
public void markStarredEntries(User user, Date olderThan) { query.where(p1, p2);
List<FeedEntryStatus> statuses = findStarred(user, null, -1, -1, null, TypedQuery<FeedEntryStatus> q = em.createQuery(query);
false); q.setMaxResults(limit);
markList(statuses, olderThan); return q.getResultList();
}
private void markList(List<FeedEntryStatus> statuses, Date olderThan) {
List<FeedEntryStatus> list = Lists.newArrayList();
for (FeedEntryStatus status : statuses) {
if (!status.isRead()) {
Date inserted = status.getEntry().getInserted();
if (olderThan == null || inserted == null
|| olderThan.after(inserted)) {
if (status.isStarred()) {
status.setRead(true);
list.add(status);
} else {
delete(status);
}
}
}
}
saveOrUpdate(list);
} }
} }

View File

@@ -28,8 +28,7 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
CriteriaQuery<FeedSubscription> query = builder.createQuery(getType()); CriteriaQuery<FeedSubscription> query = builder.createQuery(getType());
Root<FeedSubscription> root = query.from(getType()); Root<FeedSubscription> root = query.from(getType());
Predicate p1 = builder.equal( Predicate p1 = builder.equal(root.get(FeedSubscription_.user).get(User_.id), user.getId());
root.get(FeedSubscription_.user).get(User_.id), user.getId());
Predicate p2 = builder.equal(root.get(FeedSubscription_.id), id); Predicate p2 = builder.equal(root.get(FeedSubscription_.id), id);
root.fetch(FeedSubscription_.feed, JoinType.LEFT); root.fetch(FeedSubscription_.feed, JoinType.LEFT);
@@ -37,8 +36,7 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
query.where(p1, p2); query.where(p1, p2);
FeedSubscription sub = Iterables.getFirst(cache(em.createQuery(query)) FeedSubscription sub = Iterables.getFirst(cache(em.createQuery(query)).getResultList(), null);
.getResultList(), null);
initRelations(sub); initRelations(sub);
return sub; return sub;
} }
@@ -47,11 +45,8 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
CriteriaQuery<FeedSubscription> query = builder.createQuery(getType()); CriteriaQuery<FeedSubscription> query = builder.createQuery(getType());
Root<FeedSubscription> root = query.from(getType()); Root<FeedSubscription> root = query.from(getType());
query.where(builder.equal(root.get(FeedSubscription_.feed) query.where(builder.equal(root.get(FeedSubscription_.feed), feed));
.get(Feed_.id), feed.getId())); List<FeedSubscription> list = cache(em.createQuery(query)).getResultList();
List<FeedSubscription> list = cache(em.createQuery(query))
.getResultList();
initRelations(list);
return list; return list;
} }
@@ -60,18 +55,15 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
CriteriaQuery<FeedSubscription> query = builder.createQuery(getType()); CriteriaQuery<FeedSubscription> query = builder.createQuery(getType());
Root<FeedSubscription> root = query.from(getType()); Root<FeedSubscription> root = query.from(getType());
Predicate p1 = builder.equal( Predicate p1 = builder.equal(root.get(FeedSubscription_.user).get(User_.id), user.getId());
root.get(FeedSubscription_.user).get(User_.id), user.getId()); Predicate p2 = builder.equal(root.get(FeedSubscription_.feed).get(Feed_.id), feed.getId());
Predicate p2 = builder.equal(
root.get(FeedSubscription_.feed).get(Feed_.id), feed.getId());
root.fetch(FeedSubscription_.feed, JoinType.LEFT); root.fetch(FeedSubscription_.feed, JoinType.LEFT);
root.fetch(FeedSubscription_.category, JoinType.LEFT); root.fetch(FeedSubscription_.category, JoinType.LEFT);
query.where(p1, p2); query.where(p1, p2);
FeedSubscription sub = Iterables.getFirst(cache(em.createQuery(query)) FeedSubscription sub = Iterables.getFirst(cache(em.createQuery(query)).getResultList(), null);
.getResultList(), null);
initRelations(sub); initRelations(sub);
return sub; return sub;
} }
@@ -84,57 +76,46 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
root.fetch(FeedSubscription_.feed, JoinType.LEFT); root.fetch(FeedSubscription_.feed, JoinType.LEFT);
root.fetch(FeedSubscription_.category, JoinType.LEFT); root.fetch(FeedSubscription_.category, JoinType.LEFT);
query.where(builder.equal(root.get(FeedSubscription_.user) query.where(builder.equal(root.get(FeedSubscription_.user).get(User_.id), user.getId()));
.get(User_.id), user.getId()));
List<FeedSubscription> list = cache(em.createQuery(query)) List<FeedSubscription> list = cache(em.createQuery(query)).getResultList();
.getResultList();
initRelations(list); initRelations(list);
return list; return list;
} }
public List<FeedSubscription> findByCategory(User user, public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
FeedCategory category) {
CriteriaQuery<FeedSubscription> query = builder.createQuery(getType()); CriteriaQuery<FeedSubscription> query = builder.createQuery(getType());
Root<FeedSubscription> root = query.from(getType()); Root<FeedSubscription> root = query.from(getType());
Predicate p1 = builder.equal( Predicate p1 = builder.equal(root.get(FeedSubscription_.user).get(User_.id), user.getId());
root.get(FeedSubscription_.user).get(User_.id), user.getId());
Predicate p2 = null; Predicate p2 = null;
if (category == null) { if (category == null) {
p2 = builder.isNull( p2 = builder.isNull(root.get(FeedSubscription_.category));
root.get(FeedSubscription_.category));
} else { } else {
p2 = builder.equal( p2 = builder.equal(root.get(FeedSubscription_.category).get(FeedCategory_.id), category.getId());
root.get(FeedSubscription_.category).get(FeedCategory_.id),
category.getId());
} }
query.where(p1, p2); query.where(p1, p2);
List<FeedSubscription> list = cache(em.createQuery(query)) List<FeedSubscription> list = cache(em.createQuery(query)).getResultList();
.getResultList();
initRelations(list); initRelations(list);
return list; return list;
} }
public List<FeedSubscription> findByCategories(User user,
List<FeedCategory> categories) {
List<Long> categoryIds = Lists.transform(categories, public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {
new Function<FeedCategory, Long>() {
@Override List<Long> categoryIds = Lists.transform(categories, new Function<FeedCategory, Long>() {
public Long apply(FeedCategory input) { @Override
return input.getId(); public Long apply(FeedCategory input) {
} return input.getId();
}); }
});
List<FeedSubscription> subscriptions = Lists.newArrayList(); List<FeedSubscription> subscriptions = Lists.newArrayList();
for (FeedSubscription sub : findAll(user)) { for (FeedSubscription sub : findAll(user)) {
if (sub.getCategory() != null if (sub.getCategory() != null && categoryIds.contains(sub.getCategory().getId())) {
&& categoryIds.contains(sub.getCategory().getId())) {
subscriptions.add(sub); subscriptions.add(sub);
} }
} }

View File

@@ -35,8 +35,13 @@ public abstract class GenericDAO<T extends AbstractModel> {
builder = em.getCriteriaBuilder(); builder = em.getCriteriaBuilder();
} }
public void saveOrUpdate(Collection<? extends AbstractModel> models) { public Session getSession() {
Session session = em.unwrap(Session.class); Session session = em.unwrap(Session.class);
return session;
}
public void saveOrUpdate(Collection<? extends AbstractModel> models) {
Session session = getSession();
int i = 1; int i = 1;
for (AbstractModel model : models) { for (AbstractModel model : models) {
session.saveOrUpdate(model); session.saveOrUpdate(model);
@@ -91,8 +96,7 @@ public abstract class GenericDAO<T extends AbstractModel> {
return q.getResultList(); return q.getResultList();
} }
public List<T> findAll(int startIndex, int count, String orderBy, public List<T> findAll(int startIndex, int count, String orderBy, boolean asc) {
boolean asc) {
CriteriaQuery<T> query = builder.createQuery(getType()); CriteriaQuery<T> query = builder.createQuery(getType());
Root<T> root = query.from(getType()); Root<T> root = query.from(getType());
@@ -121,8 +125,7 @@ public abstract class GenericDAO<T extends AbstractModel> {
return findByField(field, value, false); return findByField(field, value, false);
} }
protected <V> List<T> findByField(Attribute<T, V> field, V value, protected <V> List<T> findByField(Attribute<T, V> field, V value, boolean cache) {
boolean cache) {
CriteriaQuery<T> query = builder.createQuery(getType()); CriteriaQuery<T> query = builder.createQuery(getType());
Root<T> root = query.from(getType()); Root<T> root = query.from(getType());
@@ -152,7 +155,7 @@ public abstract class GenericDAO<T extends AbstractModel> {
query.unwrap(Query.class).setCacheable(true); query.unwrap(Query.class).setCacheable(true);
return query; return query;
} }
protected void setTimeout(javax.persistence.Query query, int queryTimeout) { protected void setTimeout(javax.persistence.Query query, int queryTimeout) {
if (queryTimeout > 0) { if (queryTimeout > 0) {
query.setHint("javax.persistence.query.timeout", queryTimeout); query.setHint("javax.persistence.query.timeout", queryTimeout);

View File

@@ -18,11 +18,10 @@ public class UserDAO extends GenericDAO<User> {
CriteriaQuery<User> query = builder.createQuery(getType()); CriteriaQuery<User> query = builder.createQuery(getType());
Root<User> root = query.from(getType()); Root<User> root = query.from(getType());
query.where(builder.equal(builder.lower(root.get(User_.name)), query.where(builder.equal(builder.lower(root.get(User_.name)), name.toLowerCase()));
name.toLowerCase()));
TypedQuery<User> q = em.createQuery(query); TypedQuery<User> q = em.createQuery(query);
cache(q); cache(q);
User user = null; User user = null;
try { try {
user = q.getSingleResult(); user = q.getSingleResult();
@@ -38,7 +37,7 @@ public class UserDAO extends GenericDAO<User> {
query.where(builder.equal(root.get(User_.apiKey), key)); query.where(builder.equal(root.get(User_.apiKey), key));
TypedQuery<User> q = em.createQuery(query); TypedQuery<User> q = em.createQuery(query);
cache(q); cache(q);
User user = null; User user = null;
try { try {
user = q.getSingleResult(); user = q.getSingleResult();

View File

@@ -33,8 +33,7 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
CriteriaQuery<UserRole> query = builder.createQuery(getType()); CriteriaQuery<UserRole> query = builder.createQuery(getType());
Root<UserRole> root = query.from(getType()); Root<UserRole> root = query.from(getType());
query.where(builder.equal(root.get(UserRole_.user).get(User_.id), query.where(builder.equal(root.get(UserRole_.user).get(User_.id), user.getId()));
user.getId()));
return cache(em.createQuery(query)).getResultList(); return cache(em.createQuery(query)).getResultList();
} }

View File

@@ -18,8 +18,7 @@ public class UserSettingsDAO extends GenericDAO<UserSettings> {
CriteriaQuery<UserSettings> query = builder.createQuery(getType()); CriteriaQuery<UserSettings> query = builder.createQuery(getType());
Root<UserSettings> root = query.from(getType()); Root<UserSettings> root = query.from(getType());
query.where(builder.equal(root.get(UserSettings_.user).get(User_.id), query.where(builder.equal(root.get(UserSettings_.user).get(User_.id), user.getId()));
user.getId()));
UserSettings settings = null; UserSettings settings = null;
try { try {

View File

@@ -5,12 +5,12 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
@@ -19,20 +19,16 @@ import com.commafeed.backend.HttpGetter.HttpResult;
* Inspired/Ported from https://github.com/potatolondon/getfavicon * Inspired/Ported from https://github.com/potatolondon/getfavicon
* *
*/ */
@Slf4j
public class FaviconFetcher { public class FaviconFetcher {
private static Logger log = LoggerFactory.getLogger(FeedFetcher.class);
private static long MIN_ICON_LENGTH = 100; private static long MIN_ICON_LENGTH = 100;
private static long MAX_ICON_LENGTH = 20000; private static long MAX_ICON_LENGTH = 100000;
private static int TIMEOUT = 4000; private static int TIMEOUT = 4000;
protected static List<String> ICON_MIMETYPES = Arrays.asList( protected static List<String> ICON_MIMETYPES = Arrays.asList("image/x-icon", "image/vnd.microsoft.icon", "image/ico", "image/icon",
"image/x-icon", "image/vnd.microsoft.icon", "image/ico", "text/ico", "application/ico", "image/x-ms-bmp", "image/x-bmp", "image/gif", "image/png", "image/jpeg");
"image/icon", "text/ico", "application/ico", "image/x-ms-bmp", private static List<String> ICON_MIMETYPE_BLACKLIST = Arrays.asList("application/xml", "text/html");
"image/x-bmp", "image/gif", "image/png", "image/jpeg");
private static List<String> ICON_MIMETYPE_BLACKLIST = Arrays.asList(
"application/xml", "text/html");
@Inject @Inject
HttpGetter getter; HttpGetter getter;
@@ -84,7 +80,7 @@ public class FaviconFetcher {
return bytes; return bytes;
} }
boolean isValidIconResponse(byte[] content, String contentType) { private boolean isValidIconResponse(byte[] content, String contentType) {
if (content == null) { if (content == null) {
return false; return false;
} }
@@ -101,14 +97,12 @@ public class FaviconFetcher {
} }
if (length < MIN_ICON_LENGTH) { if (length < MIN_ICON_LENGTH) {
log.debug("Length {} below MIN_ICON_LENGTH {}", length, log.debug("Length {} below MIN_ICON_LENGTH {}", length, MIN_ICON_LENGTH);
MIN_ICON_LENGTH);
return false; return false;
} }
if (length > MAX_ICON_LENGTH) { if (length > MAX_ICON_LENGTH) {
log.debug("Length {} greater than MAX_ICON_LENGTH {}", length, log.debug("Length {} greater than MAX_ICON_LENGTH {}", length, MAX_ICON_LENGTH);
MAX_ICON_LENGTH);
return false; return false;
} }
@@ -126,8 +120,7 @@ public class FaviconFetcher {
return null; return null;
} }
Elements icons = doc Elements icons = doc.select("link[rel~=(?i)^(shortcut|icon|shortcut icon)$]");
.select("link[rel~=(?i)^(shortcut|icon|shortcut icon)$]");
if (icons.isEmpty()) { if (icons.isEmpty()) {
log.debug("No icon found in page {}", url); log.debug("No icon found in page {}", url);

View File

@@ -5,14 +5,14 @@ import java.util.Date;
import javax.inject.Inject; import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.StringUtils; import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ClientProtocolException;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
@@ -20,52 +20,54 @@ import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.sun.syndication.io.FeedException; import com.sun.syndication.io.FeedException;
@Slf4j
public class FeedFetcher { public class FeedFetcher {
private static Logger log = LoggerFactory.getLogger(FeedFetcher.class);
@Inject @Inject
FeedParser parser; FeedParser parser;
@Inject @Inject
HttpGetter getter; HttpGetter getter;
public FetchedFeed fetch(String feedUrl, boolean extractFeedUrlFromHtml, public FetchedFeed fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate,
String lastModified, String eTag, Date lastPublishedDate, String lastContentHash) throws FeedException, ClientProtocolException, IOException, NotModifiedException {
String lastContentHash) throws FeedException,
ClientProtocolException, IOException, NotModifiedException {
log.debug("Fetching feed {}", feedUrl); log.debug("Fetching feed {}", feedUrl);
FetchedFeed fetchedFeed = null; FetchedFeed fetchedFeed = null;
int timeout = 20000; int timeout = 20000;
HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout); HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout);
if (extractFeedUrlFromHtml) { byte[] content = result.getContent();
String extractedUrl = extractFeedUrl(
StringUtils.newStringUtf8(result.getContent()), feedUrl); try {
if (org.apache.commons.lang.StringUtils.isNotBlank(extractedUrl)) { fetchedFeed = parser.parse(feedUrl, content);
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout); } catch (FeedException e) {
feedUrl = extractedUrl; if (extractFeedUrlFromHtml) {
String extractedUrl = extractFeedUrl(StringUtils.newStringUtf8(result.getContent()), feedUrl);
if (org.apache.commons.lang.StringUtils.isNotBlank(extractedUrl)) {
feedUrl = extractedUrl;
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
content = result.getContent();
fetchedFeed = parser.parse(feedUrl, content);
}
} else {
throw e;
} }
} }
byte[] content = result.getContent();
if (content == null) { if (content == null) {
throw new IOException("Feed content is empty."); throw new IOException("Feed content is empty.");
} }
String hash = DigestUtils.sha1Hex(content); String hash = DigestUtils.sha1Hex(content);
if (lastContentHash != null && hash != null if (lastContentHash != null && hash != null && lastContentHash.equals(hash)) {
&& lastContentHash.equals(hash)) {
log.debug("content hash not modified: {}", feedUrl); log.debug("content hash not modified: {}", feedUrl);
throw new NotModifiedException("content hash not modified"); throw new NotModifiedException("content hash not modified");
} }
fetchedFeed = parser.parse(feedUrl, content); if (lastPublishedDate != null && fetchedFeed.getFeed().getLastPublishedDate() != null
&& lastPublishedDate.getTime() == fetchedFeed.getFeed().getLastPublishedDate().getTime()) {
if (lastPublishedDate != null
&& fetchedFeed.getFeed().getLastPublishedDate() != null
&& lastPublishedDate.getTime() == fetchedFeed.getFeed()
.getLastPublishedDate().getTime()) {
log.debug("publishedDate not modified: {}", feedUrl); log.debug("publishedDate not modified: {}", feedUrl);
throw new NotModifiedException("publishedDate not modified"); throw new NotModifiedException("publishedDate not modified");
} }
@@ -75,6 +77,7 @@ public class FeedFetcher {
feed.setEtagHeader(FeedUtils.truncate(result.geteTag(), 255)); feed.setEtagHeader(FeedUtils.truncate(result.geteTag(), 255));
feed.setLastContentHash(hash); feed.setLastContentHash(hash);
fetchedFeed.setFetchDuration(result.getDuration()); fetchedFeed.setFetchDuration(result.getDuration());
fetchedFeed.setUrlAfterRedirect(result.getUrlAfterRedirect());
return fetchedFeed; return fetchedFeed;
} }

View File

@@ -5,13 +5,12 @@ import java.text.DateFormat;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import org.apache.commons.codec.digest.DigestUtils; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.SystemUtils; import org.apache.commons.lang.SystemUtils;
import org.jdom.Element; import org.jdom.Element;
import org.jdom.Namespace; import org.jdom.Namespace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
@@ -29,17 +28,14 @@ import com.sun.syndication.feed.synd.SyndLinkImpl;
import com.sun.syndication.io.FeedException; import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput; import com.sun.syndication.io.SyndFeedInput;
@Slf4j
public class FeedParser { public class FeedParser {
private static Logger log = LoggerFactory.getLogger(FeedParser.class);
private static final String ATOM_10_URI = "http://www.w3.org/2005/Atom"; private static final String ATOM_10_URI = "http://www.w3.org/2005/Atom";
private static final Namespace ATOM_10_NS = Namespace private static final Namespace ATOM_10_NS = Namespace.getNamespace(ATOM_10_URI);
.getNamespace(ATOM_10_URI);
private static final Date START = new Date(86400000); private static final Date START = new Date(86400000);
private static final Date END = new Date( private static final Date END = new Date(1000l * Integer.MAX_VALUE - 86400000);
1000l * Integer.MAX_VALUE - 86400000);
private static final Function<SyndContent, String> CONTENT_TO_STRING = new Function<SyndContent, String>() { private static final Function<SyndContent, String> CONTENT_TO_STRING = new Function<SyndContent, String>() {
public String apply(SyndContent content) { public String apply(SyndContent content) {
@@ -52,15 +48,12 @@ public class FeedParser {
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();
feed.setLastUpdated(new Date());
try { try {
String encoding = FeedUtils.guessEncoding(xml); String encoding = FeedUtils.guessEncoding(xml);
String xmlString = FeedUtils.trimInvalidXmlCharacters(new String( String xmlString = FeedUtils.trimInvalidXmlCharacters(new String(xml, encoding));
xml, encoding));
if (xmlString == null) { if (xmlString == null) {
throw new FeedException("Input string is null for url " throw new FeedException("Input string is null for url " + feedUrl);
+ feedUrl);
} }
InputSource source = new InputSource(new StringReader(xmlString)); InputSource source = new InputSource(new StringReader(xmlString));
SyndFeed rss = new SyndFeedInput().build(source); SyndFeed rss = new SyndFeedInput().build(source);
@@ -89,21 +82,16 @@ public class FeedParser {
continue; continue;
} }
entry.setGuid(FeedUtils.truncate(guid, 2048)); entry.setGuid(FeedUtils.truncate(guid, 2048));
entry.setGuidHash(DigestUtils.sha1Hex(guid)); entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink()), 2048));
entry.setUrl(FeedUtils.truncate(
FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink()),
2048));
entry.setUpdated(validateDate(getEntryUpdateDate(item), true)); entry.setUpdated(validateDate(getEntryUpdateDate(item), true));
entry.setAuthor(item.getAuthor());
FeedEntryContent content = new FeedEntryContent(); FeedEntryContent content = new FeedEntryContent();
content.setContent(getContent(item)); content.setContent(getContent(item));
content.setTitle(getTitle(item)); content.setTitle(getTitle(item));
SyndEnclosure enclosure = (SyndEnclosure) Iterables.getFirst( content.setAuthor(StringUtils.trimToNull(item.getAuthor()));
item.getEnclosures(), null); SyndEnclosure enclosure = (SyndEnclosure) Iterables.getFirst(item.getEnclosures(), null);
if (enclosure != null) { if (enclosure != null) {
content.setEnclosureUrl(FeedUtils.truncate( content.setEnclosureUrl(FeedUtils.truncate(enclosure.getUrl(), 2048));
enclosure.getUrl(), 2048));
content.setEnclosureType(enclosure.getType()); content.setEnclosureType(enclosure.getType());
} }
entry.setContent(content); entry.setContent(content);
@@ -113,21 +101,17 @@ public class FeedParser {
Date lastEntryDate = null; Date lastEntryDate = null;
Date publishedDate = validateDate(rss.getPublishedDate(), false); Date publishedDate = validateDate(rss.getPublishedDate(), false);
if (!entries.isEmpty()) { if (!entries.isEmpty()) {
List<Long> sortedTimestamps = FeedUtils List<Long> sortedTimestamps = FeedUtils.getSortedTimestamps(entries);
.getSortedTimestamps(entries);
Long timestamp = sortedTimestamps.get(0); Long timestamp = sortedTimestamps.get(0);
lastEntryDate = new Date(timestamp); lastEntryDate = new Date(timestamp);
publishedDate = getFeedPublishedDate(publishedDate, entries); publishedDate = (publishedDate == null || publishedDate.before(lastEntryDate)) ? lastEntryDate : publishedDate;
} }
feed.setLastPublishedDate(validateDate(publishedDate, true)); feed.setLastPublishedDate(publishedDate);
feed.setAverageEntryInterval(FeedUtils feed.setAverageEntryInterval(FeedUtils.averageTimeBetweenEntries(entries));
.averageTimeBetweenEntries(entries));
feed.setLastEntryDate(lastEntryDate); feed.setLastEntryDate(lastEntryDate);
} catch (Exception e) { } catch (Exception e) {
throw new FeedException(String.format( throw new FeedException(String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
"Could not parse feed from %s : %s", feedUrl,
e.getMessage()), e);
} }
return fetchedFeed; return fetchedFeed;
} }
@@ -146,8 +130,7 @@ public class FeedParser {
for (Object object : elements) { for (Object object : elements) {
if (object instanceof Element) { if (object instanceof Element) {
Element element = (Element) object; Element element = (Element) object;
if ("link".equals(element.getName()) if ("link".equals(element.getName()) && ATOM_10_NS.equals(element.getNamespace())) {
&& ATOM_10_NS.equals(element.getNamespace())) {
SyndLink link = new SyndLinkImpl(); SyndLink link = new SyndLinkImpl();
link.setRel(element.getAttributeValue("rel")); link.setRel(element.getAttributeValue("rel"));
link.setHref(element.getAttributeValue("href")); link.setHref(element.getAttributeValue("href"));
@@ -158,17 +141,6 @@ public class FeedParser {
} }
} }
private Date getFeedPublishedDate(Date publishedDate,
List<FeedEntry> entries) {
for (FeedEntry entry : entries) {
if (publishedDate == null || entry.getUpdated().getTime() > publishedDate.getTime()) {
publishedDate = entry.getUpdated();
}
}
return publishedDate;
}
private Date getEntryUpdateDate(SyndEntry item) { private Date getEntryUpdateDate(SyndEntry item) {
Date date = item.getUpdatedDate(); Date date = item.getUpdatedDate();
if (date == null) { if (date == null) {
@@ -199,14 +171,11 @@ public class FeedParser {
private String getContent(SyndEntry item) { private String getContent(SyndEntry item) {
String content = null; String content = null;
if (item.getContents().isEmpty()) { if (item.getContents().isEmpty()) {
content = item.getDescription() == null ? null : item content = item.getDescription() == null ? null : item.getDescription().getValue();
.getDescription().getValue();
} else { } else {
content = StringUtils.join(Collections2.transform( content = StringUtils.join(Collections2.transform(item.getContents(), CONTENT_TO_STRING), SystemUtils.LINE_SEPARATOR);
item.getContents(), CONTENT_TO_STRING),
SystemUtils.LINE_SEPARATOR);
} }
return content; return StringUtils.trimToNull(content);
} }
private String getTitle(SyndEntry item) { private String getTitle(SyndEntry item) {
@@ -219,15 +188,14 @@ public class FeedParser {
title = "(no title)"; title = "(no title)";
} }
} }
return title; return StringUtils.trimToNull(title);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private String findHub(SyndFeed feed) { private String findHub(SyndFeed feed) {
for (SyndLink l : (List<SyndLink>) feed.getLinks()) { for (SyndLink l : (List<SyndLink>) feed.getLinks()) {
if ("hub".equalsIgnoreCase(l.getRel())) { if ("hub".equalsIgnoreCase(l.getRel())) {
log.debug("found hub {} for feed {}", l.getHref(), log.debug("found hub {} for feed {}", l.getHref(), feed.getLink());
feed.getLink());
return l.getHref(); return l.getHref();
} }
} }
@@ -238,8 +206,7 @@ public class FeedParser {
private String findSelf(SyndFeed feed) { private String findSelf(SyndFeed feed) {
for (SyndLink l : (List<SyndLink>) feed.getLinks()) { for (SyndLink l : (List<SyndLink>) feed.getLinks()) {
if ("self".equalsIgnoreCase(l.getRel())) { if ("self".equalsIgnoreCase(l.getRel())) {
log.debug("found self {} for feed {}", l.getHref(), log.debug("found self {} for feed {}", l.getHref(), feed.getLink());
feed.getLink());
return l.getHref(); return l.getHref();
} }
} }

View File

@@ -0,0 +1,42 @@
package com.commafeed.backend.feeds;
import java.util.List;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
public class FeedRefreshContext {
private Feed feed;
private List<FeedEntry> entries;
private boolean isUrgent;
public FeedRefreshContext(Feed feed, boolean isUrgent) {
this.feed = feed;
this.isUrgent = isUrgent;
}
public Feed getFeed() {
return feed;
}
public void setFeed(Feed feed) {
this.feed = feed;
}
public boolean isUrgent() {
return isUrgent;
}
public void setUrgent(boolean isUrgent) {
this.isUrgent = isUrgent;
}
public List<FeedEntry> getEntries() {
return entries;
}
public void setEntries(List<FeedEntry> entries) {
this.entries = entries;
}
}

View File

@@ -2,42 +2,42 @@ package com.commafeed.backend.feeds;
import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
/**
* Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using
* {@link Task} instead of {@link Runnable}
*
*/
@Slf4j
public class FeedRefreshExecutor { public class FeedRefreshExecutor {
private static Logger log = LoggerFactory
.getLogger(FeedRefreshExecutor.class);
private String poolName; private String poolName;
private ThreadPoolExecutor pool; private ThreadPoolExecutor pool;
private LinkedBlockingDeque<Runnable> queue; private LinkedBlockingDeque<Runnable> queue;
public FeedRefreshExecutor(final String poolName, int threads, public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity, MetricRegistry metrics) {
int queueCapacity) {
log.info("Creating pool {} with {} threads", poolName, threads); log.info("Creating pool {} with {} threads", poolName, threads);
this.poolName = poolName; this.poolName = poolName;
pool = new ThreadPoolExecutor(threads, threads, 0, pool = new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
TimeUnit.MILLISECONDS, private static final long serialVersionUID = 1L;
queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
private static final long serialVersionUID = 1L;
@Override @Override
public boolean offer(Runnable r) { public boolean offer(Runnable r) {
Task task = (Task) r; Task task = (Task) r;
if (task.isUrgent()) { if (task.isUrgent()) {
return offerFirst(r); return offerFirst(r);
} else { } else {
return offerLast(r); return offerLast(r);
} }
} }
}); });
pool.setRejectedExecutionHandler(new RejectedExecutionHandler() { pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override @Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
@@ -50,26 +50,30 @@ public class FeedRefreshExecutor {
queue.put(r); queue.put(r);
} }
} catch (InterruptedException e1) { } catch (InterruptedException e1) {
log.error(poolName log.error(poolName + " interrupted while waiting for queue.", e1);
+ " interrupted while waiting for queue.", e1);
} }
} }
}); });
pool.setThreadFactory(new NamedThreadFactory(poolName));
metrics.register(MetricRegistry.name(getClass(), poolName, "active"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return pool.getActiveCount();
}
});
metrics.register(MetricRegistry.name(getClass(), poolName, "pending"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return queue.size();
}
});
} }
public void execute(Task task) { public void execute(Task task) {
pool.execute(task); pool.execute(task);
} }
public int getQueueSize() {
return queue.size();
}
public int getActiveCount() {
return pool.getActiveCount();
}
public static interface Task extends Runnable { public static interface Task extends Runnable {
boolean isUrgent(); boolean isUrgent();
} }
@@ -80,34 +84,8 @@ public class FeedRefreshExecutor {
try { try {
Thread.sleep(100); Thread.sleep(100);
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error( log.error("{} interrupted while waiting for threads to finish.", poolName);
"{} interrupted while waiting for threads to finish.",
poolName);
} }
} }
} }
private static class NamedThreadFactory implements ThreadFactory {
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private NamedThreadFactory(String poolName) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = poolName + "-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
} }

View File

@@ -12,24 +12,29 @@ import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject; import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
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.lang.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.MetricsBean; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.google.api.client.util.Maps;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues; import com.google.common.collect.Queues;
/**
* Infinite loop fetching feeds from the database and queuing them to the {@link FeedRefreshWorker} pool. Also handles feed database updates
* at the end of the cycle through {@link #giveBack(Feed)}.
*
*/
@ApplicationScoped @ApplicationScoped
@Slf4j
public class FeedRefreshTaskGiver { public class FeedRefreshTaskGiver {
protected static final Logger log = LoggerFactory.getLogger(FeedRefreshTaskGiver.class);
@Inject @Inject
FeedDAO feedDAO; FeedDAO feedDAO;
@@ -37,24 +42,30 @@ public class FeedRefreshTaskGiver {
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Inject @Inject
MetricsBean metricsBean; MetricRegistry metrics;
@Inject @Inject
FeedRefreshWorker worker; FeedRefreshWorker worker;
private int backgroundThreads; private int backgroundThreads;
private Queue<Feed> addQueue = Queues.newConcurrentLinkedQueue(); private Queue<FeedRefreshContext> addQueue = Queues.newConcurrentLinkedQueue();
private Queue<Feed> takeQueue = Queues.newConcurrentLinkedQueue(); private Queue<FeedRefreshContext> takeQueue = Queues.newConcurrentLinkedQueue();
private Queue<Feed> giveBackQueue = Queues.newConcurrentLinkedQueue(); private Queue<Feed> giveBackQueue = Queues.newConcurrentLinkedQueue();
private ExecutorService executor; private ExecutorService executor;
private Meter feedRefreshed;
private Meter threadWaited;
private Meter refill;
@PostConstruct @PostConstruct
public void init() { public void init() {
backgroundThreads = applicationSettingsService.get() backgroundThreads = applicationSettingsService.get().getBackgroundThreads();
.getBackgroundThreads();
executor = Executors.newFixedThreadPool(1); executor = Executors.newFixedThreadPool(1);
feedRefreshed = metrics.meter(MetricRegistry.name(getClass(), "feedRefreshed"));
threadWaited = metrics.meter(MetricRegistry.name(getClass(), "threadWaited"));
refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
} }
@PreDestroy @PreDestroy
@@ -83,12 +94,13 @@ public class FeedRefreshTaskGiver {
public void run() { public void run() {
while (!executor.isShutdown()) { while (!executor.isShutdown()) {
try { try {
Feed feed = take(); FeedRefreshContext context = take();
if (feed != null) { if (context != null) {
metricsBean.feedRefreshed(); feedRefreshed.mark();
worker.updateFeed(feed); worker.updateFeed(context);
} else { } else {
log.debug("nothing to do, sleeping for 15s"); log.debug("nothing to do, sleeping for 15s");
threadWaited.mark();
try { try {
Thread.sleep(15000); Thread.sleep(15000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -103,72 +115,100 @@ public class FeedRefreshTaskGiver {
}); });
} }
private Feed take() { /**
Feed feed = takeQueue.poll(); * take a feed from the refresh queue
*/
private FeedRefreshContext take() {
FeedRefreshContext context = takeQueue.poll();
if (feed == null) { if (context == null) {
refill(); refill();
feed = takeQueue.poll(); context = takeQueue.poll();
} }
return feed; return context;
} }
public Long getUpdatableCount() { public Long getUpdatableCount() {
return feedDAO.getUpdatableCount(getThreshold()); return feedDAO.getUpdatableCount(getLastLoginThreshold());
} }
private Date getThreshold() { /**
boolean heavyLoad = applicationSettingsService.get().isHeavyLoad(); * add a feed to the refresh queue
Date threshold = DateUtils.addMinutes(new Date(), heavyLoad ? -15 : -5); */
return threshold; public void add(Feed feed, boolean urgent) {
} int refreshInterval = applicationSettingsService.get().getRefreshIntervalMinutes();
if (feed.getLastUpdated() == null || feed.getLastUpdated().before(DateUtils.addMinutes(new Date(), -1 * refreshInterval))) {
public void add(Feed feed) { addQueue.add(new FeedRefreshContext(feed, urgent));
Date threshold = getThreshold();
if (feed.getLastUpdated() == null
|| feed.getLastUpdated().before(threshold)) {
addQueue.add(feed);
} }
} }
/**
* refills the refresh queue and empties the giveBack queue while at it
*/
private void refill() { private void refill() {
Date now = new Date(); refill.mark();
int count = Math.min(100, 3 * backgroundThreads); int count = Math.min(100, 3 * backgroundThreads);
List<Feed> feeds = null;
if (applicationSettingsService.get().isCrawlingPaused()) { // first, get feeds that are up to refresh from the database
feeds = Lists.newArrayList(); List<FeedRefreshContext> contexts = Lists.newArrayList();
} else { if (!applicationSettingsService.get().isCrawlingPaused()) {
feeds = feedDAO.findNextUpdatable(count, getThreshold()); List<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold());
for (Feed feed : feeds) {
contexts.add(new FeedRefreshContext(feed, false));
}
} }
// then, add to those the feeds we got from the add() method. We add them at the beginning of the list as they probably have a
// higher priority
int size = addQueue.size(); int size = addQueue.size();
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
feeds.add(0, addQueue.poll()); contexts.add(0, addQueue.poll());
} }
Map<Long, Feed> map = Maps.newLinkedHashMap(); // set the disabledDate to now as we use the disabledDate in feedDAO to decide what to refresh next. We also use a map to remove
for (Feed f : feeds) { // duplicates.
f.setLastUpdated(now); Map<Long, FeedRefreshContext> map = Maps.newLinkedHashMap();
map.put(f.getId(), f); for (FeedRefreshContext context : contexts) {
Feed feed = context.getFeed();
feed.setDisabledUntil(new Date());
map.put(feed.getId(), context);
} }
// refill the queue
takeQueue.addAll(map.values()); takeQueue.addAll(map.values());
// add feeds from the giveBack queue to the map, overriding duplicates
size = giveBackQueue.size(); size = giveBackQueue.size();
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
Feed f = giveBackQueue.poll(); Feed feed = giveBackQueue.poll();
f.setLastUpdated(now); map.put(feed.getId(), new FeedRefreshContext(feed, false));
map.put(f.getId(), f);
} }
feedDAO.saveOrUpdate(map.values()); // update all feeds in the database
List<Feed> feeds = Lists.newArrayList();
for (FeedRefreshContext context : map.values()) {
feeds.add(context.getFeed());
}
feedDAO.saveOrUpdate(feeds);
} }
/**
* give a feed back, updating it to the database during the next refill()
*/
public void giveBack(Feed feed) { public void giveBack(Feed feed) {
String normalized = FeedUtils.normalizeURL(feed.getUrl()); String normalized = FeedUtils.normalizeURL(feed.getUrl());
feed.setNormalizedUrl(normalized); feed.setNormalizedUrl(normalized);
feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized)); feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized));
feed.setLastUpdated(new Date());
giveBackQueue.add(feed); giveBackQueue.add(feed);
} }
private Date getLastLoginThreshold() {
if (applicationSettingsService.get().isHeavyLoad()) {
return DateUtils.addDays(new Date(), -30);
} else {
return null;
}
}
} }

View File

@@ -1,7 +1,8 @@
package com.commafeed.backend.feeds; package com.commafeed.backend.feeds;
import java.util.Collection; import java.util.Arrays;
import java.util.Date; import java.util.Date;
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;
@@ -11,12 +12,15 @@ import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject; import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.MetricsBean; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
@@ -25,19 +29,19 @@ import com.commafeed.backend.feeds.FeedRefreshExecutor.Task;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
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.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.pubsubhubbub.SubscriptionHandler; import com.commafeed.backend.pubsubhubbub.SubscriptionHandler;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.FeedUpdateService; import com.commafeed.backend.services.FeedUpdateService;
import com.google.api.client.util.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Striped; import com.google.common.util.concurrent.Striped;
@ApplicationScoped @ApplicationScoped
@Slf4j
public class FeedRefreshUpdater { public class FeedRefreshUpdater {
protected static Logger log = LoggerFactory
.getLogger(FeedRefreshUpdater.class);
@Inject @Inject
FeedUpdateService feedUpdateService; FeedUpdateService feedUpdateService;
@@ -54,7 +58,7 @@ public class FeedRefreshUpdater {
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Inject @Inject
MetricsBean metricsBean; MetricRegistry metrics;
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@@ -68,12 +72,22 @@ public class FeedRefreshUpdater {
private FeedRefreshExecutor pool; private FeedRefreshExecutor pool;
private Striped<Lock> locks; private Striped<Lock> locks;
private Meter entryCacheMiss;
private Meter entryCacheHit;
private Meter feedUpdated;
private Meter entryInserted;
@PostConstruct @PostConstruct
public void init() { public void init() {
ApplicationSettings settings = applicationSettingsService.get(); ApplicationSettings settings = applicationSettingsService.get();
int threads = Math.max(settings.getDatabaseUpdateThreads(), 1); int threads = Math.max(settings.getDatabaseUpdateThreads(), 1);
pool = new FeedRefreshExecutor("feed-refresh-updater", threads, 500 * threads); pool = new FeedRefreshExecutor("feed-refresh-updater", threads, Math.min(50 * threads, 1000), metrics);
locks = Striped.lazyWeakLock(threads * 100000); locks = Striped.lazyWeakLock(threads * 100000);
entryCacheMiss = metrics.meter(MetricRegistry.name(getClass(), "entryCacheMiss"));
entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit"));
feedUpdated = metrics.meter(MetricRegistry.name(getClass(), "feedUpdated"));
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
} }
@PreDestroy @PreDestroy
@@ -81,25 +95,26 @@ public class FeedRefreshUpdater {
pool.shutdown(); pool.shutdown();
} }
public void updateFeed(Feed feed, Collection<FeedEntry> entries) { public void updateFeed(FeedRefreshContext context) {
pool.execute(new EntryTask(feed, entries)); pool.execute(new EntryTask(context));
} }
private class EntryTask implements Task { private class EntryTask implements Task {
private Feed feed; private FeedRefreshContext context;
private Collection<FeedEntry> entries;
public EntryTask(Feed feed, Collection<FeedEntry> entries) { public EntryTask(FeedRefreshContext context) {
this.feed = feed; this.context = context;
this.entries = entries;
} }
@Override @Override
public void run() { public void run() {
boolean ok = true; boolean ok = true;
if (entries.isEmpty() == false) { Feed feed = context.getFeed();
List<FeedEntry> entries = context.getEntries();
if (entries.isEmpty()) {
feed.setMessage("Feed has no entries");
} else {
List<String> lastEntries = cache.getLastEntries(feed); List<String> lastEntries = cache.getLastEntries(feed);
List<String> currentEntries = Lists.newArrayList(); List<String> currentEntries = Lists.newArrayList();
@@ -109,57 +124,87 @@ public class FeedRefreshUpdater {
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 = feedSubscriptionDAO subscriptions = feedSubscriptionDAO.findByFeed(feed);
.findByFeed(feed);
} }
ok &= updateEntry(feed, entry, subscriptions); ok &= addEntry(feed, entry, subscriptions);
metricsBean.entryCacheMiss(); entryCacheMiss.mark();
} else { } else {
log.debug("cache hit for {}", entry.getUrl()); log.debug("cache hit for {}", entry.getUrl());
metricsBean.entryCacheHit(); entryCacheHit.mark();
} }
currentEntries.add(cacheKey); currentEntries.add(cacheKey);
} }
cache.setLastEntries(feed, currentEntries); cache.setLastEntries(feed, currentEntries);
if (subscriptions == null) {
feed.setMessage("No new entries found");
}
if (CollectionUtils.isNotEmpty(subscriptions)) {
List<User> users = Lists.newArrayList();
for (FeedSubscription sub : subscriptions) {
users.add(sub.getUser());
}
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(users.toArray(new User[0]));
}
} }
if (applicationSettingsService.get().isPubsubhubbub()) { if (applicationSettingsService.get().isPubsubhubbub()) {
handlePubSub(feed); handlePubSub(feed);
} }
if (!ok) { if (!ok) {
feed.setDisabledUntil(null); // requeue asap
feed.setDisabledUntil(new Date(0));
} }
metricsBean.feedUpdated(); feedUpdated.mark();
taskGiver.giveBack(feed); taskGiver.giveBack(feed);
} }
@Override @Override
public boolean isUrgent() { public boolean isUrgent() {
return feed.isUrgent(); return context.isUrgent();
} }
} }
private boolean updateEntry(final Feed feed, final FeedEntry entry, private boolean addEntry(final Feed feed, final FeedEntry entry, final List<FeedSubscription> subscriptions) {
final List<FeedSubscription> subscriptions) {
boolean success = false; boolean success = false;
String key = StringUtils.trimToEmpty(entry.getGuid() + entry.getUrl()); // lock on feed, make sure we are not updating the same feed twice at
Lock lock = locks.get(key); // the same time
boolean locked = false; String key1 = StringUtils.trimToEmpty("" + feed.getId());
// lock on content, make sure we are not updating the same entry
// twice at the same time
FeedEntryContent content = entry.getContent();
String key2 = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent() + content.getTitle()));
Iterator<Lock> iterator = locks.bulkGet(Arrays.asList(key1, key2)).iterator();
Lock lock1 = iterator.next();
Lock lock2 = iterator.next();
boolean locked1 = false;
boolean locked2 = false;
try { try {
locked = lock.tryLock(1, TimeUnit.MINUTES); locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
if (locked) { locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
feedUpdateService.updateEntry(feed, entry, subscriptions); if (locked1 && locked2) {
boolean inserted = feedUpdateService.addEntry(feed, entry);
if (inserted) {
entryInserted.mark();
}
success = true; success = true;
} else { } else {
log.error("lock timeout for " + feed.getUrl() + " - " + key); log.error("lock timeout for " + feed.getUrl() + " - " + key1);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error("interrupted while waiting for lock for " + feed.getUrl() log.error("interrupted while waiting for lock for " + feed.getUrl() + " : " + e.getMessage(), e);
+ " : " + e.getMessage(), e);
} finally { } finally {
if (locked) { if (locked1) {
lock.unlock(); lock1.unlock();
}
if (locked2) {
lock2.unlock();
} }
} }
return success; return success;
@@ -179,13 +224,4 @@ public class FeedRefreshUpdater {
} }
} }
} }
public int getQueueSize() {
return pool.getQueueSize();
}
public int getActiveCount() {
return pool.getActiveCount();
}
} }

View File

@@ -8,26 +8,29 @@ import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.commons.codec.digest.DigestUtils; import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.HttpGetter.NotModifiedException; import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.MetricsBean;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.feeds.FeedRefreshExecutor.Task; import com.commafeed.backend.feeds.FeedRefreshExecutor.Task;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
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.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.sun.syndication.io.FeedException;
/**
* Calls {@link FeedFetcher} and handles its outcome
*
*/
@ApplicationScoped @ApplicationScoped
@Slf4j
public class FeedRefreshWorker { public class FeedRefreshWorker {
private static Logger log = LoggerFactory
.getLogger(FeedRefreshWorker.class);
@Inject @Inject
FeedRefreshUpdater feedRefreshUpdater; FeedRefreshUpdater feedRefreshUpdater;
@@ -37,23 +40,19 @@ public class FeedRefreshWorker {
@Inject @Inject
FeedRefreshTaskGiver taskGiver; FeedRefreshTaskGiver taskGiver;
@Inject
MetricRegistry metrics;
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Inject
MetricsBean metricsBean;
@Inject
FeedEntryDAO feedEntryDAO;
private FeedRefreshExecutor pool; private FeedRefreshExecutor pool;
@PostConstruct @PostConstruct
private void init() { private void init() {
ApplicationSettings settings = applicationSettingsService.get(); ApplicationSettings settings = applicationSettingsService.get();
int threads = settings.getBackgroundThreads(); int threads = settings.getBackgroundThreads();
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000), metrics);
20 * threads);
} }
@PreDestroy @PreDestroy
@@ -61,64 +60,55 @@ public class FeedRefreshWorker {
pool.shutdown(); pool.shutdown();
} }
public void updateFeed(Feed feed) { public void updateFeed(FeedRefreshContext context) {
pool.execute(new FeedTask(feed)); pool.execute(new FeedTask(context));
}
public int getQueueSize() {
return pool.getQueueSize();
}
public int getActiveCount() {
return pool.getActiveCount();
} }
private class FeedTask implements Task { private class FeedTask implements Task {
private Feed feed; private FeedRefreshContext context;
public FeedTask(Feed feed) { public FeedTask(FeedRefreshContext context) {
this.feed = feed; this.context = context;
} }
@Override @Override
public void run() { public void run() {
update(feed); update(context);
} }
@Override @Override
public boolean isUrgent() { public boolean isUrgent() {
return feed.isUrgent(); return context.isUrgent();
} }
} }
private void update(Feed feed) { private void update(FeedRefreshContext context) {
Date now = new Date(); Feed feed = context.getFeed();
int refreshInterval = applicationSettingsService.get().getRefreshIntervalMinutes();
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
try { try {
FetchedFeed fetchedFeed = fetcher.fetch(feed.getUrl(), false, String url = ObjectUtils.firstNonNull(feed.getUrlAfterRedirect(), feed.getUrl());
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 // stops here if NotModifiedException or any other exception is thrown
// thrown
List<FeedEntry> entries = fetchedFeed.getEntries(); List<FeedEntry> entries = fetchedFeed.getEntries();
Date disabledUntil = null;
if (applicationSettingsService.get().isHeavyLoad()) { if (applicationSettingsService.get().isHeavyLoad()) {
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
.getFeed().getLastEntryDate(), fetchedFeed.getFeed() .getAverageEntryInterval(), disabledUntil);
.getAverageEntryInterval());
} }
String urlAfterRedirect = fetchedFeed.getUrlAfterRedirect();
feed.setLastUpdateSuccess(now); if (StringUtils.equals(url, urlAfterRedirect)) {
urlAfterRedirect = null;
}
feed.setUrlAfterRedirect(urlAfterRedirect);
feed.setLink(fetchedFeed.getFeed().getLink()); feed.setLink(fetchedFeed.getFeed().getLink());
feed.setLastModifiedHeader(fetchedFeed.getFeed() feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader());
.getLastModifiedHeader());
feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader()); feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader());
feed.setLastContentHash(fetchedFeed.getFeed().getLastContentHash()); feed.setLastContentHash(fetchedFeed.getFeed().getLastContentHash());
feed.setLastPublishedDate(fetchedFeed.getFeed() feed.setLastPublishedDate(fetchedFeed.getFeed().getLastPublishedDate());
.getLastPublishedDate()); feed.setAverageEntryInterval(fetchedFeed.getFeed().getAverageEntryInterval());
feed.setAverageEntryInterval(fetchedFeed.getFeed()
.getAverageEntryInterval());
feed.setLastEntryDate(fetchedFeed.getFeed().getLastEntryDate()); feed.setLastEntryDate(fetchedFeed.getFeed().getLastEntryDate());
feed.setErrorCount(0); feed.setErrorCount(0);
@@ -126,36 +116,27 @@ public class FeedRefreshWorker {
feed.setDisabledUntil(disabledUntil); feed.setDisabledUntil(disabledUntil);
handlePubSub(feed, fetchedFeed.getFeed()); handlePubSub(feed, fetchedFeed.getFeed());
feedRefreshUpdater.updateFeed(feed, entries); context.setEntries(entries);
feedRefreshUpdater.updateFeed(context);
} catch (NotModifiedException e) { } catch (NotModifiedException e) {
log.debug("Feed not modified : {} - {}", feed.getUrl(), log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
e.getMessage());
Date disabledUntil = null;
if (applicationSettingsService.get().isHeavyLoad()) { if (applicationSettingsService.get().isHeavyLoad()) {
disabledUntil = FeedUtils disabledUntil = FeedUtils.buildDisabledUntil(feed.getLastEntryDate(), feed.getAverageEntryInterval(), disabledUntil);
.buildDisabledUntil(feed.getLastEntryDate(),
feed.getAverageEntryInterval());
} }
feed.setErrorCount(0); feed.setErrorCount(0);
feed.setMessage(null); feed.setMessage(e.getMessage());
feed.setDisabledUntil(disabledUntil); feed.setDisabledUntil(disabledUntil);
taskGiver.giveBack(feed); taskGiver.giveBack(feed);
} catch (Exception e) { } catch (Exception e) {
String message = "Unable to refresh feed " + feed.getUrl() + " : " String message = "Unable to refresh feed " + feed.getUrl() + " : " + e.getMessage();
+ e.getMessage(); log.debug(e.getClass().getName() + " " + message, e);
if (e instanceof FeedException) {
log.debug(e.getClass().getName() + " " + message, e);
} else {
log.debug(e.getClass().getName() + " " + message, e);
}
feed.setErrorCount(feed.getErrorCount() + 1); feed.setErrorCount(feed.getErrorCount() + 1);
feed.setMessage(message); feed.setMessage(message);
feed.setDisabledUntil(FeedUtils.buildDisabledUntil(feed feed.setDisabledUntil(FeedUtils.buildDisabledUntil(feed.getErrorCount()));
.getErrorCount()));
taskGiver.giveBack(feed); taskGiver.giveBack(feed);
} }

View File

@@ -1,13 +1,16 @@
package com.commafeed.backend.feeds; package com.commafeed.backend.feeds;
import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
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.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
@@ -21,30 +24,31 @@ 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.mozilla.universalchardet.UniversalDetector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.google.api.client.util.Base64; import com.commafeed.frontend.model.Entry;
import com.google.api.client.util.Lists; import com.google.common.collect.Lists;
import com.google.gwt.i18n.client.HasDirection.Direction; import com.google.gwt.i18n.client.HasDirection.Direction;
import com.google.gwt.i18n.shared.BidiUtils; import com.google.gwt.i18n.shared.BidiUtils;
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;
/**
* Utility methods related to feed handling
*
*/
@Slf4j
public class FeedUtils { public class FeedUtils {
protected static Logger log = LoggerFactory.getLogger(FeedUtils.class);
private static final String ESCAPED_QUESTION_MARK = Pattern.quote("?"); private static final String ESCAPED_QUESTION_MARK = Pattern.quote("?");
private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList(
"height", "width", "border"); private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border");
private static final char[] DISALLOWED_IFRAME_CSS_RULE_CHARACTERS = new char[] { private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
'(', ')' }; private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
public static String truncate(String string, int length) { public static String truncate(String string, int length) {
if (string != null) { if (string != null) {
@@ -54,8 +58,8 @@ public class FeedUtils {
} }
/** /**
* Detect feed encoding by using the declared encoding in the xml processing * Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
* instruction and by detecting the characters used in the feed * feed
* *
*/ */
public static String guessEncoding(byte[] bytes) { public static String guessEncoding(byte[] bytes) {
@@ -64,6 +68,8 @@ public class FeedUtils {
if (StringUtils.endsWith(extracted, "1") == false) { if (StringUtils.endsWith(extracted, "1") == false) {
return extracted; return extracted;
} }
} else if (StringUtils.startsWithIgnoreCase(extracted, "windows-")) {
return extracted;
} }
return detectEncoding(bytes); return detectEncoding(bytes);
} }
@@ -87,8 +93,7 @@ public class FeedUtils {
} }
/** /**
* Normalize the url. The resulting url is not meant to be fetched but * Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to identify a feed and avoid duplicates
* rather used as a mean to identify a feed and avoid duplicates
*/ */
public static String normalizeURL(String url) { public static String normalizeURL(String url) {
if (url == null) { if (url == null) {
@@ -113,13 +118,11 @@ public class FeedUtils {
normalized = normalized.replace("//www.", "//"); normalized = normalized.replace("//www.", "//");
// feedproxy redirects to feedburner // feedproxy redirects to feedburner
normalized = normalized.replace("feedproxy.google.com", normalized = normalized.replace("feedproxy.google.com", "feeds.feedburner.com");
"feeds.feedburner.com");
// feedburner feeds have a special treatment // feedburner feeds have a special treatment
if (normalized.split(ESCAPED_QUESTION_MARK)[0].contains("feedburner.com")) { if (normalized.split(ESCAPED_QUESTION_MARK)[0].contains("feedburner.com")) {
normalized = normalized.replace("feeds2.feedburner.com", normalized = normalized.replace("feeds2.feedburner.com", "feeds.feedburner.com");
"feeds.feedburner.com");
normalized = normalized.split(ESCAPED_QUESTION_MARK)[0]; normalized = normalized.split(ESCAPED_QUESTION_MARK)[0];
normalized = StringUtils.removeEnd(normalized, "/"); normalized = StringUtils.removeEnd(normalized, "/");
} }
@@ -146,17 +149,13 @@ public class FeedUtils {
return encoding; return encoding;
} }
public static String handleContent(String content, String baseUri, public static String handleContent(String content, String baseUri, boolean keepTextOnly) {
boolean keepTextOnly) {
if (StringUtils.isNotBlank(content)) { if (StringUtils.isNotBlank(content)) {
baseUri = StringUtils.trimToEmpty(baseUri); baseUri = StringUtils.trimToEmpty(baseUri);
Whitelist whitelist = new Whitelist(); Whitelist whitelist = new Whitelist();
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em",
"code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong",
"h1", "h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
"li", "ol", "p", "pre", "q", "small", "strike", "strong",
"sub", "sup", "table", "tbody", "td", "tfoot", "th",
"thead", "tr", "u", "ul");
whitelist.addAttributes("div", "dir"); whitelist.addAttributes("div", "dir");
whitelist.addAttributes("pre", "dir"); whitelist.addAttributes("pre", "dir");
@@ -167,22 +166,16 @@ public class FeedUtils {
whitelist.addAttributes("blockquote", "cite"); whitelist.addAttributes("blockquote", "cite");
whitelist.addAttributes("col", "span", "width"); whitelist.addAttributes("col", "span", "width");
whitelist.addAttributes("colgroup", "span", "width"); whitelist.addAttributes("colgroup", "span", "width");
whitelist.addAttributes("iframe", "src", "height", "width", whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
"allowfullscreen", "frameborder", "style"); whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
whitelist.addAttributes("img", "align", "alt", "height", "src",
"title", "width");
whitelist.addAttributes("ol", "start", "type"); whitelist.addAttributes("ol", "start", "type");
whitelist.addAttributes("q", "cite"); whitelist.addAttributes("q", "cite");
whitelist.addAttributes("table", "border", "bordercolor", whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
"summary", "width"); whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
whitelist.addAttributes("td", "border", "bordercolor", "abbr", whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
"axis", "colspan", "rowspan", "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", whitelist.addProtocols("a", "href", "ftp", "http", "https", "mailto");
"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");
@@ -199,8 +192,13 @@ public class FeedUtils {
e.attr("style", escaped); e.attr("style", escaped);
} }
clean.outputSettings(new OutputSettings().escapeMode( for (Element e : clean.select("img[style]")) {
EscapeMode.base).prettyPrint(false)); String style = e.attr("style");
String escaped = escapeImgCss(style);
e.attr("style", escaped);
}
clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
Element body = clean.body(); Element body = clean.body();
if (keepTextOnly) { if (keepTextOnly) {
content = body.text(); content = body.text();
@@ -212,12 +210,11 @@ public class FeedUtils {
} }
public static String escapeIFrameCss(String orig) { public static String escapeIFrameCss(String orig) {
List<String> rules = Lists.newArrayList(); String rule = "";
CSSOMParser parser = new CSSOMParser(); CSSOMParser parser = new CSSOMParser();
try { try {
CSSStyleDeclaration decl = parser List<String> rules = Lists.newArrayList();
.parseStyleDeclaration(new InputSource(new StringReader( CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
orig)));
for (int i = 0; i < decl.getLength(); i++) { for (int i = 0; i < decl.getLength(); i++) {
String property = decl.item(i); String property = decl.item(i);
@@ -226,17 +223,40 @@ public class FeedUtils {
continue; continue;
} }
if (ALLOWED_IFRAME_CSS_RULES.contains(property) if (ALLOWED_IFRAME_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
&& StringUtils.containsNone(value, rules.add(property + ":" + decl.getPropertyValue(property) + ";");
DISALLOWED_IFRAME_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property)
+ ";");
} }
} }
} catch (IOException e) { rule = StringUtils.join(rules, "");
} catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} }
return StringUtils.join(rules, ""); return rule;
}
public static String escapeImgCss(String orig) {
String rule = "";
CSSOMParser parser = new CSSOMParser();
try {
List<String> rules = Lists.newArrayList();
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) {
String property = decl.item(i);
String value = decl.getPropertyValue(property);
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
continue;
}
if (ALLOWED_IMG_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
}
}
rule = StringUtils.join(rules, "");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return rule;
} }
public static boolean isRTL(FeedEntry entry) { public static boolean isRTL(FeedEntry entry) {
@@ -278,8 +298,7 @@ public class FeedUtils {
} }
if (c >= 32 || c == 9 || c == 10 || c == 13) { if (c >= 32 || c == 9 || c == 10 || c == 13) {
if (!Character.isHighSurrogate(c) if (!Character.isHighSurrogate(c) && !Character.isLowSurrogate(c)) {
&& !Character.isLowSurrogate(c)) {
sb.append(c); sb.append(c);
} }
} }
@@ -300,14 +319,13 @@ public class FeedUtils {
disabledHours = Math.min(24 * 7, disabledHours); disabledHours = Math.min(24 * 7, disabledHours);
return DateUtils.addHours(now, disabledHours); return DateUtils.addHours(now, disabledHours);
} }
return null; return now;
} }
/** /**
* When the feed was refreshed successfully * When the feed was refreshed successfully
*/ */
public static Date buildDisabledUntil(Date publishedDate, public static Date buildDisabledUntil(Date publishedDate, Long averageEntryInterval, Date defaultRefreshInterval) {
Long averageEntryInterval) {
Date now = new Date(); Date now = new Date();
if (publishedDate == null) { if (publishedDate == null) {
@@ -323,10 +341,16 @@ public class FeedUtils {
// older than a week, recheck in 6 hours // older than a week, recheck in 6 hours
return DateUtils.addHours(now, 6); return DateUtils.addHours(now, 6);
} else if (averageEntryInterval != null) { } else if (averageEntryInterval != null) {
// use average time between entries to decide when to refresh next // use average time between entries to decide when to refresh next, divided by factor
int factor = 2; int factor = 2;
return new Date(Math.min(DateUtils.addHours(now, 6).getTime(),
now.getTime() + averageEntryInterval / factor)); // not more than 6 hours
long date = Math.min(DateUtils.addHours(now, 6).getTime(), now.getTime() + averageEntryInterval / factor);
// not less than default refresh interval
date = Math.max(defaultRefreshInterval.getTime(), date);
return new Date(date);
} else { } else {
// unknown case, recheck in 24 hours // unknown case, recheck in 24 hours
return DateUtils.addHours(now, 24); return DateUtils.addHours(now, 24);
@@ -378,14 +402,11 @@ public class FeedUtils {
return baseUrl + url; return baseUrl + url;
} }
public static String getFaviconUrl(FeedSubscription subscription, public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) {
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, public static String proxyImages(String content, String publicUrl, boolean proxyImages) {
boolean proxyImages) {
if (!proxyImages) { if (!proxyImages) {
return content; return content;
} }
@@ -398,8 +419,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) String proxy = removeTrailingSlash(publicUrl) + "/rest/server/proxy?u=" + imageProxyEncoder(href);
+ "/rest/server/proxy?u=" + imageProxyEncoder(href);
element.attr("src", proxy); element.attr("src", proxy);
} }
} }
@@ -433,4 +453,27 @@ 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) {
if (StringUtils.isBlank(keywords)) {
return;
}
Iterator<Entry> it = entries.iterator();
while (it.hasNext()) {
Entry entry = it.next();
boolean keep = true;
for (String keyword : keywords.split(" ")) {
String title = Jsoup.parse(entry.getTitle()).text();
String content = Jsoup.parse(entry.getContent()).text();
if (!StringUtils.containsIgnoreCase(content, keyword) && !StringUtils.containsIgnoreCase(title, keyword)) {
keep = false;
break;
}
}
if (!keep) {
it.remove();
}
}
}
} }

View File

@@ -4,7 +4,7 @@ import java.util.List;
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.api.client.util.Lists; import com.google.common.collect.Lists;
public class FetchedFeed { public class FetchedFeed {
@@ -12,6 +12,7 @@ public class FetchedFeed {
private List<FeedEntry> entries = Lists.newArrayList(); private List<FeedEntry> entries = Lists.newArrayList();
private String title; private String title;
private String urlAfterRedirect;
private long fetchDuration; private long fetchDuration;
public Feed getFeed() { public Feed getFeed() {
@@ -45,4 +46,13 @@ public class FetchedFeed {
public void setFetchDuration(long fetchDuration) { public void setFetchDuration(long fetchDuration) {
this.fetchDuration = fetchDuration; this.fetchDuration = fetchDuration;
} }
public String getUrlAfterRedirect() {
return urlAfterRedirect;
}
public void setUrlAfterRedirect(String urlAfterRedirect) {
this.urlAfterRedirect = urlAfterRedirect;
}
} }

View File

@@ -0,0 +1,30 @@
package com.commafeed.backend.metrics;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import lombok.extern.slf4j.Slf4j;
import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.MetricRegistry;
@ApplicationScoped
@Slf4j
public class MetricRegistryProducer {
private MetricRegistry registry;
@PostConstruct
private void init() {
log.info("initializing metrics registry");
registry = new MetricRegistry();
JmxReporter.forRegistry(registry).build().start();
log.info("metrics registry initialized");
}
@Produces
public MetricRegistry produceMetricsRegistry() {
return registry;
}
}

View File

@@ -8,21 +8,26 @@ import javax.persistence.Id;
import javax.persistence.MappedSuperclass; import javax.persistence.MappedSuperclass;
import javax.persistence.TableGenerator; import javax.persistence.TableGenerator;
import lombok.Getter;
import lombok.Setter;
/**
* Abstract model for all entities, defining id and table generator
*
*/
@SuppressWarnings("serial") @SuppressWarnings("serial")
@MappedSuperclass @MappedSuperclass
@Getter
@Setter
public abstract class AbstractModel implements Serializable { public abstract class AbstractModel implements Serializable {
@Id @Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "gen") @GeneratedValue(strategy = GenerationType.TABLE, generator = "gen")
@TableGenerator(name = "gen", table = "hibernate_sequences", pkColumnName = "sequence_name", valueColumnName = "sequence_next_hi_value", allocationSize = 1000) @TableGenerator(
name = "gen",
table = "hibernate_sequences",
pkColumnName = "sequence_name",
valueColumnName = "sequence_next_hi_value",
allocationSize = 1000)
private Long id; private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
} }

View File

@@ -3,17 +3,17 @@ package com.commafeed.backend.model;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Table; import javax.persistence.Table;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import lombok.Getter;
import javax.xml.bind.annotation.XmlRootElement; import lombok.Setter;
import org.apache.log4j.Level; import org.apache.log4j.Level;
@Entity @Entity
@Table(name = "APPLICATIONSETTINGS") @Table(name = "APPLICATIONSETTINGS")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement @Getter
@XmlAccessorType(XmlAccessType.FIELD) @Setter
public class ApplicationSettings extends AbstractModel { public class ApplicationSettings extends AbstractModel {
private String publicUrl; private String publicUrl;
@@ -35,169 +35,9 @@ public class ApplicationSettings extends AbstractModel {
private boolean imageProxyEnabled; private boolean imageProxyEnabled;
private int queryTimeout; private int queryTimeout;
private boolean crawlingPaused; private boolean crawlingPaused;
private int keepStatusDays = 0;
private int refreshIntervalMinutes = 5;
@Column(length = 255) @Column(length = 255)
private String announcement; private String announcement;
public String getPublicUrl() {
return publicUrl;
}
public void setPublicUrl(String publicUrl) {
this.publicUrl = publicUrl;
}
public boolean isAllowRegistrations() {
return allowRegistrations;
}
public void setAllowRegistrations(boolean allowRegistrations) {
this.allowRegistrations = allowRegistrations;
}
public String getGoogleClientId() {
return googleClientId;
}
public void setGoogleClientId(String googleClientId) {
this.googleClientId = googleClientId;
}
public String getGoogleClientSecret() {
return googleClientSecret;
}
public void setGoogleClientSecret(String googleClientSecret) {
this.googleClientSecret = googleClientSecret;
}
public int getBackgroundThreads() {
return backgroundThreads;
}
public void setBackgroundThreads(int backgroundThreads) {
this.backgroundThreads = backgroundThreads;
}
public String getSmtpHost() {
return smtpHost;
}
public void setSmtpHost(String smtpHost) {
this.smtpHost = smtpHost;
}
public int getSmtpPort() {
return smtpPort;
}
public void setSmtpPort(int smtpPort) {
this.smtpPort = smtpPort;
}
public boolean isSmtpTls() {
return smtpTls;
}
public void setSmtpTls(boolean smtpTls) {
this.smtpTls = smtpTls;
}
public String getSmtpUserName() {
return smtpUserName;
}
public void setSmtpUserName(String smtpUserName) {
this.smtpUserName = smtpUserName;
}
public String getSmtpPassword() {
return smtpPassword;
}
public void setSmtpPassword(String smtpPassword) {
this.smtpPassword = smtpPassword;
}
public String getGoogleAnalyticsTrackingCode() {
return googleAnalyticsTrackingCode;
}
public void setGoogleAnalyticsTrackingCode(
String googleAnalyticsTrackingCode) {
this.googleAnalyticsTrackingCode = googleAnalyticsTrackingCode;
}
public String getAnnouncement() {
return announcement;
}
public void setAnnouncement(String announcement) {
this.announcement = announcement;
}
public boolean isFeedbackButton() {
return feedbackButton;
}
public void setFeedbackButton(boolean feedbackButton) {
this.feedbackButton = feedbackButton;
}
public boolean isPubsubhubbub() {
return pubsubhubbub;
}
public void setPubsubhubbub(boolean pubsubhubbub) {
this.pubsubhubbub = pubsubhubbub;
}
public boolean isHeavyLoad() {
return heavyLoad;
}
public void setHeavyLoad(boolean heavyLoad) {
this.heavyLoad = heavyLoad;
}
public int getDatabaseUpdateThreads() {
return databaseUpdateThreads;
}
public void setDatabaseUpdateThreads(int databaseUpdateThreads) {
this.databaseUpdateThreads = databaseUpdateThreads;
}
public String getLogLevel() {
return logLevel;
}
public void setLogLevel(String logLevel) {
this.logLevel = logLevel;
}
public boolean isImageProxyEnabled() {
return imageProxyEnabled;
}
public void setImageProxyEnabled(boolean imageProxyEnabled) {
this.imageProxyEnabled = imageProxyEnabled;
}
public int getQueryTimeout() {
return queryTimeout;
}
public void setQueryTimeout(int queryTimeout) {
this.queryTimeout = queryTimeout;
}
public boolean isCrawlingPaused() {
return crawlingPaused;
}
public void setCrawlingPaused(boolean crawlingPaused) {
this.crawlingPaused = crawlingPaused;
}
} }

View File

@@ -4,13 +4,16 @@ import java.util.Date;
import java.util.Set; import java.util.Set;
import javax.persistence.Cacheable; import javax.persistence.Cacheable;
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.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;
import javax.persistence.Transient;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -20,6 +23,8 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class Feed extends AbstractModel { public class Feed extends AbstractModel {
/** /**
@@ -28,8 +33,11 @@ public class Feed extends AbstractModel {
@Column(length = 2048, nullable = false) @Column(length = 2048, nullable = false)
private String url; private String url;
@Column(length = 40, nullable = false) /**
private String urlHash; * cache the url after potential http 30x redirects
*/
@Column(name = "url_after_redirect", length = 2048, nullable = false)
private String urlAfterRedirect;
@Column(length = 2048, nullable = false) @Column(length = 2048, nullable = false)
private String normalizedUrl; private String normalizedUrl;
@@ -61,12 +69,6 @@ public class Feed extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date lastEntryDate; private Date lastEntryDate;
/**
* Last time we successfully refreshed the feed
*/
@Temporal(TemporalType.TIMESTAMP)
private Date lastUpdateSuccess;
/** /**
* error message while retrieving the feed * error message while retrieving the feed
*/ */
@@ -107,8 +109,8 @@ public class Feed extends AbstractModel {
@Column(length = 40) @Column(length = 40)
private String lastContentHash; private String lastContentHash;
@OneToMany(mappedBy = "feed") @OneToMany(mappedBy = "feed", cascade = CascadeType.REMOVE)
private Set<FeedFeedEntry> entryRelationships; private Set<FeedEntry> entries;
@OneToMany(mappedBy = "feed") @OneToMany(mappedBy = "feed")
private Set<FeedSubscription> subscriptions; private Set<FeedSubscription> subscriptions;
@@ -134,203 +136,4 @@ public class Feed extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date pushLastPing; private Date pushLastPing;
/**
* Denotes a feed that needs to be refreshed before others. Currently used
* when a feed is queued manually for refresh. Not persisted.
*/
@Transient
private boolean urgent;
public Feed() {
}
public Feed(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Date getLastUpdated() {
return lastUpdated;
}
public void setLastUpdated(Date lastUpdated) {
this.lastUpdated = lastUpdated;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Set<FeedSubscription> getSubscriptions() {
return subscriptions;
}
public void setSubscriptions(Set<FeedSubscription> subscriptions) {
this.subscriptions = subscriptions;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
public int getErrorCount() {
return errorCount;
}
public void setErrorCount(int errorCount) {
this.errorCount = errorCount;
}
public Date getDisabledUntil() {
return disabledUntil;
}
public void setDisabledUntil(Date disabledUntil) {
this.disabledUntil = disabledUntil;
}
public String getUrlHash() {
return urlHash;
}
public void setUrlHash(String urlHash) {
this.urlHash = urlHash;
}
public String getLastModifiedHeader() {
return lastModifiedHeader;
}
public void setLastModifiedHeader(String lastModifiedHeader) {
this.lastModifiedHeader = lastModifiedHeader;
}
public String getEtagHeader() {
return etagHeader;
}
public void setEtagHeader(String etagHeader) {
this.etagHeader = etagHeader;
}
public Date getLastUpdateSuccess() {
return lastUpdateSuccess;
}
public void setLastUpdateSuccess(Date lastUpdateSuccess) {
this.lastUpdateSuccess = lastUpdateSuccess;
}
public String getPushHub() {
return pushHub;
}
public void setPushHub(String pushHub) {
this.pushHub = pushHub;
}
public String getPushTopic() {
return pushTopic;
}
public void setPushTopic(String pushTopic) {
this.pushTopic = pushTopic;
}
public Date getPushLastPing() {
return pushLastPing;
}
public void setPushLastPing(Date pushLastPing) {
this.pushLastPing = pushLastPing;
}
public Date getLastPublishedDate() {
return lastPublishedDate;
}
public void setLastPublishedDate(Date lastPublishedDate) {
this.lastPublishedDate = lastPublishedDate;
}
public String getLastContentHash() {
return lastContentHash;
}
public void setLastContentHash(String lastContentHash) {
this.lastContentHash = lastContentHash;
}
public Long getAverageEntryInterval() {
return averageEntryInterval;
}
public void setAverageEntryInterval(Long averageEntryInterval) {
this.averageEntryInterval = averageEntryInterval;
}
public Date getLastEntryDate() {
return lastEntryDate;
}
public void setLastEntryDate(Date lastEntryDate) {
this.lastEntryDate = lastEntryDate;
}
public String getPushTopicHash() {
return pushTopicHash;
}
public void setPushTopicHash(String pushTopicHash) {
this.pushTopicHash = pushTopicHash;
}
public boolean isUrgent() {
return urgent;
}
public void setUrgent(boolean urgent) {
this.urgent = urgent;
}
public String getNormalizedUrl() {
return normalizedUrl;
}
public void setNormalizedUrl(String normalizedUrl) {
this.normalizedUrl = normalizedUrl;
}
public String getNormalizedUrlHash() {
return normalizedUrlHash;
}
public void setNormalizedUrlHash(String normalizedUrlHash) {
this.normalizedUrlHash = normalizedUrlHash;
}
public Set<FeedFeedEntry> getEntryRelationships() {
return entryRelationships;
}
public void setEntryRelationships(Set<FeedFeedEntry> entryRelationships) {
this.entryRelationships = entryRelationships;
}
} }

View File

@@ -11,16 +11,19 @@ import javax.persistence.ManyToOne;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
import com.google.common.collect.Sets;
@Entity @Entity
@Table(name = "FEEDCATEGORIES") @Table(name = "FEEDCATEGORIES")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class FeedCategory extends AbstractModel { public class FeedCategory extends AbstractModel {
@Column(length = 128, nullable = false) @Column(length = 128, nullable = false)
@@ -45,63 +48,4 @@ public class FeedCategory extends AbstractModel {
private Integer position; private Integer position;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public FeedCategory getParent() {
return parent;
}
public void setParent(FeedCategory parent) {
this.parent = parent;
}
public Set<FeedSubscription> getSubscriptions() {
if (subscriptions == null) {
return Sets.newHashSet();
}
return subscriptions;
}
public void setSubscriptions(Set<FeedSubscription> subscriptions) {
this.subscriptions = subscriptions;
}
public Set<FeedCategory> getChildren() {
return children;
}
public void setChildren(Set<FeedCategory> children) {
this.children = children;
}
public boolean isCollapsed() {
return collapsed;
}
public void setCollapsed(boolean collapsed) {
this.collapsed = collapsed;
}
public Integer getPosition() {
return position;
}
public void setPosition(Integer position) {
this.position = position;
}
} }

View File

@@ -9,12 +9,15 @@ import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.OneToOne; import javax.persistence.OneToOne;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import javax.persistence.Transient;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -24,6 +27,8 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class FeedEntry extends AbstractModel { public class FeedEntry extends AbstractModel {
@Column(length = 2048, nullable = false) @Column(length = 2048, nullable = false)
@@ -32,19 +37,16 @@ public class FeedEntry extends AbstractModel {
@Column(length = 40, nullable = false) @Column(length = 40, nullable = false)
private String guidHash; private String guidHash;
@OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE) @ManyToOne(fetch = FetchType.LAZY)
private Set<FeedFeedEntry> feedRelationships; private Feed feed;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false) @OneToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(nullable = false, updatable = false) @JoinColumn(nullable = false, updatable = false)
private FeedEntryContent content; private FeedEntryContent content;
@Column(length = 2048) @Column(length = 2048)
private String url; private String url;
@Column(name = "author", length = 128)
private String author;
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date inserted; private Date inserted;
@@ -54,90 +56,4 @@ public class FeedEntry extends AbstractModel {
@OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE) @OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE)
private Set<FeedEntryStatus> statuses; private Set<FeedEntryStatus> statuses;
/**
* useful placeholder for the subscription, not persisted
*/
@Transient
private FeedSubscription subscription;
public String getGuid() {
return guid;
}
public void setGuid(String guid) {
this.guid = guid;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Date getUpdated() {
return updated;
}
public void setUpdated(Date updated) {
this.updated = updated;
}
public Set<FeedEntryStatus> getStatuses() {
return statuses;
}
public void setStatuses(Set<FeedEntryStatus> statuses) {
this.statuses = statuses;
}
public Date getInserted() {
return inserted;
}
public void setInserted(Date inserted) {
this.inserted = inserted;
}
public FeedEntryContent getContent() {
return content;
}
public void setContent(FeedEntryContent content) {
this.content = content;
}
public String getGuidHash() {
return guidHash;
}
public void setGuidHash(String guidHash) {
this.guidHash = guidHash;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Set<FeedFeedEntry> getFeedRelationships() {
return feedRelationships;
}
public void setFeedRelationships(Set<FeedFeedEntry> feedRelationships) {
this.feedRelationships = feedRelationships;
}
public FeedSubscription getSubscription() {
return subscription;
}
public void setSubscription(FeedSubscription subscription) {
this.subscription = subscription;
}
} }

View File

@@ -1,11 +1,17 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.util.Set;
import javax.persistence.Cacheable; import javax.persistence.Cacheable;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Lob; import javax.persistence.Lob;
import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -14,51 +20,33 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class FeedEntryContent extends AbstractModel { public class FeedEntryContent extends AbstractModel {
@Column(length = 2048) @Column(length = 2048)
private String title; private String title;
@Column(length = 40)
private String titleHash;
@Lob @Lob
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
private String content; private String content;
@Column(length = 40)
private String contentHash;
@Column(name = "author", length = 128)
private String author;
@Column(length = 2048) @Column(length = 2048)
private String enclosureUrl; private String enclosureUrl;
@Column(length = 255) @Column(length = 255)
private String enclosureType; private String enclosureType;
public String getContent() { @OneToMany(mappedBy = "content")
return content; private Set<FeedEntry> entries;
}
public void setContent(String content) {
this.content = content;
}
public String getEnclosureUrl() {
return enclosureUrl;
}
public void setEnclosureUrl(String enclosureUrl) {
this.enclosureUrl = enclosureUrl;
}
public String getEnclosureType() {
return enclosureType;
}
public void setEnclosureType(String enclosureType) {
this.enclosureType = enclosureType;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
} }

View File

@@ -11,6 +11,10 @@ import javax.persistence.ManyToOne;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import javax.persistence.Transient;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -20,6 +24,8 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class FeedEntryStatus extends AbstractModel { public class FeedEntryStatus extends AbstractModel {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@@ -34,6 +40,9 @@ public class FeedEntryStatus extends AbstractModel {
private boolean read; private boolean read;
private boolean starred; private boolean starred;
@Transient
private boolean markable;
/** /**
* Denormalization starts here * Denormalization starts here
*/ */
@@ -60,60 +69,4 @@ public class FeedEntryStatus extends AbstractModel {
setEntryUpdated(entry.getUpdated()); setEntryUpdated(entry.getUpdated());
} }
public FeedSubscription getSubscription() {
return subscription;
}
public void setSubscription(FeedSubscription subscription) {
this.subscription = subscription;
}
public FeedEntry getEntry() {
return entry;
}
public void setEntry(FeedEntry entry) {
this.entry = entry;
}
public boolean isRead() {
return read;
}
public void setRead(boolean read) {
this.read = read;
}
public boolean isStarred() {
return starred;
}
public void setStarred(boolean starred) {
this.starred = starred;
}
public Date getEntryInserted() {
return entryInserted;
}
public void setEntryInserted(Date entryInserted) {
this.entryInserted = entryInserted;
}
public Date getEntryUpdated() {
return entryUpdated;
}
public void setEntryUpdated(Date entryUpdated) {
this.entryUpdated = entryUpdated;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
} }

View File

@@ -1,73 +0,0 @@
package com.commafeed.backend.model;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Table(name = "FEED_FEEDENTRIES")
@SuppressWarnings("serial")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
public class FeedFeedEntry implements Serializable {
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "FEED_ID")
private Feed feed;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "FEEDENTRY_ID")
private FeedEntry entry;
@Temporal(TemporalType.TIMESTAMP)
private Date entryUpdated;
public FeedFeedEntry() {
}
public FeedFeedEntry(Feed feed, FeedEntry entry) {
this.feed = feed;
this.entry = entry;
this.entryUpdated = entry.getUpdated();
}
public Feed getFeed() {
return feed;
}
public void setFeed(Feed feed) {
this.feed = feed;
}
public FeedEntry getEntry() {
return entry;
}
public void setEntry(FeedEntry entry) {
this.entry = entry;
}
public Date getEntryUpdated() {
return entryUpdated;
}
public void setEntryUpdated(Date entryUpdated) {
this.entryUpdated = entryUpdated;
}
}

View File

@@ -12,6 +12,9 @@ import javax.persistence.ManyToOne;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -20,6 +23,8 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class FeedSubscription extends AbstractModel { public class FeedSubscription extends AbstractModel {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@@ -44,52 +49,4 @@ public class FeedSubscription extends AbstractModel {
private Integer position; private Integer position;
public Feed getFeed() {
return feed;
}
public void setFeed(Feed feed) {
this.feed = feed;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public FeedCategory getCategory() {
return category;
}
public void setCategory(FeedCategory category) {
this.category = category;
}
public Set<FeedEntryStatus> getStatuses() {
return statuses;
}
public void setStatuses(Set<FeedEntryStatus> statuses) {
this.statuses = statuses;
}
public Integer getPosition() {
return position;
}
public void setPosition(Integer position) {
this.position = position;
}
} }

View File

@@ -13,6 +13,9 @@ import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -23,6 +26,8 @@ import com.google.common.collect.Sets;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class User extends AbstractModel { public class User extends AbstractModel {
@Column(length = 32, nullable = false, unique = true) @Column(length = 32, nullable = false, unique = true)
@@ -55,107 +60,10 @@ public class User extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date recoverPasswordTokenDate; private Date recoverPasswordTokenDate;
@OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, @OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, CascadeType.REMOVE })
CascadeType.REMOVE })
private Set<UserRole> roles = Sets.newHashSet(); private Set<UserRole> roles = Sets.newHashSet();
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private Set<FeedSubscription> subscriptions; private Set<FeedSubscription> subscriptions;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getPassword() {
return password;
}
public void setPassword(byte[] password) {
this.password = password;
}
public byte[] getSalt() {
return salt;
}
public void setSalt(byte[] salt) {
this.salt = salt;
}
public Set<UserRole> getRoles() {
return roles;
}
public void setRoles(Set<UserRole> roles) {
this.roles = roles;
}
public boolean isDisabled() {
return disabled;
}
public void setDisabled(boolean disabled) {
this.disabled = disabled;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getLastLogin() {
return lastLogin;
}
public void setLastLogin(Date lastLogin) {
this.lastLogin = lastLogin;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getRecoverPasswordToken() {
return recoverPasswordToken;
}
public void setRecoverPasswordToken(String recoverPasswordToken) {
this.recoverPasswordToken = recoverPasswordToken;
}
public Date getRecoverPasswordTokenDate() {
return recoverPasswordTokenDate;
}
public void setRecoverPasswordTokenDate(Date recoverPasswordTokenDate) {
this.recoverPasswordTokenDate = recoverPasswordTokenDate;
}
public Set<FeedSubscription> getSubscriptions() {
return subscriptions;
}
public void setSubscriptions(Set<FeedSubscription> subscriptions) {
this.subscriptions = subscriptions;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
} }

View File

@@ -10,6 +10,9 @@ import javax.persistence.JoinColumn;
import javax.persistence.OneToOne; import javax.persistence.OneToOne;
import javax.persistence.Table; import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -18,6 +21,8 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class UserRole extends AbstractModel { public class UserRole extends AbstractModel {
public static enum Role { public static enum Role {
@@ -41,20 +46,4 @@ public class UserRole extends AbstractModel {
this.role = role; this.role = role;
} }
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
} }

View File

@@ -11,6 +11,9 @@ import javax.persistence.Lob;
import javax.persistence.OneToOne; import javax.persistence.OneToOne;
import javax.persistence.Table; import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -19,6 +22,8 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Cacheable @Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class UserSettings extends AbstractModel { public class UserSettings extends AbstractModel {
public enum ReadingMode { public enum ReadingMode {
@@ -63,84 +68,4 @@ public class UserSettings extends AbstractModel {
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
private String customCss; private String customCss;
public ReadingMode getReadingMode() {
return readingMode;
}
public void setReadingMode(ReadingMode readingMode) {
this.readingMode = readingMode;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getCustomCss() {
return customCss;
}
public void setCustomCss(String customCss) {
this.customCss = customCss;
}
public ReadingOrder getReadingOrder() {
return readingOrder;
}
public void setReadingOrder(ReadingOrder readingOrder) {
this.readingOrder = readingOrder;
}
public boolean isShowRead() {
return showRead;
}
public void setShowRead(boolean showRead) {
this.showRead = showRead;
}
public boolean isSocialButtons() {
return socialButtons;
}
public void setSocialButtons(boolean socialButtons) {
this.socialButtons = socialButtons;
}
public ViewMode getViewMode() {
return viewMode;
}
public void setViewMode(ViewMode viewMode) {
this.viewMode = viewMode;
}
public boolean isScrollMarks() {
return scrollMarks;
}
public void setScrollMarks(boolean scrollMarks) {
this.scrollMarks = scrollMarks;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
}
} }

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend.feeds; package com.commafeed.backend.opml;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -28,13 +28,11 @@ public class OPMLExporter {
public Opml export(User user) { public Opml export(User user) {
Opml opml = new Opml(); Opml opml = new Opml();
opml.setFeedType("opml_1.1"); opml.setFeedType("opml_1.1");
opml.setTitle(String.format("%s subscriptions in CommaFeed", opml.setTitle(String.format("%s subscriptions in CommaFeed", user.getName()));
user.getName()));
opml.setCreated(new Date()); opml.setCreated(new Date());
List<FeedCategory> categories = feedCategoryDAO.findAll(user); List<FeedCategory> categories = feedCategoryDAO.findAll(user);
List<FeedSubscription> subscriptions = feedSubscriptionDAO List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
.findAll(user);
for (FeedCategory cat : categories) { for (FeedCategory cat : categories) {
opml.getOutlines().add(buildCategoryOutline(cat, subscriptions)); opml.getOutlines().add(buildCategoryOutline(cat, subscriptions));
@@ -50,20 +48,17 @@ public class OPMLExporter {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Outline buildCategoryOutline(FeedCategory cat, private Outline buildCategoryOutline(FeedCategory cat, List<FeedSubscription> subscriptions) {
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 : cat.getChildren()) {
outline.getChildren().add( outline.getChildren().add(buildCategoryOutline(child, subscriptions));
buildCategoryOutline(child, subscriptions));
} }
for (FeedSubscription sub : subscriptions) { for (FeedSubscription sub : subscriptions) {
if (sub.getCategory() != null if (sub.getCategory() != null && sub.getCategory().getId().equals(cat.getId())) {
&& sub.getCategory().getId().equals(cat.getId())) {
outline.getChildren().add(buildSubscriptionOutline(sub)); outline.getChildren().add(buildSubscriptionOutline(sub));
} }
} }
@@ -76,11 +71,9 @@ public class OPMLExporter {
outline.setText(sub.getTitle()); outline.setText(sub.getTitle());
outline.setTitle(sub.getTitle()); outline.setTitle(sub.getTitle());
outline.setType("rss"); outline.setType("rss");
outline.getAttributes().add( outline.getAttributes().add(new Attribute("xmlUrl", sub.getFeed().getUrl()));
new Attribute("xmlUrl", sub.getFeed().getUrl()));
if (sub.getFeed().getLink() != null) { if (sub.getFeed().getLink() != null) {
outline.getAttributes().add( outline.getAttributes().add(new Attribute("htmlUrl", sub.getFeed().getLink()));
new Attribute("htmlUrl", sub.getFeed().getLink()));
} }
return outline; return outline;
} }

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend.feeds; package com.commafeed.backend.opml;
import java.io.StringReader; import java.io.StringReader;
import java.util.List; import java.util.List;
@@ -9,12 +9,14 @@ import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType; import javax.ejb.TransactionAttributeType;
import javax.inject.Inject; import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.services.FeedSubscriptionService; import com.commafeed.backend.services.FeedSubscriptionService;
@@ -25,10 +27,9 @@ import com.sun.syndication.io.WireFeedInput;
@Stateless @Stateless
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
@Slf4j
public class OPMLImporter { public class OPMLImporter {
private static Logger log = LoggerFactory.getLogger(OPMLImporter.class);
@Inject @Inject
FeedSubscriptionService feedSubscriptionService; FeedSubscriptionService feedSubscriptionService;
@@ -57,19 +58,18 @@ public class OPMLImporter {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void handleOutline(User user, Outline outline, FeedCategory parent) { private void handleOutline(User user, Outline outline, FeedCategory parent) {
List<Outline> children = outline.getChildren();
if (StringUtils.isEmpty(outline.getType())) { if (CollectionUtils.isNotEmpty(children)) {
String name = FeedUtils.truncate(outline.getText(), 128); String name = FeedUtils.truncate(outline.getText(), 128);
if (name == null) { if (name == null) {
name = FeedUtils.truncate(outline.getTitle(), 128); name = FeedUtils.truncate(outline.getTitle(), 128);
} }
FeedCategory category = feedCategoryDAO.findByName(user, name, FeedCategory category = feedCategoryDAO.findByName(user, name, parent);
parent);
if (category == null) { if (category == null) {
if (StringUtils.isBlank(name)) { if (StringUtils.isBlank(name)) {
name = "Unnamed category"; name = "Unnamed category";
} }
category = new FeedCategory(); category = new FeedCategory();
category.setName(name); category.setName(name);
category.setParent(parent); category.setParent(parent);
@@ -77,7 +77,6 @@ public class OPMLImporter {
feedCategoryDAO.saveOrUpdate(category); feedCategoryDAO.saveOrUpdate(category);
} }
List<Outline> children = outline.getChildren();
for (Outline child : children) { for (Outline child : children) {
handleOutline(user, child, category); handleOutline(user, child, category);
} }
@@ -89,17 +88,15 @@ public class OPMLImporter {
if (StringUtils.isBlank(name)) { if (StringUtils.isBlank(name)) {
name = "Unnamed subscription"; name = "Unnamed subscription";
} }
// make sure we continue with the import process even a feed failed // make sure we continue with the import process even if a feed failed
try { try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent);
name, parent);
} catch (FeedSubscriptionException e) { } catch (FeedSubscriptionException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.error("error while importing {}: {}", outline.getXmlUrl(), log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
e.getMessage());
} }
} }
cache.invalidateUserData(user); cache.invalidateUserRootCategory(user);
} }
} }

View File

@@ -5,6 +5,8 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
@@ -14,21 +16,22 @@ import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver; import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.feeds.FeedUtils; import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.frontend.rest.resources.PubSubHubbubCallbackREST;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
/**
* Sends push subscription requests. Callback is handled by {@link PubSubHubbubCallbackREST}
*
*/
@Slf4j
public class SubscriptionHandler { public class SubscriptionHandler {
private static Logger log = LoggerFactory
.getLogger(SubscriptionHandler.class);
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@@ -47,16 +50,13 @@ public class SubscriptionHandler {
String hub = feed.getPushHub(); String hub = feed.getPushHub();
String topic = feed.getPushTopic(); String topic = feed.getPushTopic();
String publicUrl = FeedUtils String publicUrl = FeedUtils.removeTrailingSlash(applicationSettingsService.get().getPublicUrl());
.removeTrailingSlash(applicationSettingsService.get()
.getPublicUrl());
log.debug("sending new pubsub subscription to {} for {}", hub, topic); log.debug("sending new pubsub subscription to {} for {}", hub, topic);
HttpPost post = new HttpPost(hub); HttpPost post = new HttpPost(hub);
List<NameValuePair> nvp = Lists.newArrayList(); List<NameValuePair> nvp = Lists.newArrayList();
nvp.add(new BasicNameValuePair("hub.callback", publicUrl nvp.add(new BasicNameValuePair("hub.callback", publicUrl + "/rest/push/callback"));
+ "/rest/push/callback"));
nvp.add(new BasicNameValuePair("hub.topic", topic)); nvp.add(new BasicNameValuePair("hub.topic", topic));
nvp.add(new BasicNameValuePair("hub.mode", "subscribe")); nvp.add(new BasicNameValuePair("hub.mode", "subscribe"));
nvp.add(new BasicNameValuePair("hub.verify", "async")); nvp.add(new BasicNameValuePair("hub.verify", "async"));
@@ -65,8 +65,7 @@ public class SubscriptionHandler {
nvp.add(new BasicNameValuePair("hub.lease_seconds", "")); nvp.add(new BasicNameValuePair("hub.lease_seconds", ""));
post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed"); post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed");
post.setHeader(HttpHeaders.CONTENT_TYPE, post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
MediaType.APPLICATION_FORM_URLENCODED);
HttpClient client = HttpGetter.newClient(20000); HttpClient client = HttpGetter.newClient(20000);
try { try {
@@ -77,23 +76,19 @@ public class SubscriptionHandler {
if (code != 204 && code != 202 && code != 200) { if (code != 204 && code != 202 && code != 200) {
String message = EntityUtils.toString(response.getEntity()); String message = EntityUtils.toString(response.getEntity());
String pushpressError = " is value is not allowed. You may only subscribe to"; String pushpressError = " is value is not allowed. You may only subscribe to";
if (code == 400 if (code == 400 && StringUtils.contains(message, pushpressError)) {
&& StringUtils.contains(message, pushpressError)) {
String[] tokens = message.split(" "); String[] tokens = message.split(" ");
feed.setPushTopic(tokens[tokens.length - 1]); feed.setPushTopic(tokens[tokens.length - 1]);
taskGiver.giveBack(feed); taskGiver.giveBack(feed);
log.debug("handled pushpress subfeed {} : {}", topic, log.debug("handled pushpress subfeed {} : {}", topic, feed.getPushTopic());
feed.getPushTopic());
} else { } else {
throw new Exception("Unexpected response code: " + code throw new Exception("Unexpected response code: " + code + " " + response.getStatusLine().getReasonPhrase() + " - "
+ " " + response.getStatusLine().getReasonPhrase() + message);
+ " - " + message);
} }
} }
log.debug("subscribed to {} for {}", hub, topic); log.debug("subscribed to {} for {}", hub, topic);
} catch (Exception e) { } catch (Exception e) {
log.error("Could not subscribe to {} for {} : " + e.getMessage(), log.error("Could not subscribe to {} for {} : " + e.getMessage(), hub, topic);
hub, topic);
} finally { } finally {
client.getConnectionManager().shutdown(); client.getConnectionManager().shutdown();
} }

View File

@@ -4,8 +4,11 @@ import org.jdom.Element;
import com.sun.syndication.feed.opml.Opml; import com.sun.syndication.feed.opml.Opml;
public class OPML11Generator extends /**
com.sun.syndication.io.impl.OPML10Generator { * Add missing title to the generated OPML
*
*/
public class OPML11Generator extends com.sun.syndication.io.impl.OPML10Generator {
public OPML11Generator() { public OPML11Generator() {
super("opml_1.1"); super("opml_1.1");

View File

@@ -5,21 +5,22 @@ import org.jdom.Element;
import com.sun.syndication.io.impl.OPML10Parser; import com.sun.syndication.io.impl.OPML10Parser;
/**
* Support for OPML 1.1 parsing
*
*/
public class OPML11Parser extends OPML10Parser { public class OPML11Parser extends OPML10Parser {
public OPML11Parser() { public OPML11Parser() {
super("opml_1.1"); super("opml_1.1");
} }
@Override @Override
public boolean isMyType(Document document) { public boolean isMyType(Document document) {
Element e = document.getRootElement(); Element e = document.getRootElement();
if (e.getName().equals("opml") if (e.getName().equals("opml") && (e.getChild("head") == null || e.getChild("head").getChild("docs") == null)
&& (e.getChild("head") == null || e.getChild("head").getChild( && (e.getAttributeValue("version") == null || e.getAttributeValue("version").equals("1.1"))) {
"docs") == null)
&& (e.getAttributeValue("version") == null || e
.getAttributeValue("version").equals("1.1"))) {
return true; return true;
} }

View File

@@ -6,6 +6,10 @@ import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry; import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.impl.ConverterForRSS090; import com.sun.syndication.feed.synd.impl.ConverterForRSS090;
/**
* Support description tag for RSS09
*
*/
public class RSS090DescriptionConverter extends ConverterForRSS090 { public class RSS090DescriptionConverter extends ConverterForRSS090 {
@Override @Override

View File

@@ -6,6 +6,10 @@ import com.sun.syndication.feed.rss.Description;
import com.sun.syndication.feed.rss.Item; import com.sun.syndication.feed.rss.Item;
import com.sun.syndication.io.impl.RSS090Parser; import com.sun.syndication.io.impl.RSS090Parser;
/**
* Support description tag for RSS09
*
*/
public class RSS090DescriptionParser extends RSS090Parser { public class RSS090DescriptionParser extends RSS090Parser {
@Override @Override

View File

@@ -26,8 +26,7 @@ public class RSSRDF10Parser extends RSS10Parser {
Element rssRoot = document.getRootElement(); Element rssRoot = document.getRootElement();
Namespace defaultNS = rssRoot.getNamespace(); Namespace defaultNS = rssRoot.getNamespace();
List additionalNSs = Lists.newArrayList(rssRoot List additionalNSs = Lists.newArrayList(rssRoot.getAdditionalNamespaces());
.getAdditionalNamespaces());
List<Element> children = rssRoot.getChildren(); List<Element> children = rssRoot.getChildren();
if (CollectionUtils.isNotEmpty(children)) { if (CollectionUtils.isNotEmpty(children)) {
Element child = children.get(0); Element child = children.get(0);

View File

@@ -1,10 +1,13 @@
package com.commafeed.backend.services; package com.commafeed.backend.services;
import java.util.Date;
import java.util.Enumeration; import java.util.Enumeration;
import javax.ejb.Singleton; import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Level; import org.apache.log4j.Level;
import org.apache.log4j.LogManager; import org.apache.log4j.LogManager;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@@ -13,7 +16,7 @@ import com.commafeed.backend.dao.ApplicationSettingsDAO;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@Singleton @ApplicationScoped
public class ApplicationSettingsService { public class ApplicationSettingsService {
@Inject @Inject
@@ -21,20 +24,26 @@ public class ApplicationSettingsService {
private ApplicationSettings settings; private ApplicationSettings settings;
public void save(ApplicationSettings settings) { @PostConstruct
this.settings = settings; private void init() {
applicationSettingsDAO.saveOrUpdate(settings); settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
applyLogLevel();
} }
public ApplicationSettings get() { public ApplicationSettings get() {
if (settings == null) {
settings = Iterables.getFirst(applicationSettingsDAO.findAll(),
null);
}
return settings; return settings;
} }
public void save(ApplicationSettings settings) {
applicationSettingsDAO.saveOrUpdate(settings);
this.settings = settings;
applyLogLevel();
}
public Date getUnreadThreshold() {
int keepStatusDays = get().getKeepStatusDays();
return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void applyLogLevel() { public void applyLogLevel() {
String logLevel = get().getLogLevel(); String logLevel = get().getLogLevel();

View File

@@ -1,24 +1,30 @@
package com.commafeed.backend; package com.commafeed.backend.services;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.services.ApplicationSettingsService;
public class DatabaseCleaner { /**
* Contains utility methods for cleaning the database
private static Logger log = LoggerFactory.getLogger(DatabaseCleaner.class); *
*/
@Slf4j
public class DatabaseCleaningService {
@Inject @Inject
FeedDAO feedDAO; FeedDAO feedDAO;
@@ -29,6 +35,12 @@ public class DatabaseCleaner {
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@Inject
FeedEntryContentDAO feedEntryContentDAO;
@Inject
FeedEntryStatusDAO feedEntryStatusDAO;
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@@ -45,16 +57,16 @@ public class DatabaseCleaner {
return total; return total;
} }
public long cleanEntriesWithoutFeeds() { public long cleanContentsWithoutEntries() {
long total = 0; long total = 0;
int deleted = -1; int deleted = -1;
do { do {
deleted = feedEntryDAO.deleteWithoutFeeds(100); deleted = feedEntryContentDAO.deleteWithoutEntries(10);
total += deleted; total += deleted;
log.info("removed {} entries without feeds", total); log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} entries without feeds deleted", total); log.info("cleanup done: {} feeds without subscriptions deleted", total);
return total; return total;
} }
@@ -83,9 +95,24 @@ public class DatabaseCleaner {
sub.setFeed(into); sub.setFeed(into);
} }
feedSubscriptionDAO.saveOrUpdate(subs); feedSubscriptionDAO.saveOrUpdate(subs);
feedDAO.deleteRelationships(feed);
feedDAO.delete(feed); feedDAO.delete(feed);
} }
feedDAO.saveOrUpdate(into); feedDAO.saveOrUpdate(into);
} }
public long cleanStatusesOlderThan(Date olderThan) {
log.info("cleaning old read statuses");
long total = 0;
List<FeedEntryStatus> list = Collections.emptyList();
do {
list = feedEntryStatusDAO.getOldStatuses(olderThan, 100);
if (!list.isEmpty()) {
feedEntryStatusDAO.delete(list);
total += list.size();
log.info("cleaned {} old read statuses", total);
}
} while (!list.isEmpty());
log.info("cleanup done: {} old read statuses deleted", total);
return total;
}
} }

View File

@@ -0,0 +1,42 @@
package com.commafeed.backend.services;
import javax.inject.Inject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.FeedEntryContent;
public class FeedEntryContentService {
@Inject
FeedEntryContentDAO feedEntryContentDAO;
/**
* this is NOT thread-safe
*/
public FeedEntryContent findOrCreate(FeedEntryContent content, String baseUrl) {
String contentHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent()));
String titleHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getTitle()));
Long existingId = feedEntryContentDAO.findExisting(contentHash, titleHash);
FeedEntryContent result = null;
if (existingId == null) {
content.setContentHash(contentHash);
content.setTitleHash(titleHash);
content.setAuthor(FeedUtils.truncate(FeedUtils.handleContent(content.getAuthor(), baseUrl, true), 128));
content.setTitle(FeedUtils.truncate(FeedUtils.handleContent(content.getTitle(), baseUrl, true), 2048));
content.setContent(FeedUtils.handleContent(content.getContent(), baseUrl, false));
result = content;
feedEntryContentDAO.saveOrUpdate(result);
} else {
result = new FeedEntryContent();
result.setId(existingId);
}
return result;
}
}

View File

@@ -1,8 +1,12 @@
package com.commafeed.backend.services; package com.commafeed.backend.services;
import java.util.Date;
import java.util.List;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.inject.Inject; import javax.inject.Inject;
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;
@@ -10,6 +14,7 @@ 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;
@Stateless @Stateless
public class FeedEntryService { public class FeedEntryService {
@@ -19,50 +24,37 @@ public class FeedEntryService {
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@Inject @Inject
FeedEntryDAO feedEntryDAO; FeedEntryDAO feedEntryDAO;
public void markEntry(User user, Long entryId, Long subscriptionId, @Inject
boolean read) { CacheService cache;
FeedSubscription sub = feedSubscriptionDAO.findById(user,
subscriptionId); public void markEntry(User user, Long entryId, boolean read) {
if (sub == null) {
return;
}
FeedEntry entry = feedEntryDAO.findById(entryId); FeedEntry entry = feedEntryDAO.findById(entryId);
if (entry == null) { if (entry == null) {
return; return;
} }
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry); FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
if (sub == null) {
if (read) { return;
if (status.getId() != null) {
if (status.isStarred()) {
status.setRead(true);
feedEntryStatusDAO.saveOrUpdate(status);
} else {
feedEntryStatusDAO.delete(status);
}
}
} else {
if (status.getId() == null) {
status = new FeedEntryStatus(user, sub, entry);
status.setSubscription(sub);
}
status.setRead(false);
feedEntryStatusDAO.saveOrUpdate(status);
} }
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry);
if (status.isMarkable()) {
status.setRead(read);
feedEntryStatusDAO.saveOrUpdate(status);
cache.invalidateUnreadCount(sub);
cache.invalidateUserRootCategory(user);
}
} }
public void starEntry(User user, Long entryId, Long subscriptionId, public void starEntry(User user, Long entryId, Long subscriptionId, boolean starred) {
boolean starred) {
FeedSubscription sub = feedSubscriptionDAO.findById(user, FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId);
subscriptionId);
if (sub == null) { if (sub == null) {
return; return;
} }
@@ -73,24 +65,34 @@ public class FeedEntryService {
} }
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry); FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry);
status.setStarred(starred);
feedEntryStatusDAO.saveOrUpdate(status);
}
if (!starred) { public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) {
if (status.getId() != null) { List<FeedEntryStatus> statuses = feedEntryStatusDAO
if (!status.isRead()) { .findBySubscriptions(subscriptions, true, null, null, -1, -1, null, false, false);
status.setStarred(false); markList(statuses, olderThan);
feedEntryStatusDAO.saveOrUpdate(status); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
} else { cache.invalidateUserRootCategory(user);
feedEntryStatusDAO.delete(status); }
public void markStarredEntries(User user, Date olderThan) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findStarred(user, null, -1, -1, null, false);
markList(statuses, olderThan);
}
private void markList(List<FeedEntryStatus> statuses, Date olderThan) {
List<FeedEntryStatus> list = Lists.newArrayList();
for (FeedEntryStatus status : statuses) {
if (!status.isRead()) {
Date inserted = status.getEntry().getInserted();
if (olderThan == null || inserted == null || olderThan.after(inserted)) {
status.setRead(true);
list.add(status);
} }
} }
} else {
if (status.getId() == null) {
status = new FeedEntryStatus(user, sub, entry);
status.setSubscription(sub);
status.setRead(true);
}
status.setStarred(true);
feedEntryStatusDAO.saveOrUpdate(status);
} }
feedEntryStatusDAO.saveOrUpdate(list);
} }
} }

View File

@@ -1,5 +1,7 @@
package com.commafeed.backend.services; package com.commafeed.backend.services;
import java.util.Date;
import javax.ejb.Lock; import javax.ejb.Lock;
import javax.ejb.LockType; import javax.ejb.LockType;
import javax.ejb.Singleton; import javax.ejb.Singleton;
@@ -28,9 +30,9 @@ public class FeedService {
String normalized = FeedUtils.normalizeURL(url); String normalized = FeedUtils.normalizeURL(url);
feed = new Feed(); feed = new Feed();
feed.setUrl(url); feed.setUrl(url);
feed.setUrlHash(DigestUtils.sha1Hex(url));
feed.setNormalizedUrl(normalized); feed.setNormalizedUrl(normalized);
feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized)); feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized));
feed.setDisabledUntil(new Date(0));
feedDAO.saveOrUpdate(feed); feedDAO.saveOrUpdate(feed);
} }
return feed; return feed;

View File

@@ -6,9 +6,9 @@ import java.util.Map;
import javax.ejb.ApplicationException; import javax.ejb.ApplicationException;
import javax.inject.Inject; import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
@@ -18,18 +18,15 @@ import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.feeds.FeedUtils; import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription; 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.google.api.client.util.Lists; import com.commafeed.frontend.model.UnreadCount;
import com.google.common.collect.Maps;
@Slf4j
public class FeedSubscriptionService { public class FeedSubscriptionService {
private static Logger log = LoggerFactory
.getLogger(FeedSubscriptionService.class);
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApplicationException @ApplicationException
public static class FeedSubscriptionException extends RuntimeException { public static class FeedSubscriptionException extends RuntimeException {
@@ -59,63 +56,62 @@ public class FeedSubscriptionService {
@Inject @Inject
CacheService cache; CacheService cache;
public Feed subscribe(User user, String url, String title, public Feed subscribe(User user, String url, String title, FeedCategory category) {
FeedCategory category) {
final String pubUrl = applicationSettingsService.get().getPublicUrl(); final String pubUrl = applicationSettingsService.get().getPublicUrl();
if (StringUtils.isBlank(pubUrl)) { if (StringUtils.isBlank(pubUrl)) {
throw new FeedSubscriptionException( throw new FeedSubscriptionException("Public URL of this CommaFeed instance is not set");
"Public URL of this CommaFeed instance is not set");
} }
if (url.startsWith(pubUrl)) { if (url.startsWith(pubUrl)) {
throw new FeedSubscriptionException( throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance");
"Could not subscribe to a feed from this CommaFeed instance");
} }
Feed feed = feedService.findOrCreate(url); Feed feed = feedService.findOrCreate(url);
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, feed); FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, feed);
boolean newSubscription = false;
if (sub == null) { if (sub == null) {
sub = new FeedSubscription(); sub = new FeedSubscription();
sub.setFeed(feed); sub.setFeed(feed);
sub.setUser(user); sub.setUser(user);
newSubscription = true;
} }
sub.setCategory(category); sub.setCategory(category);
sub.setPosition(0); sub.setPosition(0);
sub.setTitle(FeedUtils.truncate(title, 128)); sub.setTitle(FeedUtils.truncate(title, 128));
feedSubscriptionDAO.saveOrUpdate(sub); feedSubscriptionDAO.saveOrUpdate(sub);
if (newSubscription) { taskGiver.add(feed, false);
try { cache.invalidateUserRootCategory(user);
List<FeedEntryStatus> statuses = Lists.newArrayList();
List<FeedEntry> allEntries = feedEntryDAO.findByFeed(feed, 0,
10);
for (FeedEntry entry : allEntries) {
FeedEntryStatus status = new FeedEntryStatus(user, sub, entry);
status.setRead(false);
status.setSubscription(sub);
statuses.add(status);
}
feedEntryStatusDAO.saveOrUpdate(statuses);
} catch (Exception e) {
log.error(
"could not fetch initial statuses when importing {} : {}",
feed.getUrl(), e.getMessage());
}
}
taskGiver.add(feed);
return feed; return feed;
} }
public Map<Long, Long> getUnreadCount(User user) { public boolean unsubscribe(User user, Long subId) {
Map<Long, Long> map = cache.getUnreadCounts(user); FeedSubscription sub = feedSubscriptionDAO.findById(user, subId);
if (map == null) { if (sub != null) {
log.debug("unread count cache miss for {}", Models.getId(user)); feedSubscriptionDAO.delete(sub);
map = feedEntryStatusDAO.getUnreadCount(user); cache.invalidateUserRootCategory(user);
cache.setUnreadCounts(user, map); return true;
} else {
return false;
}
}
public UnreadCount getUnreadCount(FeedSubscription sub) {
UnreadCount count = cache.getUnreadCount(sub);
if (count == null) {
log.debug("unread count cache miss for {}", Models.getId(sub));
count = feedEntryStatusDAO.getUnreadCount(sub);
cache.setUnreadCount(sub, count);
}
return count;
}
public Map<Long, UnreadCount> getUnreadCount(User user) {
Map<Long, UnreadCount> map = Maps.newHashMap();
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) {
map.put(sub.getId(), getUnreadCount(sub));
} }
return map; return map;
} }
} }

View File

@@ -1,91 +1,43 @@
package com.commafeed.backend.services; package com.commafeed.backend.services;
import java.util.Date; import java.util.Date;
import java.util.List;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.inject.Inject; import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.commafeed.backend.MetricsBean; import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryDAO.EntryWithFeed;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.feeds.FeedUtils;
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.FeedFeedEntry;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.google.common.collect.Lists;
@Stateless @Stateless
public class FeedUpdateService { public class FeedUpdateService {
@PersistenceContext
protected EntityManager em;
@Inject
FeedSubscriptionDAO feedSubscriptionDAO;
@Inject @Inject
FeedEntryDAO feedEntryDAO; FeedEntryDAO feedEntryDAO;
@Inject @Inject
FeedEntryStatusDAO feedEntryStatusDAO; FeedEntryContentService feedEntryContentService;
@Inject /**
MetricsBean metricsBean; * this is NOT thread-safe
*/
public boolean addEntry(Feed feed, FeedEntry entry) {
@Inject Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed.getId());
CacheService cache; if (existing != null) {
return false;
public void updateEntry(Feed feed, FeedEntry entry,
List<FeedSubscription> subscriptions) {
EntryWithFeed existing = feedEntryDAO.findExisting(entry.getGuid(),
entry.getUrl(), feed.getId());
FeedEntry update = null;
FeedFeedEntry ffe = null;
if (existing == null) {
entry.setAuthor(FeedUtils.truncate(FeedUtils.handleContent(
entry.getAuthor(), feed.getLink(), true), 128));
FeedEntryContent content = entry.getContent();
content.setTitle(FeedUtils.truncate(FeedUtils.handleContent(
content.getTitle(), feed.getLink(), true), 2048));
content.setContent(FeedUtils.handleContent(content.getContent(),
feed.getLink(), false));
entry.setInserted(new Date());
ffe = new FeedFeedEntry(feed, entry);
update = entry;
} else if (existing.ffe == null) {
ffe = new FeedFeedEntry(feed, existing.entry);
update = existing.entry;
} }
if (update != null) { FeedEntryContent content = feedEntryContentService.findOrCreate(entry.getContent(), feed.getLink());
List<FeedEntryStatus> statusUpdateList = Lists.newArrayList(); entry.setGuidHash(DigestUtils.sha1Hex(entry.getGuid()));
List<User> users = Lists.newArrayList(); entry.setContent(content);
for (FeedSubscription sub : subscriptions) { entry.setInserted(new Date());
User user = sub.getUser(); entry.setFeed(feed);
FeedEntryStatus status = new FeedEntryStatus(user, sub, update);
status.setSubscription(sub); feedEntryDAO.saveOrUpdate(entry);
statusUpdateList.add(status); return true;
users.add(user);
}
cache.invalidateUserData(users.toArray(new User[0]));
feedEntryDAO.saveOrUpdate(update);
feedEntryStatusDAO.saveOrUpdate(statusUpdateList);
em.persist(ffe);
metricsBean.entryUpdated(statusUpdateList.size());
}
} }
} }

View File

@@ -12,22 +12,20 @@ import javax.mail.Transport;
import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
/**
* Mailing service
*
*/
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class MailService implements Serializable { public class MailService implements Serializable {
protected static Logger log = LoggerFactory.getLogger(MailService.class);
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
public void sendMail(User user, String subject, String content) public void sendMail(User user, String subject, String content) throws Exception {
throws Exception {
ApplicationSettings settings = applicationSettingsService.get(); ApplicationSettings settings = applicationSettingsService.get();
@@ -50,8 +48,7 @@ public class MailService implements Serializable {
Message message = new MimeMessage(session); Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(username, "CommaFeed")); message.setFrom(new InternetAddress(username, "CommaFeed"));
message.setRecipients(Message.RecipientType.TO, message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest));
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

@@ -10,25 +10,19 @@ import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEKeySpec;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.dao.UserDAO; // taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
// http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Slf4j
public class PasswordEncryptionService implements Serializable { public class PasswordEncryptionService implements Serializable {
private static final Logger log = LoggerFactory.getLogger(UserDAO.class); public boolean authenticate(String attemptedPassword, byte[] encryptedPassword, byte[] salt) {
public boolean authenticate(String attemptedPassword,
byte[] encryptedPassword, byte[] salt) {
// Encrypt the clear-text password using the same salt that was used to // Encrypt the clear-text password using the same salt that was used to
// encrypt the original password // encrypt the original password
byte[] encryptedAttemptedPassword = null; byte[] encryptedAttemptedPassword = null;
try { try {
encryptedAttemptedPassword = getEncryptedPassword( encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt);
attemptedPassword, salt);
} catch (Exception e) { } catch (Exception e) {
// should never happen // should never happen
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
@@ -53,8 +47,7 @@ public class PasswordEncryptionService implements Serializable {
// http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/ // http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/
int iterations = 20000; int iterations = 20000;
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength);
derivedKeyLength);
byte[] bytes = null; byte[] bytes = null;
try { try {

View File

@@ -48,15 +48,13 @@ public class UserService {
User user = userDAO.findByName(name); User user = userDAO.findByName(name);
if (user != null && !user.isDisabled()) { if (user != null && !user.isDisabled()) {
boolean authenticated = encryptionService.authenticate(password, boolean authenticated = encryptionService.authenticate(password, user.getPassword(), user.getSalt());
user.getPassword(), user.getSalt());
if (authenticated) { if (authenticated) {
Date lastLogin = user.getLastLogin(); Date lastLogin = user.getLastLogin();
Date now = new Date(); Date now = new Date();
// only update lastLogin field every hour in order to not // only update lastLogin field every hour in order to not
// invalidate the cache everytime someone logs in // invalidate the cache everytime someone logs in
if (lastLogin == null if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
|| lastLogin.before(DateUtils.addHours(now, -1))) {
user.setLastLogin(now); user.setLastLogin(now);
userDAO.saveOrUpdate(user); userDAO.saveOrUpdate(user);
} }
@@ -66,39 +64,30 @@ public class UserService {
return null; return null;
} }
public User register(String name, String password, String email, public User register(String name, String password, String email, Collection<Role> roles) {
Collection<Role> roles) {
return register(name, password, email, roles, false); return register(name, password, email, roles, false);
} }
public User register(String name, String password, String email, public User register(String name, String password, String email, Collection<Role> roles, boolean forceRegistration) {
Collection<Role> roles, boolean forceRegistration) {
Preconditions.checkNotNull(name); Preconditions.checkNotNull(name);
Preconditions.checkArgument(StringUtils.length(name) <= 32, Preconditions.checkArgument(StringUtils.length(name) <= 32, "Name too long (32 characters maximum)");
"Name too long (32 characters maximum)");
Preconditions.checkNotNull(password); Preconditions.checkNotNull(password);
if (!forceRegistration) { if (!forceRegistration) {
Preconditions.checkState(applicationSettingsService.get() Preconditions.checkState(applicationSettingsService.get().isAllowRegistrations(),
.isAllowRegistrations(),
"Registrations are closed on this CommaFeed instance"); "Registrations are closed on this CommaFeed instance");
Preconditions.checkNotNull(email); Preconditions.checkNotNull(email);
Preconditions.checkArgument(StringUtils.length(name) >= 3, Preconditions.checkArgument(StringUtils.length(name) >= 3, "Name too short (3 characters minimum)");
"Name too short (3 characters minimum)"); Preconditions
Preconditions.checkArgument( .checkArgument(forceRegistration || StringUtils.length(password) >= 6, "Password too short (6 characters maximum)");
forceRegistration || StringUtils.length(password) >= 6, Preconditions.checkArgument(StringUtils.contains(email, "@"), "Invalid email address");
"Password too short (6 characters maximum)");
Preconditions.checkArgument(StringUtils.contains(email, "@"),
"Invalid email address");
} }
Preconditions.checkArgument(userDAO.findByName(name) == null, Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken");
"Name already taken");
if (StringUtils.isNotBlank(email)) { if (StringUtils.isNotBlank(email)) {
Preconditions.checkArgument(userDAO.findByEmail(email) == null, Preconditions.checkArgument(userDAO.findByEmail(email) == null, "Email already taken");
"Email already taken");
} }
User user = new User(); User user = new User();
@@ -122,8 +111,7 @@ public class UserService {
} }
public String generateApiKey(User user) { public String generateApiKey(User user) {
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID() byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt());
.toString(), user.getSalt());
return DigestUtils.sha1Hex(key); return DigestUtils.sha1Hex(key);
} }
} }

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend; package com.commafeed.backend.startup;
import java.sql.Connection; import java.sql.Connection;
@@ -20,6 +20,10 @@ import liquibase.structure.DatabaseObject;
import com.commafeed.backend.services.ApplicationPropertiesService; import com.commafeed.backend.services.ApplicationPropertiesService;
/**
* Executes needed liquibase database schema upgrades
*
*/
@Stateless @Stateless
@TransactionManagement(TransactionManagementType.BEAN) @TransactionManagement(TransactionManagementType.BEAN)
public class DatabaseUpdater { public class DatabaseUpdater {
@@ -33,18 +37,14 @@ public class DatabaseUpdater {
try { try {
Thread currentThread = Thread.currentThread(); Thread currentThread = Thread.currentThread();
ClassLoader classLoader = currentThread.getContextClassLoader(); ClassLoader classLoader = currentThread.getContextClassLoader();
ResourceAccessor accessor = new ClassLoaderResourceAccessor( ResourceAccessor accessor = new ClassLoaderResourceAccessor(classLoader);
classLoader);
context = new InitialContext(); context = new InitialContext();
DataSource dataSource = (DataSource) context DataSource dataSource = (DataSource) context.lookup(datasourceName);
.lookup(datasourceName);
connection = dataSource.getConnection(); connection = dataSource.getConnection();
JdbcConnection jdbcConnection = new JdbcConnection(connection); JdbcConnection jdbcConnection = new JdbcConnection(connection);
Database database = DatabaseFactory.getInstance() Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcConnection);
.findCorrectDatabaseImplementation(
jdbcConnection);
if (database instanceof PostgresDatabase) { if (database instanceof PostgresDatabase) {
database = new PostgresDatabase() { database = new PostgresDatabase() {
@@ -56,9 +56,7 @@ public class DatabaseUpdater {
database.setConnection(jdbcConnection); database.setConnection(jdbcConnection);
} }
Liquibase liq = new Liquibase( Liquibase liq = new Liquibase("changelogs/db.changelog-master.xml", accessor, database);
"changelogs/db.changelog-master.xml", accessor,
database);
liq.update("prod"); liq.update("prod");
} finally { } finally {
if (context != null) { if (context != null) {

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend; package com.commafeed.backend.startup;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@@ -13,24 +13,28 @@ import javax.ejb.Singleton;
import javax.ejb.Startup; import javax.ejb.Startup;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.commons.io.IOUtils; import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.dao.UserDAO; import org.apache.commons.io.IOUtils;
import com.commafeed.backend.dao.ApplicationSettingsDAO;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver; import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.UserService; import com.commafeed.backend.services.UserService;
import com.google.api.client.util.Maps; import com.google.common.collect.Maps;
/**
* Starting point of the application
*
*/
@Startup @Startup
@Singleton @Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN) @ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@Slf4j
public class StartupBean { public class StartupBean {
private static Logger log = LoggerFactory.getLogger(StartupBean.class);
public static final String USERNAME_ADMIN = "admin"; public static final String USERNAME_ADMIN = "admin";
public static final String USERNAME_DEMO = "demo"; public static final String USERNAME_DEMO = "demo";
@@ -38,7 +42,7 @@ public class StartupBean {
DatabaseUpdater databaseUpdater; DatabaseUpdater databaseUpdater;
@Inject @Inject
UserDAO userDAO; ApplicationSettingsDAO applicationSettingsDAO;
@Inject @Inject
UserService userService; UserService userService;
@@ -56,14 +60,19 @@ public class StartupBean {
private void init() { private void init() {
startupTime = System.currentTimeMillis(); startupTime = System.currentTimeMillis();
// update database schema
databaseUpdater.update(); databaseUpdater.update();
if (userDAO.getCount() == 0) { if (applicationSettingsDAO.getCount() == 0) {
// import initial data
initialData(); initialData();
} }
applicationSettingsService.applyLogLevel(); applicationSettingsService.applyLogLevel();
initSupportedLanguages(); initSupportedLanguages();
// start fetching feeds
taskGiver.start(); taskGiver.start();
} }
@@ -79,11 +88,13 @@ public class StartupBean {
IOUtils.closeQuietly(is); IOUtils.closeQuietly(is);
} }
for (Object key : props.keySet()) { for (Object key : props.keySet()) {
supportedLanguages.put(key.toString(), supportedLanguages.put(key.toString(), props.getProperty(key.toString()));
props.getProperty(key.toString()));
} }
} }
/**
* create default users
*/
private void initialData() { private void initialData() {
log.info("Populating database with default values"); log.info("Populating database with default values");
@@ -92,11 +103,8 @@ public class StartupBean {
applicationSettingsService.save(settings); applicationSettingsService.save(settings);
try { try {
userService.register(USERNAME_ADMIN, "admin", userService.register(USERNAME_ADMIN, "admin", "admin@commafeed.com", Arrays.asList(Role.ADMIN, Role.USER), true);
"admin@commafeed.com", userService.register(USERNAME_DEMO, "demo", "demo@commafeed.com", Arrays.asList(Role.USER), true);
Arrays.asList(Role.ADMIN, Role.USER), true);
userService.register(USERNAME_DEMO, "demo", "demo@commafeed.com",
Arrays.asList(Role.USER), true);
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} }

View File

@@ -0,0 +1,101 @@
package com.commafeed.frontend;
import java.io.OutputStream;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.apache.commons.lang.StringUtils;
import org.apache.wicket.util.io.IOUtils;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.request.MarkRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.core.Documentation;
import com.wordnik.swagger.core.DocumentationEndPoint;
import com.wordnik.swagger.core.SwaggerSpec;
import com.wordnik.swagger.core.util.TypeUtil;
import com.wordnik.swagger.jaxrs.HelpApi;
import com.wordnik.swagger.jaxrs.JaxrsApiReader;
@SupportedAnnotationTypes("com.wordnik.swagger.annotations.Api")
@SupportedOptions("outputDirectory")
public class APIGenerator extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
return processInternal(annotations, roundEnv);
} catch (Exception e) {
e.printStackTrace();
processingEnv.getMessager().printMessage(Kind.ERROR, e.getMessage());
}
return false;
}
private boolean processInternal(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws Exception {
JaxrsApiReader.setFormatString("");
TypeUtil.addAllowablePackage(Entries.class.getPackage().getName());
TypeUtil.addAllowablePackage(MarkRequest.class.getPackage().getName());
String apiVersion = "1.0";
String swaggerVersion = SwaggerSpec.version();
String basePath = "../rest";
Documentation doc = new Documentation();
for (Element element : roundEnv.getElementsAnnotatedWith(Api.class)) {
TypeElement type = (TypeElement) element;
String fqn = type.getQualifiedName().toString();
Class<?> resource = Class.forName(fqn);
Api api = resource.getAnnotation(Api.class);
String apiPath = api.value();
Documentation apiDoc = JaxrsApiReader.read(resource, apiVersion, swaggerVersion, basePath, apiPath);
apiDoc = new HelpApi(null).filterDocs(apiDoc, null, null, null, null);
apiDoc.setSwaggerVersion(swaggerVersion);
apiDoc.setApiVersion(apiVersion);
write(apiDoc.getResourcePath(), apiDoc, element);
doc.addApi(new DocumentationEndPoint(api.value(), api.description()));
}
doc.setSwaggerVersion(swaggerVersion);
doc.setApiVersion(apiVersion);
write(doc.getResourcePath(), doc, null);
return true;
}
private void write(String resourcePath, Object doc, Element element) throws Exception {
String fileName = StringUtils.defaultString(resourcePath, "resources");
fileName = StringUtils.removeStart(fileName, "/");
FileObject resource = null;
try {
resource = processingEnv.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, "", fileName, element);
} catch (Exception e) {
// already processed
}
if (resource != null) {
OutputStream os = resource.openOutputStream();
try {
IOUtils.write(new ObjectMapper().writeValueAsString(doc), os, "UTF-8");
} finally {
IOUtils.closeQuietly(os);
}
}
}
}

View File

@@ -1,14 +1,17 @@
package com.commafeed.frontend; package com.commafeed.frontend;
import java.util.ResourceBundle;
import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import javax.naming.InitialContext; import javax.naming.InitialContext;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.wicket.Application; import org.apache.wicket.Application;
import org.apache.wicket.Component; import org.apache.wicket.Component;
import org.apache.wicket.IRequestCycleProvider;
import org.apache.wicket.Page; import org.apache.wicket.Page;
import org.apache.wicket.RuntimeConfigurationType; import org.apache.wicket.RuntimeConfigurationType;
import org.apache.wicket.Session; import org.apache.wicket.Session;
@@ -20,6 +23,7 @@ import org.apache.wicket.authroles.authentication.AbstractAuthenticatedWebSessio
import org.apache.wicket.authroles.authentication.AuthenticatedWebApplication; import org.apache.wicket.authroles.authentication.AuthenticatedWebApplication;
import org.apache.wicket.authroles.authorization.strategies.role.Roles; import org.apache.wicket.authroles.authorization.strategies.role.Roles;
import org.apache.wicket.cdi.CdiConfiguration; import org.apache.wicket.cdi.CdiConfiguration;
import org.apache.wicket.cdi.CdiContainer;
import org.apache.wicket.cdi.ConversationPropagation; import org.apache.wicket.cdi.ConversationPropagation;
import org.apache.wicket.core.request.handler.PageProvider; import org.apache.wicket.core.request.handler.PageProvider;
import org.apache.wicket.core.request.handler.RenderPageRequestHandler; import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
@@ -31,13 +35,16 @@ import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.Request; import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response; import org.apache.wicket.request.Response;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.UrlRenderer;
import org.apache.wicket.request.component.IRequestableComponent; import org.apache.wicket.request.component.IRequestableComponent;
import org.apache.wicket.request.cycle.AbstractRequestCycleListener; import org.apache.wicket.request.cycle.AbstractRequestCycleListener;
import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.cycle.RequestCycleContext;
import org.apache.wicket.util.cookies.CookieUtils; import org.apache.wicket.util.cookies.CookieUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.services.ApplicationPropertiesService;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.frontend.pages.DemoLoginPage; import com.commafeed.frontend.pages.DemoLoginPage;
import com.commafeed.frontend.pages.HomePage; import com.commafeed.frontend.pages.HomePage;
import com.commafeed.frontend.pages.LogoutPage; import com.commafeed.frontend.pages.LogoutPage;
@@ -45,43 +52,38 @@ import com.commafeed.frontend.pages.NextUnreadRedirectPage;
import com.commafeed.frontend.pages.PasswordRecoveryCallbackPage; import com.commafeed.frontend.pages.PasswordRecoveryCallbackPage;
import com.commafeed.frontend.pages.PasswordRecoveryPage; import com.commafeed.frontend.pages.PasswordRecoveryPage;
import com.commafeed.frontend.pages.WelcomePage; import com.commafeed.frontend.pages.WelcomePage;
import com.commafeed.frontend.utils.WicketUtils;
import com.commafeed.frontend.utils.exception.DisplayExceptionPage; import com.commafeed.frontend.utils.exception.DisplayExceptionPage;
@Slf4j
public class CommaFeedApplication extends AuthenticatedWebApplication { public class CommaFeedApplication extends AuthenticatedWebApplication {
private static Logger log = LoggerFactory @Inject
.getLogger(CommaFeedApplication.class); ApplicationSettingsService applicationSettingsService;
public CommaFeedApplication() { public CommaFeedApplication() {
super(); super();
String prod = ResourceBundle.getBundle("application").getString( boolean prod = ApplicationPropertiesService.get().isProduction();
"production"); setConfigurationType(prod ? RuntimeConfigurationType.DEPLOYMENT : RuntimeConfigurationType.DEVELOPMENT);
setConfigurationType(Boolean.valueOf(prod) ? RuntimeConfigurationType.DEPLOYMENT
: RuntimeConfigurationType.DEVELOPMENT);
} }
@Override @Override
protected void init() { protected void init() {
super.init(); super.init();
setupInjection();
setupSecurity();
mountPage("welcome", WelcomePage.class); mountPage("welcome", WelcomePage.class);
mountPage("demo", DemoLoginPage.class); mountPage("demo", DemoLoginPage.class);
mountPage("recover", PasswordRecoveryPage.class); mountPage("recover", PasswordRecoveryPage.class);
mountPage("recover2", PasswordRecoveryCallbackPage.class); mountPage("recover2", PasswordRecoveryCallbackPage.class);
mountPage("logout", LogoutPage.class); mountPage("logout", LogoutPage.class);
mountPage("error", DisplayExceptionPage.class); mountPage("error", DisplayExceptionPage.class);
// mountPage("google/import/redirect", GoogleImportRedirectPage.class);
// mountPage(GoogleImportCallbackPage.PAGE_PATH,
// GoogleImportCallbackPage.class);
mountPage("next", NextUnreadRedirectPage.class); mountPage("next", NextUnreadRedirectPage.class);
setupInjection();
setupSecurity();
getMarkupSettings().setStripWicketTags(true); getMarkupSettings().setStripWicketTags(true);
getMarkupSettings().setCompressWhitespace(true); getMarkupSettings().setCompressWhitespace(true);
getMarkupSettings().setDefaultMarkupEncoding("UTF-8"); getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
@@ -89,8 +91,7 @@ public class CommaFeedApplication extends AuthenticatedWebApplication {
setHeaderResponseDecorator(new IHeaderResponseDecorator() { setHeaderResponseDecorator(new IHeaderResponseDecorator() {
@Override @Override
public IHeaderResponse decorate(IHeaderResponse response) { public IHeaderResponse decorate(IHeaderResponse response) {
return new JavaScriptFilteredIntoFooterHeaderResponse(response, return new JavaScriptFilteredIntoFooterHeaderResponse(response, "footer-container");
"footer-container");
} }
}); });
@@ -98,63 +99,77 @@ public class CommaFeedApplication extends AuthenticatedWebApplication {
@Override @Override
public IRequestHandler onException(RequestCycle cycle, Exception ex) { public IRequestHandler onException(RequestCycle cycle, Exception ex) {
AjaxRequestTarget target = cycle.find(AjaxRequestTarget.class); AjaxRequestTarget target = cycle.find(AjaxRequestTarget.class);
// redirect to the error page if ajax request, render error on // redirect to the error page if ajax request, render error on current page otherwise
// current page otherwise RedirectPolicy policy = target == null ? RedirectPolicy.NEVER_REDIRECT : RedirectPolicy.AUTO_REDIRECT;
RedirectPolicy policy = target == null ? RedirectPolicy.NEVER_REDIRECT return new RenderPageRequestHandler(new PageProvider(new DisplayExceptionPage(ex)), policy);
: RedirectPolicy.AUTO_REDIRECT; }
return new RenderPageRequestHandler(new PageProvider( });
new DisplayExceptionPage(ex)), policy);
setRequestCycleProvider(new IRequestCycleProvider() {
@Override
public RequestCycle get(RequestCycleContext context) {
return new RequestCycle(context) {
@Override
protected UrlRenderer newUrlRenderer() {
return new UrlRenderer(getRequest()) {
@Override
public String renderUrl(Url url) {
// override wicket's relative-to-absolute url conversion with what we know is the correct protocol
String publicUrl = applicationSettingsService.get().getPublicUrl();
if (StringUtils.isNotBlank(publicUrl)) {
Url parsed = Url.parse(publicUrl);
url.setProtocol(parsed.getProtocol());
url.setPort(parsed.getPort());
}
return super.renderUrl(url);
}
};
}
};
} }
}); });
} }
private void setupSecurity() { private void setupSecurity() {
getSecuritySettings().setAuthenticationStrategy( getSecuritySettings().setAuthenticationStrategy(new DefaultAuthenticationStrategy("LoggedIn") {
new DefaultAuthenticationStrategy("LoggedIn") {
private CookieUtils cookieUtils = null; private CookieUtils cookieUtils = null;
@Override @Override
protected CookieUtils getCookieUtils() { protected CookieUtils getCookieUtils() {
if (cookieUtils == null) { if (cookieUtils == null) {
cookieUtils = new CookieUtils() { cookieUtils = new CookieUtils() {
@Override @Override
protected void initializeCookie(Cookie cookie) { protected void initializeCookie(Cookie cookie) {
super.initializeCookie(cookie); super.initializeCookie(cookie);
cookie.setHttpOnly(true); cookie.setHttpOnly(true);
}
};
} }
return cookieUtils; };
} }
}); return cookieUtils;
getSecuritySettings().setAuthorizationStrategy( }
new IAuthorizationStrategy() { });
getSecuritySettings().setAuthorizationStrategy(new IAuthorizationStrategy() {
@Override @Override
public <T extends IRequestableComponent> boolean isInstantiationAuthorized( public <T extends IRequestableComponent> boolean isInstantiationAuthorized(Class<T> componentClass) {
Class<T> componentClass) { boolean authorized = true;
boolean authorized = true;
boolean restricted = componentClass boolean restricted = componentClass.isAnnotationPresent(SecurityCheck.class);
.isAnnotationPresent(SecurityCheck.class); if (restricted) {
if (restricted) { SecurityCheck annotation = componentClass.getAnnotation(SecurityCheck.class);
SecurityCheck annotation = componentClass Roles roles = CommaFeedSession.get().getRoles();
.getAnnotation(SecurityCheck.class); authorized = roles.hasAnyRole(new Roles(annotation.value().name()));
Roles roles = CommaFeedSession.get().getRoles(); }
authorized = roles.hasAnyRole(new Roles(annotation return authorized;
.value().name())); }
}
return authorized;
}
@Override @Override
public boolean isActionAuthorized(Component component, public boolean isActionAuthorized(Component component, Action action) {
Action action) { return true;
return true; }
} });
});
} }
@Override @Override
@@ -164,15 +179,21 @@ public class CommaFeedApplication extends AuthenticatedWebApplication {
protected void setupInjection() { protected void setupInjection() {
try { try {
BeanManager beanManager = (BeanManager) new InitialContext() BeanManager beanManager = (BeanManager) new InitialContext().lookup("java:comp/BeanManager");
.lookup("java:comp/BeanManager"); CdiContainer container = new CdiConfiguration(beanManager).setPropagation(ConversationPropagation.NONE).configure(this);
new CdiConfiguration(beanManager).setPropagation( container.getNonContextualManager().inject(this);
ConversationPropagation.NONE).configure(this);
} catch (NamingException e) { } catch (NamingException e) {
log.warn("Could not locate bean manager. CDI is disabled."); log.warn("Could not locate bean manager. CDI is disabled.");
} }
} }
@Override
public void restartResponseAtSignInPage() {
// preserve https if it was set
RequestCycle.get().getUrlRenderer().setBaseUrl(Url.parse(WicketUtils.getClientFullUrl()));
super.restartResponseAtSignInPage();
}
@Override @Override
public Session newSession(Request request, Response response) { public Session newSession(Request request, Response response) {
return new CommaFeedSession(request); return new CommaFeedSession(request);

View File

@@ -2,32 +2,27 @@ package com.commafeed.frontend;
import java.util.Set; import java.util.Set;
import javax.inject.Inject; import lombok.Getter;
import org.apache.wicket.Session; import org.apache.wicket.Session;
import org.apache.wicket.authroles.authentication.AuthenticatedWebSession; import org.apache.wicket.authroles.authentication.AuthenticatedWebSession;
import org.apache.wicket.authroles.authorization.strategies.role.Roles; import org.apache.wicket.authroles.authorization.strategies.role.Roles;
import org.apache.wicket.request.Request; import org.apache.wicket.request.Request;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.services.UserService;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
public class CommaFeedSession extends AuthenticatedWebSession { public class CommaFeedSession extends AuthenticatedWebSession {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Inject
UserService userService;
@Inject
UserRoleDAO userRoleDAO;
private User user; private User user;
private Roles roles = new Roles(); private Roles roles = new Roles();
@Getter(lazy = true)
private final CommaFeedSessionServices services = newServices();
public CommaFeedSession(Request request) { public CommaFeedSession(Request request) {
super(request); super(request);
} }
@@ -47,7 +42,7 @@ public class CommaFeedSession extends AuthenticatedWebSession {
@Override @Override
public boolean authenticate(String userName, String password) { public boolean authenticate(String userName, String password) {
User user = userService.login(userName, password); User user = getServices().getUserService().login(userName, password);
setUser(user); setUser(user);
return user != null; return user != null;
} }
@@ -59,7 +54,7 @@ public class CommaFeedSession extends AuthenticatedWebSession {
} else { } else {
Set<String> roleSet = Sets.newHashSet(); Set<String> roleSet = Sets.newHashSet();
for (Role role : userRoleDAO.findRoles(user)) { for (Role role : getServices().getUserRoleDAO().findRoles(user)) {
roleSet.add(role.name()); roleSet.add(role.name());
} }
this.user = user; this.user = user;
@@ -67,4 +62,8 @@ public class CommaFeedSession extends AuthenticatedWebSession {
} }
} }
private CommaFeedSessionServices newServices() {
return new CommaFeedSessionServices();
}
} }

View File

@@ -0,0 +1,37 @@
package com.commafeed.frontend;
import javax.inject.Inject;
import org.apache.wicket.Component;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.services.UserService;
// extend Component in order to benefit from injection
public class CommaFeedSessionServices extends Component {
private static final long serialVersionUID = 1L;
@Inject
UserService userService;
@Inject
UserRoleDAO userRoleDAO;
public CommaFeedSessionServices() {
super("services");
}
public UserService getUserService() {
return userService;
}
public UserRoleDAO getUserRoleDAO() {
return userRoleDAO;
}
@Override
protected void onRender() {
// do nothing
}
}

View File

@@ -0,0 +1,53 @@
package com.commafeed.frontend;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
@WebFilter(urlPatterns = "/*")
public class InterceptingFilter implements Filter {
private static final String HEADER_CORS = "Access-Control-Allow-Origin";
private static final String HEADER_CORS_VALUE = "*";
private static final String HEADER_CORS_METHODS = "Access-Control-Allow-Methods";
private static final String HEADER_CORS_METHODS_VALUE = "POST, GET, OPTIONS";
private static final String HEADER_CORS_MAXAGE = "Access-Control-Max-Age";
private static final String HEADER_CORS_MAXAGE_VALUE = "2592000";
private static final String HEADER_CORS_ALLOW_HEADERS = "Access-Control-Allow-Headers";
private static final String HEADER_CORS_ALLOW_HEADERS_VALUE = "Authorization";
private static final String HEADER_CORS_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
private static final String HEADER_CORS_ALLOW_CREDENTIALS_VALUE = "true";
private static final String HEADER_X_UA_COMPATIBLE = "X-UA-Compatible";
private static final String HEADER_X_UA_COMPATIBLE_VALUE = "IE=Edge,chrome=1";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse) response;
resp.addHeader(HEADER_CORS, HEADER_CORS_VALUE);
resp.addHeader(HEADER_CORS_METHODS, HEADER_CORS_METHODS_VALUE);
resp.addHeader(HEADER_CORS_MAXAGE, HEADER_CORS_MAXAGE_VALUE);
resp.addHeader(HEADER_CORS_ALLOW_HEADERS, HEADER_CORS_ALLOW_HEADERS_VALUE);
resp.addHeader(HEADER_CORS_ALLOW_CREDENTIALS, HEADER_CORS_ALLOW_CREDENTIALS_VALUE);
resp.addHeader(HEADER_X_UA_COMPATIBLE, HEADER_X_UA_COMPATIBLE_VALUE);
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}

View File

@@ -3,18 +3,15 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Entry details") @ApiClass("Entry details")
@Data
public class Category implements Serializable { public class Category implements Serializable {
@ApiProperty("category id") @ApiProperty("category id")
@@ -37,61 +34,4 @@ public class Category implements Serializable {
@ApiProperty("position of the category in the list") @ApiProperty("position of the category in the list")
private Integer position; private Integer position;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Category> getChildren() {
return children;
}
public void setChildren(List<Category> children) {
this.children = children;
}
public List<Subscription> getFeeds() {
return feeds;
}
public void setFeeds(List<Subscription> feeds) {
this.feeds = feeds;
}
public boolean isExpanded() {
return expanded;
}
public void setExpanded(boolean expanded) {
this.expanded = expanded;
}
public Integer getPosition() {
return position;
}
public void setPosition(Integer position) {
this.position = position;
}
} }

View File

@@ -3,18 +3,15 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("List of entries with some metadata") @ApiClass("List of entries with some metadata")
@Data
public class Entries implements Serializable { public class Entries implements Serializable {
@ApiProperty("name of the feed or the category requested") @ApiProperty("name of the feed or the category requested")
@@ -35,63 +32,13 @@ public class Entries implements Serializable {
@ApiProperty("if the query has more elements") @ApiProperty("if the query has more elements")
private boolean hasMore; private boolean hasMore;
@ApiProperty("the requested offset")
private int offset;
@ApiProperty("the requested limit")
private int limit;
@ApiProperty("list of entries") @ApiProperty("list of entries")
private List<Entry> entries = Lists.newArrayList(); private List<Entry> entries = Lists.newArrayList();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Entry> getEntries() {
return entries;
}
public void setEntries(List<Entry> entries) {
this.entries = entries;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getErrorCount() {
return errorCount;
}
public void setErrorCount(int errorCount) {
this.errorCount = errorCount;
}
public String getFeedLink() {
return feedLink;
}
public void setFeedLink(String feedLink) {
this.feedLink = feedLink;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public boolean isHasMore() {
return hasMore;
}
public void setHasMore(boolean hasMore) {
this.hasMore = hasMore;
}
} }

View File

@@ -4,12 +4,11 @@ import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.commafeed.backend.feeds.FeedUtils; import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.sun.syndication.feed.synd.SyndContentImpl; import com.sun.syndication.feed.synd.SyndContentImpl;
@@ -19,40 +18,40 @@ import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Entry details") @ApiClass("Entry details")
@Data
public class Entry implements Serializable { public class Entry implements Serializable {
public static Entry build(FeedEntryStatus status, String publicUrl, public static Entry build(FeedEntryStatus status, String publicUrl, boolean proxyImages) {
boolean proxyImages) {
Entry entry = new Entry(); Entry entry = new Entry();
FeedEntry feedEntry = status.getEntry(); FeedEntry feedEntry = status.getEntry();
FeedSubscription sub = status.getSubscription(); FeedSubscription sub = status.getSubscription();
FeedEntryContent content = feedEntry.getContent();
entry.setRead(status.isRead());
entry.setStarred(status.isStarred());
entry.setId(String.valueOf(feedEntry.getId())); entry.setId(String.valueOf(feedEntry.getId()));
entry.setGuid(feedEntry.getGuid()); entry.setGuid(feedEntry.getGuid());
entry.setTitle(feedEntry.getContent().getTitle()); entry.setRead(status.isRead());
entry.setContent(FeedUtils.proxyImages(feedEntry.getContent() entry.setStarred(status.isStarred());
.getContent(), publicUrl, proxyImages)); entry.setMarkable(status.isMarkable());
entry.setRtl(FeedUtils.isRTL(feedEntry));
entry.setAuthor(feedEntry.getAuthor());
entry.setEnclosureUrl(feedEntry.getContent().getEnclosureUrl());
entry.setEnclosureType(feedEntry.getContent().getEnclosureType());
entry.setDate(feedEntry.getUpdated()); entry.setDate(feedEntry.getUpdated());
entry.setInsertedDate(feedEntry.getInserted()); entry.setInsertedDate(feedEntry.getInserted());
entry.setUrl(feedEntry.getUrl()); entry.setUrl(feedEntry.getUrl());
entry.setFeedName(sub.getTitle()); entry.setFeedName(sub.getTitle());
entry.setFeedId(String.valueOf(sub.getId())); entry.setFeedId(String.valueOf(sub.getId()));
entry.setFeedUrl(sub.getFeed().getUrl()); entry.setFeedUrl(sub.getFeed().getUrl());
entry.setFeedLink(sub.getFeed().getLink()); entry.setFeedLink(sub.getFeed().getLink());
entry.setIconUrl(FeedUtils.getFaviconUrl(sub, publicUrl)); entry.setIconUrl(FeedUtils.getFaviconUrl(sub, publicUrl));
if (content != null) {
entry.setRtl(FeedUtils.isRTL(feedEntry));
entry.setTitle(content.getTitle());
entry.setContent(FeedUtils.proxyImages(content.getContent(), publicUrl, proxyImages));
entry.setAuthor(content.getAuthor());
entry.setEnclosureUrl(content.getEnclosureUrl());
entry.setEnclosureType(content.getEnclosureType());
}
return entry; return entry;
} }
@@ -124,148 +123,6 @@ public class Entry implements Serializable {
@ApiProperty("starred status") @ApiProperty("starred status")
private boolean starred; private boolean starred;
public String getId() { @ApiProperty("wether the entry is still markable (old entry statuses are discarded)")
return id; private boolean markable;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getFeedId() {
return feedId;
}
public void setFeedId(String feedId) {
this.feedId = feedId;
}
public String getFeedName() {
return feedName;
}
public void setFeedName(String feedName) {
this.feedName = feedName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public boolean isRead() {
return read;
}
public void setRead(boolean read) {
this.read = read;
}
public boolean isStarred() {
return starred;
}
public void setStarred(boolean starred) {
this.starred = starred;
}
public String getFeedUrl() {
return feedUrl;
}
public void setFeedUrl(String feedUrl) {
this.feedUrl = feedUrl;
}
public String getEnclosureUrl() {
return enclosureUrl;
}
public void setEnclosureUrl(String enclosureUrl) {
this.enclosureUrl = enclosureUrl;
}
public String getEnclosureType() {
return enclosureType;
}
public void setEnclosureType(String enclosureType) {
this.enclosureType = enclosureType;
}
public String getGuid() {
return guid;
}
public void setGuid(String guid) {
this.guid = guid;
}
public String getFeedLink() {
return feedLink;
}
public void setFeedLink(String feedLink) {
this.feedLink = feedLink;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIconUrl() {
return iconUrl;
}
public void setIconUrl(String iconUrl) {
this.iconUrl = iconUrl;
}
public boolean isRtl() {
return rtl;
}
public void setRtl(boolean rtl) {
this.rtl = rtl;
}
public Date getInsertedDate() {
return insertedDate;
}
public void setInsertedDate(Date insertedDate) {
this.insertedDate = insertedDate;
}
} }

View File

@@ -0,0 +1,25 @@
package com.commafeed.frontend.model;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
import com.commafeed.backend.model.Feed;
import com.google.common.collect.Lists;
import com.wordnik.swagger.annotations.ApiClass;
@ApiClass("Feed count")
@Data
public class FeedCount implements Serializable {
private static final long serialVersionUID = 1L;
private String value;
private List<Feed> feeds = Lists.newArrayList();;
public FeedCount(String value) {
this.value = value;
}
}

View File

@@ -2,35 +2,16 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Feed details") @ApiClass("Feed details")
@Data
public class FeedInfo implements Serializable { public class FeedInfo implements Serializable {
private String url; private String url;
private String title; private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
} }

View File

@@ -3,17 +3,14 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Map; import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.google.api.client.util.Maps; import com.google.common.collect.Maps;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Server infos") @ApiClass("Server infos")
@Data
public class ServerInfo implements Serializable { public class ServerInfo implements Serializable {
private String announcement; private String announcement;
@@ -21,36 +18,4 @@ public class ServerInfo implements Serializable {
private String gitCommit; private String gitCommit;
private Map<String, String> supportedLanguages = Maps.newHashMap(); private Map<String, String> supportedLanguages = Maps.newHashMap();
public String getAnnouncement() {
return announcement;
}
public void setAnnouncement(String announcement) {
this.announcement = announcement;
}
public Map<String, String> getSupportedLanguages() {
return supportedLanguages;
}
public void setSupportedLanguages(Map<String, String> supportedLanguages) {
this.supportedLanguages = supportedLanguages;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getGitCommit() {
return gitCommit;
}
public void setGitCommit(String gitCommit) {
this.gitCommit = gitCommit;
}
} }

View File

@@ -2,17 +2,14 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("User settings") @ApiClass("User settings")
@Data
public class Settings implements Serializable { public class Settings implements Serializable {
@ApiProperty(value = "user's preferred language, english if none") @ApiProperty(value = "user's preferred language, english if none")
@@ -42,76 +39,4 @@ public class Settings implements Serializable {
@ApiProperty(value = "user's custom css for the website") @ApiProperty(value = "user's custom css for the website")
private String customCss; private String customCss;
public String getReadingMode() {
return readingMode;
}
public void setReadingMode(String readingMode) {
this.readingMode = readingMode;
}
public String getCustomCss() {
return customCss;
}
public void setCustomCss(String customCss) {
this.customCss = customCss;
}
public String getReadingOrder() {
return readingOrder;
}
public void setReadingOrder(String readingOrder) {
this.readingOrder = readingOrder;
}
public boolean isShowRead() {
return showRead;
}
public void setShowRead(boolean showRead) {
this.showRead = showRead;
}
public boolean isSocialButtons() {
return socialButtons;
}
public void setSocialButtons(boolean socialButtons) {
this.socialButtons = socialButtons;
}
public String getViewMode() {
return viewMode;
}
public void setViewMode(String viewMode) {
this.viewMode = viewMode;
}
public boolean isScrollMarks() {
return scrollMarks;
}
public void setScrollMarks(boolean scrollMarks) {
this.scrollMarks = scrollMarks;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
}
} }

View File

@@ -3,9 +3,7 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.commafeed.backend.feeds.FeedUtils; import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
@@ -15,13 +13,11 @@ import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("User information") @ApiClass("User information")
@Data
public class Subscription implements Serializable { public class Subscription implements Serializable {
public static Subscription build(FeedSubscription subscription, public static Subscription build(FeedSubscription subscription, String publicUrl, UnreadCount unreadCount) {
String publicUrl, long unreadCount) {
Date now = new Date(); Date now = new Date();
FeedCategory category = subscription.getCategory(); FeedCategory category = subscription.getCategory();
Feed feed = subscription.getFeed(); Feed feed = subscription.getFeed();
@@ -35,12 +31,10 @@ public class Subscription implements Serializable {
sub.setFeedLink(feed.getLink()); sub.setFeedLink(feed.getLink());
sub.setIconUrl(FeedUtils.getFaviconUrl(subscription, publicUrl)); sub.setIconUrl(FeedUtils.getFaviconUrl(subscription, publicUrl));
sub.setLastRefresh(feed.getLastUpdated()); sub.setLastRefresh(feed.getLastUpdated());
sub.setNextRefresh((feed.getDisabledUntil() != null && feed sub.setNextRefresh((feed.getDisabledUntil() != null && feed.getDisabledUntil().before(now)) ? null : feed.getDisabledUntil());
.getDisabledUntil().before(now)) ? null : feed sub.setUnread(unreadCount.getUnreadCount());
.getDisabledUntil()); sub.setNewestItemTime(unreadCount.getNewestItemTime());
sub.setUnread(unreadCount); sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
sub.setCategoryId(category == null ? null : String.valueOf(category
.getId()));
return sub; return sub;
} }
@@ -80,100 +74,7 @@ public class Subscription implements Serializable {
@ApiProperty("position of the subscription's in the list") @ApiProperty("position of the subscription's in the list")
private Integer position; private Integer position;
public Long getId() { @ApiProperty("date of the newest item")
return id; private Date newestItemTime;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getUnread() {
return unread;
}
public void setUnread(long unread) {
this.unread = unread;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getFeedUrl() {
return feedUrl;
}
public void setFeedUrl(String feedUrl) {
this.feedUrl = feedUrl;
}
public int getErrorCount() {
return errorCount;
}
public void setErrorCount(int errorCount) {
this.errorCount = errorCount;
}
public String getFeedLink() {
return feedLink;
}
public void setFeedLink(String feedLink) {
this.feedLink = feedLink;
}
public Date getLastRefresh() {
return lastRefresh;
}
public void setLastRefresh(Date lastRefresh) {
this.lastRefresh = lastRefresh;
}
public String getCategoryId() {
return categoryId;
}
public void setCategoryId(String categoryId) {
this.categoryId = categoryId;
}
public Date getNextRefresh() {
return nextRefresh;
}
public void setNextRefresh(Date nextRefresh) {
this.nextRefresh = nextRefresh;
}
public String getIconUrl() {
return iconUrl;
}
public void setIconUrl(String iconUrl) {
this.iconUrl = iconUrl;
}
public Integer getPosition() {
return position;
}
public void setPosition(Integer position) {
this.position = position;
}
} }

View File

@@ -1,45 +1,29 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Unread count") @ApiClass("Unread count")
@Data
public class UnreadCount implements Serializable { public class UnreadCount implements Serializable {
private long feedId; private long feedId;
private long unreadCount; private long unreadCount;
private Date newestItemTime;
public UnreadCount() { public UnreadCount() {
} }
public UnreadCount(long feedId, long unreadCount) { public UnreadCount(long feedId, long unreadCount, Date newestItemTime) {
this.feedId = feedId; this.feedId = feedId;
this.unreadCount = unreadCount; this.unreadCount = unreadCount;
} this.newestItemTime = newestItemTime;
public long getFeedId() {
return feedId;
}
public void setFeedId(long feedId) {
this.feedId = feedId;
}
public long getUnreadCount() {
return unreadCount;
}
public void setUnreadCount(long unreadCount) {
this.unreadCount = unreadCount;
} }
} }

View File

@@ -3,17 +3,14 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("User information") @ApiClass("User information")
@Data
public class UserModel implements Serializable { public class UserModel implements Serializable {
@ApiProperty(value = "user id", required = true) @ApiProperty(value = "user id", required = true)
@@ -43,76 +40,4 @@ public class UserModel implements Serializable {
@ApiProperty(value = "user is admin") @ApiProperty(value = "user is admin")
private boolean admin; private boolean admin;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isAdmin() {
return admin;
}
public void setAdmin(boolean admin) {
this.admin = admin;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getLastLogin() {
return lastLogin;
}
public void setLastLogin(Date lastLogin) {
this.lastLogin = lastLogin;
}
} }

View File

@@ -2,17 +2,14 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Add Category Request") @ApiClass("Add Category Request")
@Data
public class AddCategoryRequest implements Serializable { public class AddCategoryRequest implements Serializable {
@ApiProperty(value = "name", required = true) @ApiProperty(value = "name", required = true)
@@ -21,20 +18,4 @@ public class AddCategoryRequest implements Serializable {
@ApiProperty(value = "parent category id, if any") @ApiProperty(value = "parent category id, if any")
private String parentId; private String parentId;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
} }

View File

@@ -2,17 +2,14 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Category modification request") @ApiClass("Category modification request")
@Data
public class CategoryModificationRequest implements Serializable { public class CategoryModificationRequest implements Serializable {
@ApiProperty(value = "id", required = true) @ApiProperty(value = "id", required = true)
@@ -27,36 +24,4 @@ public class CategoryModificationRequest implements Serializable {
@ApiProperty(value = "new display position, null if not changed") @ApiProperty(value = "new display position, null if not changed")
private Integer position; private Integer position;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public Integer getPosition() {
return position;
}
public void setPosition(Integer position) {
this.position = position;
}
} }

View File

@@ -2,17 +2,14 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Mark Request") @ApiClass("Mark Request")
@Data
public class CollapseRequest implements Serializable { public class CollapseRequest implements Serializable {
@ApiProperty(value = "category id", required = true) @ApiProperty(value = "category id", required = true)
@@ -21,20 +18,4 @@ public class CollapseRequest implements Serializable {
@ApiProperty(value = "collapse", required = true) @ApiProperty(value = "collapse", required = true)
private boolean collapse; private boolean collapse;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public boolean isCollapse() {
return collapse;
}
public void setCollapse(boolean collapse) {
this.collapse = collapse;
}
} }

View File

@@ -2,30 +2,17 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Feed information request") @ApiClass("Feed information request")
@Data
public class FeedInfoRequest implements Serializable { public class FeedInfoRequest implements Serializable {
@ApiProperty(value = "feed url", required = true) @ApiProperty(value = "feed url", required = true)
private String url; private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
} }

View File

@@ -3,17 +3,14 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Feed merge Request") @ApiClass("Feed merge Request")
@Data
public class FeedMergeRequest implements Serializable { public class FeedMergeRequest implements Serializable {
@ApiProperty(value = "merge into this feed", required = true) @ApiProperty(value = "merge into this feed", required = true)
@@ -22,20 +19,4 @@ public class FeedMergeRequest implements Serializable {
@ApiProperty(value = "id of the feeds to merge", required = true) @ApiProperty(value = "id of the feeds to merge", required = true)
private List<Long> feedIds; private List<Long> feedIds;
public Long getIntoFeedId() {
return intoFeedId;
}
public void setIntoFeedId(Long intoFeedId) {
this.intoFeedId = intoFeedId;
}
public List<Long> getFeedIds() {
return feedIds;
}
public void setFeedIds(List<Long> feedIds) {
this.feedIds = feedIds;
}
} }

View File

@@ -2,17 +2,14 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Feed modification request") @ApiClass("Feed modification request")
@Data
public class FeedModificationRequest implements Serializable { public class FeedModificationRequest implements Serializable {
@ApiProperty(value = "id", required = true) @ApiProperty(value = "id", required = true)
@@ -27,36 +24,4 @@ public class FeedModificationRequest implements Serializable {
@ApiProperty(value = "new display position, null if not changed") @ApiProperty(value = "new display position, null if not changed")
private Integer position; private Integer position;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCategoryId() {
return categoryId;
}
public void setCategoryId(String categoryId) {
this.categoryId = categoryId;
}
public Integer getPosition() {
return position;
}
public void setPosition(Integer position) {
this.position = position;
}
} }

View File

@@ -2,28 +2,17 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass @ApiClass
@Data
public class IDRequest implements Serializable { public class IDRequest implements Serializable {
@ApiProperty @ApiProperty
private Long id; private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
} }

View File

@@ -1,62 +1,30 @@
package com.commafeed.frontend.model.request; package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Mark Request") @ApiClass("Mark Request")
@Data
public class MarkRequest implements Serializable { public class MarkRequest implements Serializable {
@ApiProperty(value = "entry id, category id, 'all' or 'starred'", required = true) @ApiProperty(value = "entry id, category id, 'all' or 'starred'", required = true)
private String id; private String id;
@ApiProperty(value = "feed id, only required when marking an entry")
private Long feedId;
@ApiProperty(value = "mark as read or unread") @ApiProperty(value = "mark as read or unread")
private boolean read; private boolean read;
@ApiProperty(value = "only entries older than this, pass the timestamp you got from the entry list to prevent marking an entry that was not retrieved", required = false) @ApiProperty(
value = "only entries older than this, pass the timestamp you got from the entry list to prevent marking an entry that was not retrieved",
required = false)
private Long olderThan; private Long olderThan;
public String getId() { @ApiProperty(value = "if marking a category or 'all', exclude those subscriptions from the marking", required = false)
return id; private List<Long> excludedSubscriptions;
}
public void setId(String id) {
this.id = id;
}
public boolean isRead() {
return read;
}
public void setRead(boolean read) {
this.read = read;
}
public Long getOlderThan() {
return olderThan;
}
public void setOlderThan(Long olderThan) {
this.olderThan = olderThan;
}
public Long getFeedId() {
return feedId;
}
public void setFeedId(Long feedId) {
this.feedId = feedId;
}
} }

View File

@@ -3,28 +3,17 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Multiple Mark Request") @ApiClass("Multiple Mark Request")
@Data
public class MultipleMarkRequest implements Serializable { public class MultipleMarkRequest implements Serializable {
@ApiProperty(value = "list of mark requests", required = true) @ApiProperty(value = "list of mark requests", required = true)
private List<MarkRequest> requests; private List<MarkRequest> requests;
public List<MarkRequest> getRequests() {
return requests;
}
public void setRequests(List<MarkRequest> requests) {
this.requests = requests;
}
} }

View File

@@ -1,16 +1,16 @@
package com.commafeed.frontend.model.request; package com.commafeed.frontend.model.request;
import javax.xml.bind.annotation.XmlAccessType; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement; import lombok.Data;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@XmlRootElement @SuppressWarnings("serial")
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Profile modification request") @ApiClass("Profile modification request")
public class ProfileModificationRequest { @Data
public class ProfileModificationRequest implements Serializable {
@ApiProperty(value = "changes email of the user, if specified") @ApiProperty(value = "changes email of the user, if specified")
private String email; private String email;
@@ -21,28 +21,4 @@ public class ProfileModificationRequest {
@ApiProperty(value = "generate a new api key") @ApiProperty(value = "generate a new api key")
private boolean newApiKey; private boolean newApiKey;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isNewApiKey() {
return newApiKey;
}
public void setNewApiKey(boolean newApiKey) {
this.newApiKey = newApiKey;
}
} }

View File

@@ -2,15 +2,12 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement @Data
@XmlAccessorType(XmlAccessType.FIELD)
public class RegistrationRequest implements Serializable { public class RegistrationRequest implements Serializable {
@ApiProperty(value = "username, between 3 and 32 characters", required = true) @ApiProperty(value = "username, between 3 and 32 characters", required = true)
@@ -22,27 +19,4 @@ public class RegistrationRequest implements Serializable {
@ApiProperty(value = "email address for password recovery", required = true) @ApiProperty(value = "email address for password recovery", required = true)
private String email; private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
} }

View File

@@ -2,17 +2,14 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Star Request") @ApiClass("Star Request")
@Data
public class StarRequest implements Serializable { public class StarRequest implements Serializable {
@ApiProperty(value = "id", required = true) @ApiProperty(value = "id", required = true)
@@ -24,28 +21,4 @@ public class StarRequest implements Serializable {
@ApiProperty(value = "starred or not") @ApiProperty(value = "starred or not")
private boolean starred; private boolean starred;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public boolean isStarred() {
return starred;
}
public void setStarred(boolean starred) {
this.starred = starred;
}
public Long getFeedId() {
return feedId;
}
public void setFeedId(Long feedId) {
this.feedId = feedId;
}
} }

View File

@@ -2,17 +2,14 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType; import lombok.Data;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass; import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty; import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Subscription request") @ApiClass("Subscription request")
@Data
public class SubscribeRequest implements Serializable { public class SubscribeRequest implements Serializable {
@ApiProperty(value = "url of the feed", required = true) @ApiProperty(value = "url of the feed", required = true)
@@ -24,28 +21,4 @@ public class SubscribeRequest implements Serializable {
@ApiProperty(value = "id of the user category to place the feed in") @ApiProperty(value = "id of the user category to place the feed in")
private String categoryId; private String categoryId;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getCategoryId() {
return categoryId;
}
public void setCategoryId(String categoryId) {
this.categoryId = categoryId;
}
} }

View File

@@ -15,7 +15,6 @@ import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.WebPage;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
@@ -29,9 +28,10 @@ import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.MailService; import com.commafeed.backend.services.MailService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.CommaFeedSession; import com.commafeed.frontend.CommaFeedSession;
import com.commafeed.frontend.utils.WicketUtils; import com.commafeed.frontend.utils.WicketUtils;
import com.google.api.client.util.Maps; import com.google.common.collect.Maps;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public abstract class BasePage extends WebPage { public abstract class BasePage extends WebPage {
@@ -79,15 +79,12 @@ public abstract class BasePage extends WebPage {
if (user != null) { if (user != null) {
UserSettings settings = userSettingsDAO.findByUser(user); UserSettings settings = userSettingsDAO.findByUser(user);
if (settings != null) { if (settings != null) {
lang = settings.getLanguage() == null ? "en" : settings lang = settings.getLanguage() == null ? "en" : settings.getLanguage();
.getLanguage(); theme = settings.getTheme() == null ? "default" : settings.getTheme();
theme = settings.getTheme() == null ? "default" : settings
.getTheme();
} }
} }
add(new TransparentWebMarkupContainer("html").setMarkupId( add(new TransparentWebMarkupContainer("html").setMarkupId("theme-" + theme).add(new AttributeModifier("lang", lang)));
"theme-" + theme).add(new AttributeModifier("lang", lang)));
settings = applicationSettingsService.get(); settings = applicationSettingsService.get();
add(new HeaderResponseContainer("footer-container", "footer-container")); add(new HeaderResponseContainer("footer-container", "footer-container"));
@@ -107,8 +104,7 @@ public abstract class BasePage extends WebPage {
if (getApplication().getConfigurationType() == RuntimeConfigurationType.DEPLOYMENT) { if (getApplication().getConfigurationType() == RuntimeConfigurationType.DEPLOYMENT) {
long startupTime = startupBean.getStartupTime(); long startupTime = startupBean.getStartupTime();
String suffix = "?" + startupTime; String suffix = "?" + startupTime;
response.render(JavaScriptHeaderItem.forUrl("static/all.js" response.render(JavaScriptHeaderItem.forUrl("static/all.js" + suffix));
+ suffix));
response.render(CssHeaderItem.forUrl("static/all.css" + suffix)); response.render(CssHeaderItem.forUrl("static/all.css" + suffix));
} else { } else {
response.render(JavaScriptHeaderItem.forUrl("wro/lib.js")); response.render(JavaScriptHeaderItem.forUrl("wro/lib.js"));

View File

@@ -4,8 +4,8 @@ import javax.inject.Inject;
import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.WebPage;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.services.UserService; import com.commafeed.backend.services.UserService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.CommaFeedSession; import com.commafeed.frontend.CommaFeedSession;
public class DemoLoginPage extends WebPage { public class DemoLoginPage extends WebPage {
@@ -16,8 +16,7 @@ public class DemoLoginPage extends WebPage {
UserService userService; UserService userService;
public DemoLoginPage() { public DemoLoginPage() {
CommaFeedSession.get().authenticate(StartupBean.USERNAME_DEMO, CommaFeedSession.get().authenticate(StartupBean.USERNAME_DEMO, StartupBean.USERNAME_DEMO);
StartupBean.USERNAME_DEMO);
setResponsePage(getApplication().getHomePage()); setResponsePage(getApplication().getHomePage());
} }
} }

View File

@@ -1,109 +0,0 @@
package com.commafeed.frontend.pages;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.feeds.OPMLImporter;
import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.User;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.frontend.CommaFeedSession;
import com.commafeed.frontend.utils.WicketUtils;
import com.commafeed.frontend.utils.exception.DisplayException;
import com.google.api.client.auth.oauth2.AuthorizationCodeResponseUrl;
import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest;
import com.google.api.client.auth.oauth2.BearerToken;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
@SuppressWarnings("serial")
public class GoogleImportCallbackPage extends WebPage {
private static final String TOKEN_URL = "https://accounts.google.com/o/oauth2/token";
private static final String EXPORT_URL = "https://www.google.com/reader/subscriptions/export";
public static final String PAGE_PATH = "google/import/callback";
@Inject
ApplicationSettingsService applicationSettingsService;
@Inject
OPMLImporter importer;
@Inject
UserDAO userDAO;
public static String getCallbackUrl(String publicUrl) {
return FeedUtils.removeTrailingSlash(publicUrl) + "/" + PAGE_PATH;
}
public GoogleImportCallbackPage(PageParameters params) {
HttpServletRequest request = WicketUtils.getHttpServletRequest();
StringBuffer urlBuffer = request.getRequestURL();
if (request.getQueryString() != null) {
urlBuffer.append('?').append(request.getQueryString());
}
AuthorizationCodeResponseUrl responseUrl = new AuthorizationCodeResponseUrl(
urlBuffer.toString());
String code = responseUrl.getCode();
if (responseUrl.getError() != null) {
// user declined
throw new RestartResponseException(getApplication().getHomePage());
} else if (code == null) {
throw new DisplayException("Missing authorization code");
} else {
ApplicationSettings settings = applicationSettingsService.get();
String redirectUri = getCallbackUrl(settings.getPublicUrl());
String clientId = settings.getGoogleClientId();
String clientSecret = settings.getGoogleClientSecret();
HttpTransport httpTransport = new NetHttpTransport();
JacksonFactory jsonFactory = new JacksonFactory();
AuthorizationCodeTokenRequest tokenRequest = new AuthorizationCodeTokenRequest(
httpTransport, jsonFactory, new GenericUrl(TOKEN_URL), code);
tokenRequest.setRedirectUri(redirectUri);
tokenRequest.put("client_id", clientId);
tokenRequest.put("client_secret", clientSecret);
tokenRequest.setGrantType("authorization_code");
try {
// potential fix for invalid_grant error, happens if local
// system time is ahead of google servers time
Thread.sleep(1000);
TokenResponse tokenResponse = tokenRequest.execute();
String accessToken = tokenResponse.getAccessToken();
HttpRequest httpRequest = httpTransport.createRequestFactory()
.buildGetRequest(new GenericUrl(EXPORT_URL));
BearerToken.authorizationHeaderAccessMethod().intercept(
httpRequest, accessToken);
String opml = httpRequest.execute().parseAsString();
User user = CommaFeedSession.get().getUser();
if (user != null) {
if (StartupBean.USERNAME_DEMO.equals(user.getName())) {
throw new DisplayException(
"Import is disabled for the demo account");
}
importer.importOpml(CommaFeedSession.get().getUser(), opml);
}
} catch (Exception e) {
throw new DisplayException(e);
}
}
setResponsePage(getApplication().getHomePage());
}
}

View File

@@ -1,50 +0,0 @@
package com.commafeed.frontend.pages;
import java.net.URISyntaxException;
import javax.inject.Inject;
import org.apache.http.client.utils.URIBuilder;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.request.flow.RedirectToUrlException;
import org.jboss.logging.Logger;
import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.services.ApplicationSettingsService;
@SuppressWarnings("serial")
public class GoogleImportRedirectPage extends WebPage {
private static Logger log = Logger
.getLogger(GoogleImportRedirectPage.class);
private static final String SCOPE = "https://www.google.com/reader/subscriptions/export email profile";
private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
@Inject
ApplicationSettingsService applicationSettingsService;
public GoogleImportRedirectPage() {
ApplicationSettings settings = applicationSettingsService.get();
String clientId = settings.getGoogleClientId();
String redirectUri = GoogleImportCallbackPage.getCallbackUrl(settings.getPublicUrl());
try {
URIBuilder builder = new URIBuilder(AUTH_URL);
builder.addParameter("redirect_uri", redirectUri);
builder.addParameter("response_type", "code");
builder.addParameter("scope", SCOPE);
builder.addParameter("approval_prompt", "force");
builder.addParameter("client_id", clientId);
builder.addParameter("access_type", "offline");
throw new RedirectToUrlException(builder.build().toString());
} catch (URISyntaxException e) {
log.error(e.getMessage(), e);
}
}
}

View File

@@ -4,6 +4,7 @@ import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.mapper.parameter.PageParameters;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
import com.commafeed.frontend.CommaFeedSession; import com.commafeed.frontend.CommaFeedSession;
@@ -18,16 +19,16 @@ public class HomePage extends BasePage {
public void renderHead(IHeaderResponse response) { public void renderHead(IHeaderResponse response) {
super.renderHead(response); super.renderHead(response);
response.render(CssHeaderItem.forReference( response.render(CssHeaderItem.forReference(new UserCustomCssReference() {
new UserCustomCssReference() { @Override
@Override protected String getCss() {
protected String getCss() { User user = CommaFeedSession.get().getUser();
UserSettings settings = userSettingsDAO if (user == null) {
.findByUser(CommaFeedSession.get().getUser()); return null;
return settings == null ? null : settings }
.getCustomCss(); UserSettings settings = userSettingsDAO.findByUser(user);
} return settings == null ? null : settings.getCustomCss();
}, new PageParameters().add("_t", System.currentTimeMillis()), }
null)); }, new PageParameters().add("_t", System.currentTimeMillis()), null));
} }
} }

View File

@@ -52,20 +52,15 @@ public class NextUnreadRedirectPage extends WebPage {
} }
List<FeedEntryStatus> statuses = null; List<FeedEntryStatus> statuses = null;
if (StringUtils.isBlank(categoryId) if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
|| CategoryREST.ALL.equals(categoryId)) { List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
statuses = feedEntryStatusDAO.findAllUnread(user, null, 0, 1, statuses = feedEntryStatusDAO.findBySubscriptions(subs, true, null, null, 0, 1, order, true, false);
order, true);
} else { } else {
FeedCategory category = feedCategoryDAO.findById(user, FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
Long.valueOf(categoryId));
if (category != null) { if (category != null) {
List<FeedCategory> children = feedCategoryDAO List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
.findAllChildrenCategories(user, category); List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
List<FeedSubscription> subscriptions = feedSubscriptionDAO statuses = feedEntryStatusDAO.findBySubscriptions(subscriptions, true, null, null, 0, 1, order, true, false);
.findByCategories(user, children);
statuses = feedEntryStatusDAO.findUnreadBySubscriptions(
subscriptions, null, 0, 1, order, true);
} }
} }

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