1
0
mirror of https://github.com/sismics/docs.git synced 2025-12-14 10:16:21 +00:00

225 Commits
v1.4 ... v1.5

Author SHA1 Message Date
Benjamin Gamard
ea210d6aac v1.5 2018-03-01 14:18:23 +01:00
Benjamin Gamard
f70cded634 file importer 2018-03-01 12:28:29 +01:00
Benjamin Gamard
cc6e1b4052 init files importer 2018-02-28 22:53:06 +01:00
Benjamin Gamard
1ce5ba4f06 #180: expain that only unread emails will be imported, fix logo display 2018-02-28 13:08:30 +01:00
Benjamin Gamard
2b23a1d048 #180: fix tests 2018-02-27 20:17:09 +01:00
Benjamin Gamard
37f262177d #180: advertise inbox scanning 2018-02-27 20:06:34 +01:00
Benjamin Gamard
7ded510625 Closes #180: IMAP inbox synching (ui) 2018-02-27 20:05:10 +01:00
Benjamin Gamard
797a987e2b refresh ui css + init inbox scanning settings 2018-02-27 19:02:23 +01:00
Benjamin Gamard
062dee987f #180: last inbox sync infos 2018-02-27 17:11:04 +01:00
Benjamin Gamard
1054931e63 #180: fix tests for travis 2018-02-27 15:34:20 +01:00
Benjamin Gamard
3720a881a4 #180: IMAP inbox synching (api) 2018-02-27 14:58:37 +01:00
Benjamin Gamard
f379b4e5ab #179: default language (ui) 2018-02-25 16:55:45 +01:00
Benjamin Gamard
9a9e86829e #179: default language (api) 2018-02-22 12:57:33 +01:00
Benjamin Gamard
66cd4abd8b #177: truncate data to fit the database 2018-02-22 11:42:41 +01:00
Benjamin Gamard
ba4470f155 Closes #177: import document from EML file (UI done) 2018-02-22 11:35:34 +01:00
Benjamin Gamard
b95ec019de #177: import document from EML file (api done) 2018-02-22 10:46:32 +01:00
Benjamin Gamard
d3a40ebca8 #177: import document from EML file (wip) 2018-02-21 21:47:57 +01:00
Benjamin Gamard
5d335049a2 fix api doc + fix translations 2018-02-15 20:38:03 +01:00
Benjamin Gamard
bd51e2ab55 fix password reset button 2018-02-14 12:32:50 +01:00
Benjamin Gamard
f8278bd44e update translations 2018-02-02 18:12:14 +01:00
Benjamin Gamard
4797cd1eca Closes #139: screenshot on README.md 2018-02-02 17:28:23 +01:00
Benjamin Gamard
706d244ff8 Closes #159: cancel routes + email at route step validation 2018-02-02 17:18:34 +01:00
Benjamin Gamard
5b8cd18128 #159: display and validate route steps 2018-02-02 12:37:56 +01:00
Benjamin Gamard
8a854bb37d #159: get routes on a document 2018-02-01 23:26:29 +01:00
Benjamin Gamard
6e6f892cb0 fix 2018-02-01 18:26:33 +01:00
Benjamin Gamard
2b4ddfa072 #159: validate route steps 2018-02-01 18:01:11 +01:00
Benjamin Gamard
c9adff5a25 #159: add temporary READ ACL for route step 2018-02-01 11:48:02 +01:00
Benjamin Gamard
503cfff82e #159: return the active step in GET /document/id 2018-01-31 22:07:38 +01:00
Benjamin Gamard
5e713f0c2a #159: start a route on a document 2018-01-29 23:34:43 +01:00
Benjamin Gamard
e035007070 #159: route model steps validation 2018-01-28 14:52:13 +01:00
Benjamin Gamard
17a94395f3 #159: route model api 2018-01-28 12:44:11 +01:00
Benjamin Gamard
0ab6c8e4b0 #159: workflow steps ui 2018-01-28 12:24:40 +01:00
Benjamin Gamard
8284169923 #159: workflow steps ui 2018-01-27 23:41:57 +01:00
Benjamin Gamard
5f9094c540 #159: workflow ui init 2018-01-27 17:33:46 +01:00
Benjamin Gamard
332ac9c109 #159: workflow db model 2018-01-26 11:26:34 +01:00
Benjamin Gamard
9ba49f35ff enable cors 2018-01-24 15:39:27 +01:00
Benjamin Gamard
d0646f12e6 translations 2018-01-08 12:02:01 +01:00
Benjamin Gamard
a8817c75b0 Update README.md 2018-01-08 11:51:15 +01:00
Benjamin Gamard
7fc50f2629 Closes #164: admin can send a password reset email to users 2018-01-07 19:56:11 +01:00
Benjamin Gamard
b2d9684738 russian translation 2018-01-06 11:50:03 +01:00
Benjamin Gamard
8ab284ff98 russian translation 2018-01-06 11:44:44 +01:00
Benjamin Gamard
a099d29524 Merge remote-tracking branch 'origin/master' 2018-01-04 16:18:24 +01:00
Benjamin Gamard
cf44af0065 Closes #169 feedback for username/group name already taken 2018-01-04 16:18:06 +01:00
Benjamin Gamard
b5bf8b6545 info about Sismics Docs Cloud in README.md 2018-01-04 15:23:55 +01:00
Benjamin Gamard
4cc3fa4d89 fix zh_TW translation 2018-01-02 19:25:41 +01:00
Benjamin Gamard
ed50e202d7 zh_CN/zh_TW translations 2018-01-02 19:24:32 +01:00
Benjamin Gamard
6d865af15a Closes #173: fix pagination default page size 2018-01-01 23:24:24 +01:00
Benjamin Gamard
66d331ddb8 env var for admin password expected hashed 2018-01-01 17:14:12 +01:00
Benjamin Gamard
0903c03a29 new android icon 2017-12-21 12:51:47 +01:00
Benjamin Gamard
5f546b6c6d new logo 2017-12-21 12:28:33 +01:00
Benjamin Gamard
9bb73bb35b Merge remote-tracking branch 'origin/master' 2017-12-11 10:17:19 +01:00
Benjamin Gamard
2ca5b04de2 initial admin password by env variable 2017-12-11 10:17:08 +01:00
bgamard
e883c1e678 Closes #171: strip alpha channel from png 2017-11-23 15:40:53 +01:00
bgamard
4fc434a222 Merge remote-tracking branch 'origin/master' 2017-11-23 15:32:31 +01:00
bgamard
ecfa747a2c Closes #172: smart images caching 2017-11-23 15:32:20 +01:00
Benjamin Gamard
dc28ebfa50 fix file modal + fix file link in audit log + high quality thumbs 2017-11-23 01:16:54 +01:00
Benjamin Gamard
6596eba6ca advertise demo app 2017-11-21 23:56:32 +01:00
Benjamin Gamard
7194f9aac0 update fr translation 2017-11-21 20:23:35 +01:00
Benjamin Gamard
e4fe1cfa90 fix active user count 2017-11-21 19:37:29 +01:00
Benjamin Gamard
5bc73548b3 Closes #162: feedback box 2017-11-21 12:01:53 +01:00
Benjamin Gamard
2156848e4a GET /app returns document count 2017-11-21 09:49:33 +01:00
Benjamin Gamard
3f807b3e51 edit -> save 2017-11-20 21:25:52 +01:00
Benjamin Gamard
d786862a60 Closes #167: disable users 2017-11-20 21:21:50 +01:00
Benjamin Gamard
fb75bafe96 Closes #166: global quota 2017-11-20 20:34:29 +01:00
Benjamin Gamard
66f781b716 cleanup logs for Travis 2017-11-18 20:14:50 +01:00
Benjamin Gamard
b3dc409926 cleanup logs for Travis + new process for each test 2017-11-18 20:01:11 +01:00
Benjamin Gamard
287ed06b6a prevent lastpass autofill in non editable fields 2017-11-18 19:45:08 +01:00
Benjamin Gamard
df1d013b1c Closes #165: smtp hostname/port/username/password configurables with env 2017-11-18 19:34:13 +01:00
Benjamin Gamard
fdb95484c1 fix sending email to an unauthenticated smtp server 2017-11-17 23:47:53 +01:00
Benjamin Gamard
4cf1f29e0a Closes #161: password recovery by email 2017-11-17 23:17:05 +01:00
Benjamin Gamard
332fd9d1f6 fix tests 2017-11-17 22:26:20 +01:00
Benjamin Gamard
039d881a07 #161: password recovery by email (wip, server part done) 2017-11-17 22:03:54 +01:00
bgamard
b8176a9fe9 fix tests 2017-11-17 17:10:05 +01:00
bgamard
65937d6f4c #161: password recovery by email (wip) 2017-11-17 17:01:08 +01:00
bgamard
590bf74e98 display two-factor authentication activation in admin area 2017-11-17 15:18:16 +01:00
bgamard
642a3e10ce fix for mobile 2017-11-17 14:05:19 +01:00
Benjamin Gamard
52a8cf92c8 fix homepage dom 2017-11-15 23:07:34 +01:00
bgamard
fc68ee56d5 fix en translation 2017-11-15 10:49:47 +01:00
Benjamin Gamard
644f4803df Merge remote-tracking branch 'origin/master' 2017-11-15 00:19:36 +01:00
Benjamin Gamard
eda6106de8 upgrade android build 2017-11-15 00:19:12 +01:00
bgamard
3a1691066e Closes #155: localize share app 2017-11-14 15:44:40 +01:00
Benjamin Gamard
b02039bad4 Fix zh_CN translation 2017-11-14 01:14:29 +01:00
Benjamin Gamard
6527a9e9bb Closes #156: Localize Android app 2017-11-14 01:07:16 +01:00
Benjamin Gamard
c59ad4d446 fix pagination 2017-11-13 22:50:21 +01:00
Benjamin Gamard
f475cbc5d8 oops 2017-11-13 22:44:06 +01:00
Benjamin Gamard
00452cc505 Closes #158: advanced search form 2017-11-13 22:37:03 +01:00
Benjamin Gamard
23660961bd #158: advanced search form (wip) 2017-11-13 18:11:54 +01:00
Benjamin Gamard
742ff183bf #158: advanced search form (wip) 2017-11-12 23:24:10 +01:00
Benjamin Gamard
dca8c28b84 Closes #157: Deskew before OCR 2017-11-12 14:49:52 +01:00
Benjamin Gamard
46079393d5 Fix file modal routing 2017-11-12 02:18:02 +01:00
Benjamin Gamard
517e4a4507 Closes #150: Display file name in audit log 2017-11-12 02:06:41 +01:00
Benjamin Gamard
6f3ae6da9d Better thumbnails UI 2017-11-11 23:25:52 +01:00
Benjamin Gamard
273136ab23 Closes #152 closes #154: localize date and time format 2017-11-10 23:43:35 +01:00
Benjamin Gamard
e74f86e118 Closes #153: fix missing localization string 2017-11-10 22:56:42 +01:00
Benjamin Gamard
84d4d3b165 Closes #151: upgrade JS libraries 2017-11-10 22:00:34 +01:00
bgamard
c355cb8bd5 Closes #148: force Qihoo 360 to use webkit rendering 2017-11-09 14:43:37 +01:00
bgamard
2957034286 Closes #147: fix IE file upload 2017-11-09 14:39:25 +01:00
bgamard
36b4fbd303 ie fix 2017-11-09 13:36:41 +01:00
bgamard
f57cf46313 Closes #146: no cache 2017-11-09 13:36:10 +01:00
Benjamin Gamard
244ddc7ce2 Closes #141: Never close full file content in memory 2017-11-06 16:45:47 +01:00
Benjamin Gamard
4d161aea07 Closes #117: fix templates minification 2017-11-06 00:48:55 +01:00
Benjamin Gamard
cf9101d157 Closes #143: Select the default language for new documents from browser language 2017-11-05 22:28:23 +01:00
Benjamin Gamard
614c8a1d13 log to stdout 2017-11-05 21:27:54 +01:00
Benjamin Gamard
879ab7951d fix build 2017-11-05 17:30:45 +01:00
Benjamin Gamard
311b42ad25 fix flash on untranslated content 2017-11-05 16:59:04 +01:00
Benjamin Gamard
0ebbbac9a6 optimize docker image 2017-11-04 20:50:57 +01:00
Benjamin Gamard
d2f9fcdda0 zh_CN translation + footer fix 2017-11-04 20:39:39 +01:00
bgamard
cfe5690a73 Closes #142: design cleanup 2017-11-03 14:53:09 +01:00
bgamard
a055b3ff5c #117: more logs + possible fix 2017-11-03 11:13:50 +01:00
bgamard
18f37ec2a8 Closes #131: validate only dirty forms 2017-11-03 11:05:04 +01:00
Benjamin Gamard
14b4e5aeec design refresh 2017-11-03 00:10:17 +01:00
Benjamin Gamard
a980930e69 Closes #137: upload files without drag & drop 2017-11-02 23:36:38 +01:00
Benjamin Gamard
1b4eb70d8d Closes #136 #138: ui fixes 2017-11-02 23:14:00 +01:00
Benjamin Gamard
1856ccc3aa less padding 2017-11-02 21:16:47 +01:00
Benjamin Gamard
3217c67ff6 flat design 2017-11-02 21:00:32 +01:00
bgamard
54d5f1cb1b #111: french translation 2017-11-02 17:14:34 +01:00
bgamard
e49d002941 #111: translate templates 2017-11-02 15:39:50 +01:00
Benjamin Gamard
4822b8bf23 Update README.md 2017-11-01 19:50:42 +01:00
Benjamin Gamard
198a6d5665 #111: translate templates (wip) 2017-11-01 19:48:50 +01:00
Benjamin Gamard
c7b9ec3a4c #111: translate controllers 2017-11-01 14:34:15 +01:00
Benjamin Gamard
f46e10e11c support more languages 2017-10-31 21:20:22 +01:00
Benjamin Gamard
b9acc4ecf8 support more languages 2017-10-31 21:16:46 +01:00
Benjamin Gamard
403d094a3d support more languages 2017-10-31 21:06:12 +01:00
Benjamin Gamard
3b1f11e5a8 Merge branch 'master' into sismics_prod 2017-10-31 21:01:32 +01:00
Benjamin Gamard
ddba06cca3 support more languages 2017-10-31 21:01:23 +01:00
Benjamin Gamard
bf89af0da9 Merge branch 'master' into sismics_prod 2017-10-31 20:39:10 +01:00
Benjamin Gamard
5a30164848 support more languages 2017-10-31 20:36:55 +01:00
Benjamin Gamard
0c4e200900 support more languages 2017-10-31 20:34:54 +01:00
Benjamin Gamard
fe8e8f041c Merge remote-tracking branch 'origin/sismics_prod' into sismics_prod 2017-07-31 14:49:17 +02:00
Benjamin Gamard
43084d9d86 Merge branch 'master' into sismics_prod 2017-07-31 14:48:59 +02:00
Benjamin Gamard
e1e1b4e278 embed free monospaced font 2017-07-31 14:40:35 +02:00
Benjamin Gamard
14f8239ba3 register fonts 2017-07-31 14:09:56 +02:00
Benjamin Gamard
e660a70d00 travis: install microsoft fonts 2017-07-31 13:52:32 +02:00
Benjamin Gamard
119d30bb16 fix plain text file save 2017-07-31 12:08:42 +02:00
Benjamin Gamard
5686de56e2 Merge branch 'master' into sismics_prod 2017-07-31 01:51:39 +02:00
Benjamin Gamard
e0214a6a9f Closes #118: create pdf from text plain files 2017-07-31 01:51:23 +02:00
Benjamin Gamard
330de495db #118: extract text content from text plain files (WIP) 2017-06-11 11:33:30 +02:00
jendib
dcc7fe55f4 Closes #125: Confirmation before deleting a comment 2017-05-07 01:39:20 +02:00
jendib
3274b4c79a Closes #130: Fix document language icon 2017-05-07 01:34:21 +02:00
jendib
cbfa4b1c41 Closes #127: Edit -> Save 2017-05-07 01:32:55 +02:00
jendib
5f7d2f2a68 Closes #129: bigger checkbox 2017-05-07 01:31:59 +02:00
jendib
e38bdbe508 Closes #128: Delete cursor on comment delete button 2017-05-07 01:27:24 +02:00
jendib
c352b94b38 Closes #126: click to copy 2017-05-07 01:25:20 +02:00
Benjamin Gamard
6b0106e385 update readme with docker instructions 2017-04-25 11:09:18 +02:00
Benjamin Gamard
60021e5123 build prod package before pushing to dockerhub 2017-04-25 10:39:58 +02:00
Benjamin Gamard
76d3157247 travis push to dockerhub 2017-04-25 10:26:48 +02:00
Benjamin Gamard
7c24778460 Merge branch 'master' into sismics_prod 2017-03-21 09:14:19 +01:00
Jean-Marc Tremeaux
8231db4a5a Fix dockerfile 2017-03-21 08:56:00 +01:00
jendib
fe5dd5e8dc Merge branch 'master' of https://github.com/sismics/docs 2017-01-24 22:00:35 +01:00
jendib
5872928812 Hide filename if not available + upgrade Gradle 2017-01-24 22:00:24 +01:00
Benjamin Gamard
8a8f4bb388 Merge pull request #121 from sismics/master
Update Jetty version
2017-01-03 11:43:54 +01:00
Benjamin Gamard
0e8d5fd5ec Update Jetty version 2017-01-03 11:43:31 +01:00
Benjamin Gamard
b9344149b0 Merge pull request #120 from sismics/master
Update base image
2017-01-03 11:11:44 +01:00
Benjamin Gamard
5053a6852f Update base image 2017-01-03 11:11:12 +01:00
jendib
bb3faca533 Closes #119: Keep and display original file name 2016-12-07 01:28:52 +01:00
jendib
4f7fcbfdf0 Merge branch 'master' into sismics_prod 2016-12-05 19:30:15 +01:00
jendib
87c1cc88be #116: Allow all file types 2016-12-05 19:25:52 +01:00
jendib
1d78551f4c Fix tests, add logs for #117 2016-11-20 18:52:47 +01:00
jendib
b36d08db8e Closes #116: Allow all file types 2016-11-20 18:41:42 +01:00
Benjamin Gamard
c99b1a1867 cleanup private scripts 2016-10-24 16:55:56 +02:00
Benjamin Gamard
fc4380e5cc Merge pull request #115 from sismics/master
Edit production domain
2016-10-06 15:35:11 +02:00
Benjamin Gamard
850ed7f76b Edit production domain 2016-10-06 15:34:38 +02:00
jendib
0f6aa3befb Concatenate Angular templates in minified JS file 2016-08-31 19:34:37 +02:00
jendib
ddd976162c Merge branch 'master' into sismics_prod 2016-08-26 22:00:58 +02:00
jendib
cdd19e182b Closes #113: Fire async events after request transaction commit 2016-08-26 21:22:27 +02:00
jendib
afc22a547e Closes #112: Don't update auth token on each request 2016-08-26 20:34:23 +02:00
jendib
79ca54c5af Android: Ask permission to write files 2016-07-30 02:52:53 +02:00
jendib
0aacf20c16 Android: upgrade to Nougat 2016-07-09 19:35:05 +02:00
jendib
cdfb43dbd8 Cleanup Lucene DAO 2016-06-28 23:31:59 +02:00
jendib
35ec8b951c Build fails if grunt fails 2016-06-16 22:15:54 +02:00
Benjamin Gamard
05bfaa0035 Merge pull request #110 from sismics/master
Push to production
2016-06-16 21:38:14 +02:00
jendib
f5705b1153 Minor UI tweaks 2016-06-16 20:31:39 +02:00
jendib
ed1353a4eb Android: upgrade build tools 2016-06-16 20:13:34 +02:00
jendib
a79922d7c9 Android: upgrade okhttp (fix for Android 6) 2016-06-06 20:51:05 +02:00
jendib
7a7cbd570c Closes #85: UI for login as guest 2016-05-29 18:34:51 +02:00
jendib
d7865cfaf0 #85: Login as guest 2016-05-29 16:37:26 +02:00
jendib
ead01ce1d0 #85: Guest login configuration 2016-05-28 23:09:52 +02:00
jendib
8aca012c99 Reduce log verbosity 2016-05-16 21:21:19 +02:00
jendib
67a4dc63ca Closes #106: Header base authentication 2016-05-16 21:07:01 +02:00
jendib
ce0678784b #81: Android: Display dublincore metadata 2016-05-15 19:50:12 +02:00
jendib
cbc4bbb818 API documentation introduction 2016-05-14 23:10:29 +02:00
jendib
1c558a884d Closes #105: Upgrade grunt dependencies 2016-05-14 14:21:00 +02:00
jendib
d84d1428b2 Android: Better layout for read-only documents 2016-05-14 02:05:03 +02:00
Benjamin Gamard
b870ee8d62 Merge pull request #104 from sismics/master
Push to production
2016-05-13 00:46:18 +02:00
jendib
ef18581e71 #103: API documentation for /document 2016-05-13 00:45:08 +02:00
jendib
177bbceaf4 #103: API documentation for all resources except /document 2016-05-12 01:26:02 +02:00
jendib
a13174ac4d Android: use GET /tag/list instead of /tag/stats 2016-05-11 00:55:27 +02:00
jendib
e181b7d24b #103: API documentation for /user and /vocabulary 2016-05-10 23:30:28 +02:00
jendib
e631aa0e8a Closes #101: Allow export buttons for read-only documents 2016-05-10 21:18:16 +02:00
jendib
575ad75a0a Closes #102: Android: Clear all auth tokens on logout 2016-05-10 20:00:14 +02:00
jendib
394f667ab0 Prepare 1.5 development cycle 2016-05-09 22:52:34 +02:00
Benjamin Gamard
f80cf43ae8 Merge pull request #100 from sismics/master
Push to production
2016-05-09 22:10:49 +02:00
Benjamin Gamard
6b57d29f51 Merge pull request #96 from sismics/master
Push to production
2016-05-08 23:40:50 +02:00
Benjamin Gamard
f2c4dde56e Merge pull request #95 from sismics/master
Fix batch for ACLs on tags
2016-05-08 23:21:36 +02:00
Benjamin Gamard
3a22132363 Merge pull request #94 from sismics/master
Push to production
2016-05-08 23:07:43 +02:00
Benjamin Gamard
767099b7ea Merge pull request #89 from sismics/master
Push to production
2016-03-24 00:06:52 +01:00
Benjamin Gamard
6aef7246a0 Merge pull request #86 from sismics/master
Push to production
2016-03-21 01:01:14 +01:00
Benjamin Gamard
1f6d9f0211 Merge pull request #76 from sismics/master
Push to production
2016-03-02 00:43:37 +01:00
Benjamin Gamard
3248637e8c Merge pull request #64 from sismics/master
Push to production
2016-02-11 23:41:47 +01:00
Benjamin Gamard
bf8e0827e2 Merge pull request #60 from sismics/master
Push to production
2016-01-24 16:30:50 +01:00
Benjamin Gamard
ad1e57316f Merge pull request #56 from sismics/master
Push to production
2016-01-01 01:58:32 +01:00
Benjamin Gamard
737b3299ff Merge pull request #54 from sismics/master
Push to production
2015-12-11 22:22:44 +01:00
Benjamin Gamard
1934bb71f0 Merge pull request #52 from sismics/master
Push to production
2015-12-01 01:17:36 +01:00
Benjamin Gamard
b3ef9b0476 Merge pull request #50 from sismics/master
Push to production
2015-11-30 00:29:31 +01:00
Benjamin Gamard
8477920475 Merge pull request #47 from sismics/master
Push to production
2015-11-30 00:10:26 +01:00
Benjamin Gamard
f98a12b96f Merge pull request #46 from sismics/master
Push to production
2015-11-21 20:32:16 +01:00
Benjamin Gamard
bee8a4fcdc Merge pull request #43 from sismics/master
Push to production
2015-11-18 23:11:02 +01:00
Benjamin Gamard
f984595b97 Merge pull request #40 from sismics/master
Push to production
2015-11-02 23:55:16 +01:00
Benjamin Gamard
f01d78a9ea Merge pull request #39 from sismics/master
Push to production
2015-11-02 21:36:14 +01:00
Benjamin Gamard
97f25de0dc Merge pull request #31 from sismics/master
Push to production
2015-09-08 21:50:12 +02:00
Benjamin Gamard
3d1b5a7394 Merge pull request #28 from sismics/master
#4: Upgrade to unrelease PDFBox 2
2015-09-05 23:12:29 +02:00
Benjamin Gamard
df1eaf54c8 Merge pull request #27 from sismics/master
Push to production
2015-09-05 21:41:43 +02:00
Benjamin Gamard
3851408100 Merge pull request #25 from sismics/master
Push to production
2015-08-28 01:16:33 +02:00
Benjamin Gamard
794c5012ad Merge pull request #19 from sismics/master
Fix post-ACL system
2015-05-10 14:03:44 +02:00
Benjamin Gamard
1ba9f7d7d9 Merge pull request #17 from sismics/master
#13: Fix performance issue
2015-05-09 18:00:33 +02:00
Benjamin Gamard
9b4330d618 Merge pull request #16 from sismics/master
#13: Disable shared status in GET /document/list (too slow)
2015-05-09 17:32:04 +02:00
Benjamin Gamard
3a32b742e8 Merge pull request #15 from sismics/master
ACL system
2015-05-09 16:37:13 +02:00
Benjamin Gamard
87b3d25c0f Merge pull request #12 from sismics/master
Push to production
2015-05-02 17:19:30 +02:00
Benjamin Gamard
9f28649a3a Merge pull request #9 from sismics/master
Upload drag & dropped files sequentially
2015-03-29 16:07:07 +02:00
Benjamin Gamard
8f9df8961b Merge pull request #8 from sismics/master
Attach orphan files to a new document
2015-03-28 18:05:01 +01:00
Benjamin Gamard
9e9217bfcb Merge pull request #7 from sismics/master
Drag & drop
2015-03-27 23:11:33 +01:00
Benjamin Gamard
ffdd5a3631 hook me 2015-03-23 17:30:15 +01:00
Benjamin Gamard
db4bf8d35a Merge pull request #5 from sismics/master
chmod +x
2015-03-17 00:03:00 +01:00
Jean-Marc Tremeaux
7b2859f96e Deploy script 2015-03-11 22:56:56 +01:00
Jean-Marc Tremeaux
03c5c33ea7 Deploy script 2015-03-11 22:54:42 +01:00
319 changed files with 102342 additions and 18782 deletions

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@
/.idea
/.project
*.iml
node_modules
import_test

View File

@@ -3,9 +3,22 @@ dist: trusty
language: java
before_install:
- sudo apt-get -qq update
- sudo apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-jpn
- sudo apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra
- sudo apt-get -y -q install haveged && sudo service haveged start
after_success:
- mvn -Pprod -DskipTests clean install
- docker login -u $DOCKER_USER -p $DOCKER_PASS
- export REPO=sismics/docs
- export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi`
- docker build -f Dockerfile -t $REPO:$COMMIT .
- docker tag $REPO:$COMMIT $REPO:$TAG
- docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER
- docker push $REPO
env:
global:
- TESSDATA_PREFIX=/usr/share/tesseract-ocr
- LC_NUMERIC=C
- TESSDATA_PREFIX=/usr/share/tesseract-ocr
- LC_NUMERIC=C
- secure: LRGpjWORb0qy6VuypZjTAfA8uRHlFUMTwb77cenS9PPRBxuSnctC531asS9Xg3DqC5nsRxBBprgfCKotn5S8nBSD1ceHh84NASyzLSBft3xSMbg7f/2i7MQ+pGVwLncusBU6E/drnMFwZBleo+9M8Tf96axY5zuUp90MUTpSgt0=
- secure: bCDDR6+I7PmSkuTYZv1HF/z98ANX/SFEESUCqxVmV5Gs0zFC0vQXaPJQ2xaJNRop1HZBFMZLeMMPleb0iOs985smpvK2F6Rbop9Tu+Vyo0uKqv9tbZ7F8Nfgnv9suHKZlL84FNeUQZJX6vsFIYPEJ/r7K5P/M0PdUy++fEwxEhU=
- secure: ewXnzbkgCIHpDWtaWGMa1OYZJ/ki99zcIl4jcDPIC0eB3njX/WgfcC6i0Ke9mLqDqwXarWJ6helm22sNh+xtQiz6isfBtBX+novfRt9AANrBe3koCMUemMDy7oh5VflBaFNP0DVb8LSCnwf6dx6ZB5E9EB8knvk40quc/cXpGjY=
- COMMIT=${TRAVIS_COMMIT::8}

View File

@@ -1,10 +1,11 @@
FROM sismics/debian-java7-jetty9
MAINTAINER benjamin.gam@gmail.com
FROM sismics/jetty:9.2.20-jdk7
MAINTAINER b.gamard@sismics.com
RUN apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-jpn
RUN apt-get update && apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra && \
apt-get clean && rm -rf /var/lib/apt/lists/*
ENV TESSDATA_PREFIX /usr/share/tesseract-ocr
ENV LC_NUMERIC C
ADD docs-web/target/docs-web-*.war /opt/jetty/webapps/docs.war
ADD docs.xml /opt/jetty/webapps/docs.xml
ADD docs-web/target/docs-web-*.war /opt/jetty/webapps/docs.war

View File

@@ -1,20 +1,28 @@
Sismics Docs [![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs)
============
<h3 align="center">
<img src="https://www.sismicsdocs.com/img/github-title.png" alt="Sismics Docs" width=500 />
</h3>
_Web interface_
[![Twitter: @sismicsdocs](https://img.shields.io/badge/contact-@sismicsdocs-blue.svg?style=flat)](https://twitter.com/sismicsdocs)
[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
[![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs)
![Web interface](http://sismics.com/docs/screenshot1.png)
Docs is an open source, lightweight document management system for individuals and businesses.
_Android application_
<hr />
<h2 align="center">
✨ We just launched a Cloud version of Sismics Docs! Head to <a href="https://www.sismicsdocs.com/">sismicsdocs.com</a> for more informations ✨
</h2>
<hr />
![Android documents list](http://sismics.com/docs/android1.png) ![Android navigation](http://sismics.com/docs/android2.png) ![Android document details](http://sismics.com/docs/android3.png) ![Android document actions](http://sismics.com/docs/android4.png)
![New!](https://www.sismicsdocs.com/img/laptop-demo.png)
What is Docs?
---------------
Demo
----
Docs is an open source, lightweight document management system.
Docs is written in Java, and may be run on any operating system with Java support.
A demo is available at [demo.sismicsdocs.com](https://demo.sismicsdocs.com)
- Guest login is enabled with read access on all documents
- "admin" login with "admin" password
- "demo" login with "password" password
Features
--------
@@ -25,9 +33,13 @@ Features
- Flexible search engine
- Full text search in all supported files
- All [Dublin Core](http://dublincore.org/) metadata
- Workflow system ![New!](https://www.sismics.com/public/img/new.png)
- 256-bit AES encryption of stored files
- Tag system with nesting
- Import document from email (EML format) ![New!](https://www.sismics.com/public/img/new.png)
- Automatic inbox scanning and importing ![New!](https://www.sismics.com/public/img/new.png)
- User/group permission system
- 2-factor authentication
- Hierarchical groups
- Audit log
- Comments
@@ -35,13 +47,23 @@ Features
- Document sharing by URL
- RESTful Web API
- Fully featured Android client
- [Mass files importer](https://github.com/sismics/docs/tree/master/docs-importer) (single or scan mode) ![New!](https://www.sismics.com/public/img/new.png)
- Tested to 100k documents
Download
--------
The latest release is downloadable here: <https://github.com/sismics/docs/releases> in WAR format.
You will need a Java webapp server to run it, like [Jetty](http://eclipse.org/jetty/) or [Tomcat](http://tomcat.apache.org/)
You will need a Java webapp server to run it, like [Jetty](http://eclipse.org/jetty/) or [Tomcat](http://tomcat.apache.org/).
The default admin password is "admin". Don't forget to change it before going to production.
Install with Docker
-------------------
From a Docker host, run this command to download and install Sismics Docs. The server will run on <http://[your-docker-host-ip]:8100>.
The default admin password is "admin". Don't forget to change it before going to production.
docker run --rm --name sismics_docs_latest -d -p 8100:8080 -v sismics_docs_latest:/data sismics/docs:latest
How to build Docs from the sources
----------------------------------

View File

@@ -1,2 +0,0 @@
#!/bin/sh
docker build -t sismics/docs .

View File

@@ -1,46 +1,27 @@
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0-beta1'
classpath 'com.android.tools.build:gradle:3.0.1'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
google()
}
android {
compileSdkVersion 23
buildToolsVersion '23.0.3'
compileSdkVersion 26
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
targetSdkVersion 26
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
signingConfigs {
release {
storeFile file(System.getenv("TRACKINO_STORE_PATH"))
storePassword System.getenv("TRACKINO_STORE_PASS")
keyAlias System.getenv("TRACKINO_STORE_ALIAS")
keyPassword System.getenv("TRACKINO_STORE_KEYPASS")
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
versionName '1.0'
}
lintOptions {
@@ -50,13 +31,13 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support:recyclerview-v7:23.3.0'
compile 'com.android.support:design:23.3.0'
compile 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support:recyclerview-v7:26.1.0'
compile 'com.android.support:design:26.1.0'
compile 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5'
compile 'org.greenrobot:eventbus:3.0.0'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp3:okhttp:3.1.1'
compile "com.squareup.okhttp3:okhttp-urlconnection:3.1.1"
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2'
compile 'com.squareup.okhttp3:okhttp:3.7.0'
compile 'com.squareup.okhttp3:okhttp-urlconnection:3.4.0'
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
}

View File

@@ -101,7 +101,7 @@ public class DocumentEditActivity extends AppCompatActivity {
finish();
return;
}
JSONArray tagArray = tags.optJSONArray("stats");
JSONArray tagArray = tags.optJSONArray("tags");
List<JSONObject> tagList = new ArrayList<>();
for (int i = 0; i < tagArray.length(); i++) {

View File

@@ -16,6 +16,7 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.method.LinkMovementMethod;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
@@ -51,7 +52,7 @@ import com.sismics.docs.resource.FileResource;
import com.sismics.docs.service.FileUploadService;
import com.sismics.docs.util.NetworkUtil;
import com.sismics.docs.util.PreferenceUtil;
import com.sismics.docs.util.TagUtil;
import com.sismics.docs.util.SpannableUtil;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -75,11 +76,6 @@ public class DocumentViewActivity extends AppCompatActivity {
*/
public static final int REQUEST_CODE_ADD_FILE = 1;
/**
* Request code of editing document.
*/
public static final int REQUEST_CODE_EDIT_DOCUMENT = 2;
/**
* File view pager.
*/
@@ -176,9 +172,11 @@ public class DocumentViewActivity extends AppCompatActivity {
}
// Fill the layout
// Create date
TextView createdDateTextView = (TextView) findViewById(R.id.createdDateTextView);
createdDateTextView.setText(date);
// Description
TextView descriptionTextView = (TextView) findViewById(R.id.descriptionTextView);
if (description.isEmpty() || document.isNull("description")) {
descriptionTextView.setVisibility(View.GONE);
@@ -187,17 +185,20 @@ public class DocumentViewActivity extends AppCompatActivity {
descriptionTextView.setText(description);
}
// Tags
TextView tagTextView = (TextView) findViewById(R.id.tagTextView);
if (tags.length() == 0) {
tagTextView.setVisibility(View.GONE);
} else {
tagTextView.setVisibility(View.VISIBLE);
tagTextView.setText(TagUtil.buildSpannable(tags));
tagTextView.setText(SpannableUtil.buildSpannableTags(tags));
}
// Language
ImageView languageImageView = (ImageView) findViewById(R.id.languageImageView);
languageImageView.setImageResource(getResources().getIdentifier(language, "drawable", getPackageName()));
// Shared status
ImageView sharedImageView = (ImageView) findViewById(R.id.sharedImageView);
sharedImageView.setVisibility(shared ? View.VISIBLE : View.GONE);
@@ -208,7 +209,7 @@ public class DocumentViewActivity extends AppCompatActivity {
public void onClick(View view) {
Intent intent = new Intent(DocumentViewActivity.this, DocumentEditActivity.class);
intent.putExtra("document", DocumentViewActivity.this.document.toString());
startActivityForResult(intent, REQUEST_CODE_EDIT_DOCUMENT);
startActivity(intent);
}
});
@@ -642,10 +643,10 @@ public class DocumentViewActivity extends AppCompatActivity {
}
// Action only available if the document is writable
findViewById(R.id.actionEditDocument).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionUploadFile).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionSharing).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionDelete).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionEditDocument).setVisibility(writable ? View.VISIBLE : View.GONE);
findViewById(R.id.actionUploadFile).setVisibility(writable ? View.VISIBLE : View.GONE);
findViewById(R.id.actionSharing).setVisibility(writable ? View.VISIBLE : View.GONE);
findViewById(R.id.actionDelete).setVisibility(writable ? View.VISIBLE : View.GONE);
// ACLs
ListView aclListView = (ListView) findViewById(R.id.aclListView);
@@ -679,10 +680,54 @@ public class DocumentViewActivity extends AppCompatActivity {
startActivity(intent);
}
});
// Contributors
TextView contributorsTextView = (TextView) findViewById(R.id.contributorsTextView);
contributorsTextView.setText(SpannableUtil.buildSpannableContributors(document.optJSONArray("contributors")));
// Relations
JSONArray relations = document.optJSONArray("relations");
if (relations.length() > 0) {
TextView relationsTextView = (TextView) findViewById(R.id.relationsTextView);
relationsTextView.setMovementMethod(LinkMovementMethod.getInstance());
relationsTextView.setText(SpannableUtil.buildSpannableRelations(relations));
} else {
findViewById(R.id.relationsLayout).setVisibility(View.GONE);
}
// Additional dublincore metadata
displayDublincoreMetadata(R.id.subjectTextView, R.id.subjectLayout, "subject");
displayDublincoreMetadata(R.id.identifierTextView, R.id.identifierLayout, "identifier");
displayDublincoreMetadata(R.id.publisherTextView, R.id.publisherLayout, "publisher");
displayDublincoreMetadata(R.id.formatTextView, R.id.formatLayout, "format");
displayDublincoreMetadata(R.id.sourceTextView, R.id.sourceLayout, "source");
displayDublincoreMetadata(R.id.typeTextView, R.id.typeLayout, "type");
displayDublincoreMetadata(R.id.coverageTextView, R.id.coverageLayout, "coverage");
displayDublincoreMetadata(R.id.rightsTextView, R.id.rightsLayout, "rights");
}
});
}
/**
* Display a dublincore metadata.
*
* @param textViewId TextView ID
* @param blockViewId View ID
* @param name Name
*/
private void displayDublincoreMetadata(int textViewId, int blockViewId, String name) {
if (document == null) return;
String value = document.optString(name);
if (document.isNull(name) || value.isEmpty()) {
findViewById(blockViewId).setVisibility(View.GONE);
return;
}
findViewById(blockViewId).setVisibility(View.VISIBLE);
TextView textView = (TextView) findViewById(textViewId);
textView.setText(value);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
switch (view.getId()) {

View File

@@ -87,13 +87,13 @@ public class MainActivity extends AppCompatActivity {
tagListView.setEmptyView(tagProgressView);
JSONObject cacheTags = PreferenceUtil.getCachedJson(this, PreferenceUtil.PREF_CACHED_TAGS_JSON);
if (cacheTags != null) {
tagListView.setAdapter(new TagListAdapter(cacheTags.optJSONArray("stats")));
tagListView.setAdapter(new TagListAdapter(cacheTags.optJSONArray("tags")));
}
TagResource.stats(this, new HttpCallback() {
TagResource.list(this, new HttpCallback() {
@Override
public void onSuccess(JSONObject response) {
PreferenceUtil.setCachedJson(MainActivity.this, PreferenceUtil.PREF_CACHED_TAGS_JSON, response);
tagListView.setAdapter(new TagListAdapter(response.optJSONArray("stats")));
tagListView.setAdapter(new TagListAdapter(response.optJSONArray("tags")));
tagProgressView.setVisibility(View.GONE);
tagListView.setEmptyView(tagEmptyView);
}
@@ -158,6 +158,7 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onFinish() {
// Force logout in all cases, so the user is not stuck in case of network error
PreferenceUtil.clearAuthToken(MainActivity.this);
ApplicationContext.getInstance().setUserInfo(getApplicationContext(), null);
startActivity(new Intent(MainActivity.this, LoginActivity.class));
finish();

View File

@@ -9,7 +9,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.sismics.docs.R;
import com.sismics.docs.util.TagUtil;
import com.sismics.docs.util.SpannableUtil;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -69,7 +69,7 @@ public class DocListAdapter extends RecyclerView.Adapter<DocListAdapter.ViewHold
holder.titleTextView.setText(document.optString("title"));
JSONArray tags = document.optJSONArray("tags");
holder.subtitleTextView.setText(TagUtil.buildSpannable(tags));
holder.subtitleTextView.setText(SpannableUtil.buildSpannableTags(tags));
String date = DateFormat.getDateFormat(holder.dateTextView.getContext()).format(new Date(document.optLong("create_date")));
holder.dateTextView.setText(date);

View File

@@ -33,7 +33,6 @@ public class LanguageAdapter extends BaseAdapter {
}
languageList.add(new Language("fra", R.string.language_french, R.drawable.fra));
languageList.add(new Language("eng", R.string.language_english, R.drawable.eng));
languageList.add(new Language("jpn", R.string.language_japanese, R.drawable.jpn));
}
@Override

View File

@@ -46,7 +46,7 @@ public class TagListAdapter extends BaseAdapter {
// Reorder tags by parent/child relation and compute depth
int depth = 0;
initTags(tags, JSONObject.NULL.toString(), depth);
initTags(tags, "", depth);
}
/**
@@ -60,11 +60,10 @@ public class TagListAdapter extends BaseAdapter {
// Get all tags with this parent
for (JSONObject tag : tags) {
String tagParentId = tag.optString("parent");
if (tagParentId.equals(parentId)) {
if (parentId.equals(tagParentId)) {
TagItem tagItem = new TagItem();
tagItem.id = tag.optString("id");
tagItem.name = tag.optString("name");
tagItem.count = tag.optInt("count");
tagItem.color = tag.optString("color");
tagItem.depth = depth;
tagItemList.add(tagItem);
@@ -99,8 +98,6 @@ public class TagListAdapter extends BaseAdapter {
TagItem tagItem = getItem(position);
TextView tagTextView = (TextView) view.findViewById(R.id.tagTextView);
tagTextView.setText(tagItem.name);
TextView tagCountTextView = (TextView) view.findViewById(R.id.tagCountTextView);
tagCountTextView.setText(String.format(Locale.ENGLISH, "%d", tagItem.count));
// Label color filtering
ImageView labelImageView = (ImageView) view.findViewById(R.id.labelImageView);
@@ -125,7 +122,6 @@ public class TagListAdapter extends BaseAdapter {
public static class TagItem {
private String id;
private String name;
private int count;
private String color;
private int depth;

View File

@@ -73,7 +73,7 @@ public class SearchFragment extends DialogFragment {
dialog.cancel();
return dialog;
}
JSONArray tagArray = tags.optJSONArray("stats");
JSONArray tagArray = tags.optJSONArray("tags");
List<JSONObject> tagList = new ArrayList<>();
for (int i = 0; i < tagArray.length(); i++) {

View File

@@ -16,14 +16,14 @@ import okhttp3.Request;
*/
public class TagResource extends BaseResource {
/**
* GET /tag/stats.
* GET /tag/list.
*
* @param context Context
* @param callback Callback
*/
public static void stats(Context context, HttpCallback callback) {
public static void list(Context context, HttpCallback callback) {
Request request = new Request.Builder()
.url(HttpUrl.parse(getApiUrl(context) + "/tag/stats"))
.url(HttpUrl.parse(getApiUrl(context) + "/tag/list"))
.get()
.build();
OkHttpUtil.buildClient(context)

View File

@@ -0,0 +1,33 @@
package com.sismics.docs.ui.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.ListView;
/**
* Non-scrollable ListView.
* All items are visible from the start.
*
* @author http://stackoverflow.com/questions/18813296/non-scrollable-listview-inside-scrollview/24629341#24629341
*/
public class NonScrollListView extends ListView {
public NonScrollListView(Context context) {
super(context);
}
public NonScrollListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NonScrollListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
ViewGroup.LayoutParams params = getLayoutParams();
params.height = getMeasuredHeight();
}
}

View File

@@ -1,9 +1,14 @@
package com.sismics.docs.util;
import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
/**
* Utility class for network actions.
@@ -19,9 +24,14 @@ public class NetworkUtil {
* @param title Notification title
* @param description Notification description
*/
public static void downloadFile(Context context, String url, String fileName, String title, String description) {
String authToken = PreferenceUtil.getAuthToken(context);
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
public static void downloadFile(Activity activity, String url, String fileName, String title, String description) {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 0);
return;
}
String authToken = PreferenceUtil.getAuthToken(activity);
DownloadManager downloadManager = (DownloadManager) activity.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);

View File

@@ -126,6 +126,7 @@ public class PreferenceUtil {
/**
* Returns auth token cookie from shared preferences.
*
* @param context Context
* @return Auth token
*/
public static String getAuthToken(Context context) {
@@ -140,6 +141,16 @@ public class PreferenceUtil {
return null;
}
/**
* Clear all auth tokens.
*
* @param context Context
*/
public static void clearAuthToken(Context context) {
PersistentCookieStore cookieStore = new PersistentCookieStore(context);
cookieStore.removeAll();
}
/**
* Returns cleaned server URL.
*

View File

@@ -0,0 +1,85 @@
package com.sismics.docs.util;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Utility class for spannable.
*
* @author bgamard.
*/
public class SpannableUtil {
/**
* Create a colored spannable from tags.
*
* @param tags Tags
* @return Colored spannable
*/
public static Spannable buildSpannableTags(JSONArray tags) {
return buildSpannable(tags, "name", "color");
}
/**
* Create a spannable for contributors.
*
* @param contributors Contributors
* @return Spannable
*/
public static Spannable buildSpannableContributors(JSONArray contributors) {
return buildSpannable(contributors, "username", null);
}
/**
* Create a spannable for relations.
*
* @param relations Relations
* @return Spannable
*/
public static Spannable buildSpannableRelations(JSONArray relations) {
return buildSpannable(relations, "title", null);
}
/**
* Create a spannable from a JSONArray.
*
* @param array JSONArray
* @param valueName Name of the value part
* @param colorName Name of the color part (optional)
* @return Spannable
*/
private static Spannable buildSpannable(JSONArray array, String valueName, String colorName) {
SpannableStringBuilder builder = new SpannableStringBuilder();
for (int i = 0; i < array.length(); i++) {
final JSONObject tag = array.optJSONObject(i);
int start = builder.length();
builder.append(" ").append(tag.optString(valueName)).append(" ");
builder.setSpan(new ForegroundColorSpan(Color.WHITE), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new BackgroundColorSpan(Color.parseColor(tag.optString(colorName, "#5bc0de"))), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
/*
TODO : Make tags, relations and contributors clickable
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(Color.WHITE);
ds.setUnderlineText(false);
}
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);*/
builder.append(" ");
}
return builder;
}
}

View File

@@ -1,39 +0,0 @@
package com.sismics.docs.util;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Utility class for tags.
*
* @author bgamard.
*/
public class TagUtil {
/**
* Create a colored spannable from tags.
*
* @param tags Tags
* @return Colored spannable
*/
public static Spannable buildSpannable(JSONArray tags) {
SpannableStringBuilder builder = new SpannableStringBuilder();
for (int i = 0; i < tags.length(); i++) {
JSONObject tag = tags.optJSONObject(i);
int start = builder.length();
builder.append(" ").append(tag.optString("name")).append(" ");
builder.setSpan(new ForegroundColorSpan(Color.WHITE), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new BackgroundColorSpan(Color.parseColor(tag.optString("color"))), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.append(" ");
}
return builder;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 B

View File

@@ -142,241 +142,523 @@
<!-- Right drawer -->
<LinearLayout
<ScrollView
android:id="@+id/right_drawer"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:orientation="vertical"
android:clickable="true"
android:background="#fff"
android:elevation="5dp">
<!-- Actions -->
android:elevation="5dp"
android:layout_gravity="end">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
android:orientation="vertical">
<!-- Actions -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
style="?android:buttonBarStyle">
android:orientation="vertical"
android:gravity="center">
<Button
android:id="@+id/actionEditDocument"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_create_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/edit_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
android:orientation="horizontal"
style="?android:buttonBarStyle">
<Button
android:id="@+id/actionUploadFile"
<Button
android:id="@+id/actionDownload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_file_download_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/download_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
<Button
android:id="@+id/actionExportPdf"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_description_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/export_pdf"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
<Button
android:id="@+id/actionAuditLog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_assignment_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/activity"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_file_upload_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/upload_file"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
android:orientation="horizontal"
style="?android:buttonBarStyle">
<Button
android:id="@+id/actionDownload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_file_download_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/download_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
<Button
android:id="@+id/actionEditDocument"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_create_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/edit_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<Button
android:id="@+id/actionUploadFile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_file_upload_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/upload_file"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<Button
android:id="@+id/actionSharing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_share_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/share"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<Button
android:id="@+id/actionDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_delete_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/delete_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:background="#eee"/>
<!-- Document metadata -->
<RelativeLayout
android:id="@+id/detailLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
style="?android:buttonBarStyle">
android:padding="12dp">
<Button
android:id="@+id/actionExportPdf"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_description_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/export_pdf"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<TextView
android:id="@+id/createdDateLabel"
android:layout_width="100dp"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/created_date"/>
<Button
android:id="@+id/actionSharing"
<TextView
android:id="@+id/createdDateTextView"
android:layout_toRightOf="@id/createdDateLabel"
android:layout_toEndOf="@id/createdDateLabel"
android:layout_toLeftOf="@id/sharedImageView"
android:layout_toStartOf="@id/sharedImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_share_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/share"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:fontFamily="sans-serif-light"/>
<Button
android:id="@+id/actionAuditLog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_assignment_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/activity"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<TextView
android:id="@+id/creatorLabel"
android:layout_width="100dp"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_below="@+id/createdDateLabel"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/creator"/>
<Button
android:id="@+id/actionDelete"
<TextView
android:id="@+id/creatorTextView"
android:layout_toRightOf="@id/creatorLabel"
android:layout_toEndOf="@id/creatorLabel"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_below="@+id/createdDateTextView"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/tagTextView"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_delete_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/delete_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
android:layout_below="@id/creatorLabel"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/descriptionTextView"
android:layout_marginTop="12dp"
android:layout_below="@id/tagTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
<ImageView
android:contentDescription="@string/shared"
android:id="@+id/sharedImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_folder_shared_grey600_24dp"
android:layout_alignParentTop="true"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_toLeftOf="@+id/languageImageView"
android:layout_toStartOf="@+id/languageImageView"/>
<ImageView
android:contentDescription="@string/language"
android:id="@+id/languageImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"/>
</RelativeLayout>
<!-- Additional dublincore metadata -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingBottom="12dp">
<!-- Subject -->
<LinearLayout
android:id="@+id/subjectLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/subject"/>
<TextView
android:id="@+id/subjectTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Identifier -->
<LinearLayout
android:id="@+id/identifierLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/identifier"/>
<TextView
android:id="@+id/identifierTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Publisher -->
<LinearLayout
android:id="@+id/publisherLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/publisher"/>
<TextView
android:id="@+id/publisherTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Format -->
<LinearLayout
android:id="@+id/formatLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/format"/>
<TextView
android:id="@+id/formatTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Source -->
<LinearLayout
android:id="@+id/sourceLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/source"/>
<TextView
android:id="@+id/sourceTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Type -->
<LinearLayout
android:id="@+id/typeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/type"/>
<TextView
android:id="@+id/typeTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Coverage -->
<LinearLayout
android:id="@+id/coverageLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/coverage"/>
<TextView
android:id="@+id/coverageTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Rights -->
<LinearLayout
android:id="@+id/rightsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/rights"/>
<TextView
android:id="@+id/rightsTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Contributors -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/contributors"/>
<TextView
android:id="@+id/contributorsTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Relations -->
<LinearLayout
android:id="@+id/relationsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="2dp">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/relations"/>
<TextView
android:id="@+id/relationsTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:background="#eee"/>
<!-- ACLs -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#de000000"
android:text="@string/who_can_access"
android:layout_margin="12dp"/>
<com.sismics.docs.ui.view.NonScrollListView
android:id="@+id/aclListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:background="#eee"/>
<!-- Document metadata -->
<RelativeLayout
android:id="@+id/detailLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:id="@+id/createdDateLabel"
android:layout_width="100dp"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/created_date"/>
<TextView
android:id="@+id/createdDateTextView"
android:layout_toRightOf="@id/createdDateLabel"
android:layout_toEndOf="@id/createdDateLabel"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/creatorLabel"
android:layout_width="100dp"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_below="@+id/createdDateLabel"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/creator"/>
<TextView
android:id="@+id/creatorTextView"
android:layout_toRightOf="@id/creatorLabel"
android:layout_toEndOf="@id/creatorLabel"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_below="@+id/createdDateTextView"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/tagTextView"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/creatorLabel"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/descriptionTextView"
android:layout_marginTop="12dp"
android:layout_below="@id/tagTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
<ImageView
android:id="@+id/sharedImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_folder_shared_grey600_24dp"
android:layout_alignParentTop="true"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_toLeftOf="@+id/languageImageView"
android:layout_toStartOf="@+id/languageImageView"/>
<ImageView
android:id="@+id/languageImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"/>
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:background="#eee"/>
<!-- ACLs -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#de000000"
android:text="@string/who_can_access"
android:layout_margin="12dp"/>
<ListView
android:id="@+id/aclListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"/>
</LinearLayout>
</ScrollView>
</android.support.v4.widget.DrawerLayout>

View File

@@ -29,16 +29,4 @@
android:text="Appartement"
android:textSize="14sp"/>
<TextView
android:id="@+id/tagCountTextView"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:textColor="#888"
android:text="5"
android:textSize="14sp"/>
</RelativeLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Validation -->
<string name="validate_error_email">Email invalide</string>
<string name="validate_error_length_min">Trop court (min. %d)</string>
<string name="validate_error_length_max">Trop long (max. %d)</string>
<string name="validate_error_required">Requis</string>
<string name="validate_error_alphanumeric">Seuls les lettres et les nombres sont autorisés</string>
<!-- App -->
<string name="drawer_open">Ouvrir le menu de navigation</string>
<string name="drawer_close">Fermer le menu de navigation</string>
<string name="login_explain"><![CDATA[Pour commencer, vous devez télécharger et installer le serveur Sismics Docs sur <a href="https://github.com/sismics/docs">github.com/sismics/docs</a> et entrer son URL ci-dessous]]></string>
<string name="server">Serveur</string>
<string name="username">Nom d\'utilisateur</string>
<string name="password">Mot de passe</string>
<string name="login">Connexion</string>
<string name="ok">OK</string>
<string name="cancel">Annuler</string>
<string name="login_fail_title">Echec de la connexion</string>
<string name="login_fail">Mauvais nom d\'utilisateur ou de mot de passe</string>
<string name="network_error_title">Erreur réseau</string>
<string name="network_error">Erreur réseau, veuillez vérifier votre connexion internet et l\'URL du serveur</string>
<string name="invalid_url_title">URL invalide</string>
<string name="invalid_url">Veuillez vérifier l\URL du serveur et réessayer</string>
<string name="crash_toast_text">Une erreur s\'est produite, un rapport a été envoyé afin de corriger ce problème</string>
<string name="created_date">Date création</string>
<string name="download_file">Télécharger ce fichier</string>
<string name="download_document">Télécharger</string>
<string name="action_search">Rechercher dans les documents</string>
<string name="all_documents">Tous les documents</string>
<string name="shared_documents">Documents partagés</string>
<string name="all_tags">Tous les tags</string>
<string name="no_tags">Aucun tag</string>
<string name="error_loading_tags">Erreur de chargement des tags</string>
<string name="no_documents">Aucun document</string>
<string name="error_loading_documents">Erreur de chargement des documents</string>
<string name="no_files">Aucun fichier</string>
<string name="error_loading_files">Erreur de chargement des fichiers</string>
<string name="new_document">Nouveau document</string>
<string name="share">Partage</string>
<string name="close">Fermer</string>
<string name="add">Ajouter</string>
<string name="add_share_hint">Nom du partage (facultatif)</string>
<string name="document_not_shared">Ce document n\'est pas partagé</string>
<string name="delete_share">Supprimer ce partage</string>
<string name="send_share">Envoi ce lien de partage</string>
<string name="error_loading_shares">Erreur de chargement des partages</string>
<string name="error_adding_share">Erreur lors de l\'ajout du partage</string>
<string name="share_default_name">Lien de partage</string>
<string name="error_deleting_share">Erreur lors de la suppression de ce partage</string>
<string name="send_share_to">Envoi ce lien de partage à</string>
<string name="upload_file">Aj. fichier</string>
<string name="upload_from">Envoyer un fichier depuis</string>
<string name="settings">Paramètres</string>
<string name="logout">Déconnexion</string>
<string name="version">Version</string>
<string name="build">Build</string>
<string name="pref_advanced_category">Paramètres avancés</string>
<string name="pref_about_category">A propors</string>
<string name="pref_github">GitHub</string>
<string name="pref_issue">Rapporter un bug</string>
<string name="pref_clear_cache_title">Vider le cache</string>
<string name="pref_clear_cache_summary">Nettoyer les fichiers en cache</string>
<string name="pref_clear_cache_success">Cache vidé</string>
<string name="pref_clear_history_title">Vider l\'historique de recherche</string>
<string name="pref_clear_history_summary">Supprimer les recherches récentes</string>
<string name="pref_clear_history_success">Historique de recherche vidé</string>
<string name="pref_cache_size">Taille du cache</string>
<string name="save">Enregistrer</string>
<string name="edit_document">Modifier</string>
<string name="error_editing_document">Erreur réseau, veuillez réessayer</string>
<string name="please_wait">Veuillez patienter</string>
<string name="document_editing_message">Envoi des données</string>
<string name="delete_document">Suppr.</string>
<string name="delete_document_title">Supprimer le document</string>
<string name="delete_document_message">Etes-vous sûr de vouloir supprimer ce document et tous les fichiers associés ?</string>
<string name="document_delete_failure">Erreur réseau lors de la suppression de ce document</string>
<string name="document_deleting_message">Suppression du document</string>
<string name="delete_file_title">Supprimer le fichier</string>
<string name="delete_file_message">Etes-vous sûr de vouloir supprimer ce fichier ?</string>
<string name="file_delete_failure">Erreur réseau lors de la suppression du fichier</string>
<string name="file_deleting_message">Suppression du fichier</string>
<string name="error_reading_file">Erreur lors de la lecture du fichier</string>
<string name="upload_notification_title">Sismics Docs</string>
<string name="upload_notification_message">Envoi du nouveau fichier</string>
<string name="upload_notification_error">Erreur lors de l\'envoi du nouveau fichier</string>
<string name="delete_file">Supprimer ce fichier</string>
<string name="advanced_search">Recherche avancée</string>
<string name="search">Rechercher</string>
<string name="add_tags">Ajouter des tags</string>
<string name="creation_date">Date de création</string>
<string name="description">Description</string>
<string name="title">Titre</string>
<string name="simple_search">Recherche simple</string>
<string name="fulltext_search">Recherche texte intégral</string>
<string name="creator">Créateur</string>
<string name="after_date">Après cette date</string>
<string name="before_date">Avant cette date</string>
<string name="search_tags">Rechercher dans les tags</string>
<string name="all_languages">Toutes les langues</string>
<string name="toggle_informations">Afficher/masquer les informations</string>
<string name="who_can_access">Qui a accès</string>
<string name="comments">Commentaires</string>
<string name="no_comments">Aucun commentaire</string>
<string name="error_loading_comments">Erreur de chargement des commentaires</string>
<string name="send">Envoyer</string>
<string name="add_comment">Ajouter un commentaire</string>
<string name="comment_add_failure">Erreur lors de l\'ajout du commentaire</string>
<string name="adding_comment">Ajout du commentaire</string>
<string name="comment_delete">Supprimer le commentaire</string>
<string name="deleting_comment">Suppression du commentaire</string>
<string name="error_deleting_comment">Erreur lors de la suppression du commentaire</string>
<string name="export_pdf">PDF</string>
<string name="download">Télécharger</string>
<string name="margin">Marge</string>
<string name="fit_image_to_page">Ajuster les images à la page</string>
<string name="export_comments">Exporter les commentaires</string>
<string name="export_metadata">Exporter les métadonnées</string>
<string name="mm">mm</string>
<string name="download_file_title">Export de fichier Sismics Docs</string>
<string name="download_document_title">Export de document Sismics Docs</string>
<string name="download_pdf_title">Export PDF Sismics Docs</string>
<string name="latest_activity">Activité récente</string>
<string name="activity">Activité</string>
<string name="email">E-mail</string>
<string name="storage_quota">Quota de stockage</string>
<string name="storage_display">%1$d/%2$d Mo</string>
<string name="validation_code">Code de validation</string>
<string name="shared">Partagé</string>
<string name="language">Langue</string>
<string name="coverage">Couverture</string>
<string name="type">Type</string>
<string name="source">Source</string>
<string name="format">Format</string>
<string name="publisher">Editeur</string>
<string name="identifier">Identifiant</string>
<string name="subject">Sujet</string>
<string name="rights">Droits</string>
<string name="contributors">Contributeurs</string>
<string name="relations">Relations</string>
</resources>

View File

@@ -9,10 +9,10 @@
<string name="validate_error_alphanumeric">Only letters and numbers</string>
<!-- App -->
<string name="app_name">Sismics Docs</string>
<string name="app_name" translatable="false">Sismics Docs</string>
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>
<string name="login_explain"><![CDATA[To start, you must download and install Sismics Docs Server on <a href="http://www.sismics.com/docs">www.sismics.com/docs</a> and enter your server URL below]]></string>
<string name="login_explain"><![CDATA[To start, you must download and install Sismics Docs Server on <a href="https://github.com/sismics/docs">github.com/sismics/docs</a> and enter its below]]></string>
<string name="server">Server</string>
<string name="username">Username</string>
<string name="password">Password</string>
@@ -46,7 +46,7 @@
<string name="add_share_hint">Name the share (optional)</string>
<string name="document_not_shared">This document is not currently shared</string>
<string name="delete_share">Delete this share</string>
<string name="send_share">Send this share</string>
<string name="send_share">Send this share link</string>
<string name="error_loading_shares">Error loading shares</string>
<string name="error_adding_share">Error adding share</string>
<string name="share_default_name">Share link</string>
@@ -69,9 +69,8 @@
<string name="pref_clear_history_summary">Wipe the recent search suggestions</string>
<string name="pref_clear_history_success">Search history cleared</string>
<string name="pref_cache_size">Cache size</string>
<string name="language_french">French</string>
<string name="language_english">English</string>
<string name="language_japanese">Japanese</string>
<string name="language_french" translatable="false">Français</string>
<string name="language_english" translatable="false">English</string>
<string name="save">Save</string>
<string name="edit_document">Edit</string>
<string name="error_editing_document">Network error, please try again</string>
@@ -132,5 +131,17 @@
<string name="storage_quota">Storage quota</string>
<string name="storage_display">%1$d/%2$d MB</string>
<string name="validation_code">Validation code</string>
<string name="shared">Shared</string>
<string name="language">Language</string>
<string name="coverage">Coverage</string>
<string name="type">Type</string>
<string name="source">Source</string>
<string name="format">Format</string>
<string name="publisher">Publisher</string>
<string name="identifier">Identifier</string>
<string name="subject">Subject</string>
<string name="rights">Rights</string>
<string name="contributors">Contributors</string>
<string name="relations">Relations</string>
</resources>

View File

@@ -12,6 +12,8 @@
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx3072m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

View File

@@ -1,6 +1,6 @@
#Sat Jan 16 19:15:13 CET 2016
#Tue Nov 14 23:55:56 CET 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.sismics.docs</groupId>
<artifactId>docs-parent</artifactId>
<version>1.4</version>
<version>1.5</version>
<relativePath>..</relativePath>
</parent>
@@ -52,6 +52,26 @@
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-email</artifactId>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
@@ -92,6 +112,11 @@
<artifactId>lucene-queryparser</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</dependency>
<!-- Only there to read old index and rebuild them -->
<dependency>
<groupId>org.apache.lucene</groupId>

View File

@@ -0,0 +1,18 @@
package com.sismics.docs.core.constant;
/**
* ACL type.
*
* @author bgamard
*/
public enum AclType {
/**
* User created ACL.
*/
USER,
/**
* ACL created by the routing module.
*/
ROUTING
}

View File

@@ -13,5 +13,34 @@ public enum ConfigType {
/**
* Theme configuration.
*/
THEME
THEME,
/**
* Guest login.
*/
GUEST_LOGIN,
/**
* Default language.
*/
DEFAULT_LANGUAGE,
/**
* SMTP server configuration.
*/
SMTP_HOSTNAME,
SMTP_PORT,
SMTP_FROM,
SMTP_USERNAME,
SMTP_PASSWORD,
/**
* Inbox scanning configuration.
*/
INBOX_ENABLED,
INBOX_HOSTNAME,
INBOX_PORT,
INBOX_USERNAME,
INBOX_PASSWORD,
INBOX_TAG
}

View File

@@ -1,9 +1,9 @@
package com.sismics.docs.core.constant;
import java.util.List;
import com.google.common.collect.Lists;
import java.util.List;
/**
* Application constants.
*
@@ -30,6 +30,11 @@ public class Constants {
*/
public static final String LUCENE_DIRECTORY_STORAGE_FILE = "FILE";
/**
* Guest user ID.
*/
public static final String GUEST_USER_ID = "guest";
/**
* Default generic user role.
*/
@@ -38,5 +43,48 @@ public class Constants {
/**
* Supported document languages.
*/
public static final List<String> SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "jpn");
public static final List<String> SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor");
/**
* Base URL environment variable.
*/
public static final String BASE_URL_ENV = "DOCS_BASE_URL";
/**
* Default language environment variable.
*/
public static final String DEFAULT_LANGUAGE_ENV = "DOCS_DEFAULT_LANGUAGE";
/**
* SMTP configuration environment variables.
*/
public static final String SMTP_HOSTNAME_ENV = "DOCS_SMTP_HOSTNAME";
public static final String SMTP_PORT_ENV = "DOCS_SMTP_PORT";
public static final String SMTP_USERNAME_ENV = "DOCS_SMTP_USERNAME";
public static final String SMTP_PASSWORD_ENV = "DOCS_SMTP_PASSWORD";
/**
* Global quota environment variable.
*/
public static final String GLOBAL_QUOTA_ENV = "DOCS_GLOBAL_QUOTA";
/**
* Initial admin password environment variable.
*/
public static final String ADMIN_PASSWORD_INIT_ENV = "DOCS_ADMIN_PASSWORD_INIT";
/**
* Expiration time of the password recovery in hours.
*/
public static final int PASSWORD_RECOVERY_EXPIRATION_HOUR = 2;
/**
* Email template for password recovery.
*/
public static final String EMAIL_TEMPLATE_PASSWORD_RECOVERY = "password_recovery";
/**
* Email template for route step validate.
*/
public static final String EMAIL_TEMPLATE_ROUTE_STEP_VALIDATE = "route_step_validate";
}

View File

@@ -0,0 +1,23 @@
package com.sismics.docs.core.constant;
/**
* Route step transitions.
*
* @author bgamard
*/
public enum RouteStepTransition {
/**
* Route step approved.
*/
APPROVED,
/**
* Route step rejected.
*/
REJECTED,
/**
* Route step validated.
*/
VALIDATED
}

View File

@@ -0,0 +1,18 @@
package com.sismics.docs.core.constant;
/**
* Route step types.
*
* @author bgamard
*/
public enum RouteStepType {
/**
* Approval step with 2 choices.
*/
APPROVE,
/**
* Simple validation step, no possible choice.
*/
VALIDATE
}

View File

@@ -1,14 +1,7 @@
package com.sismics.docs.core.dao.jpa;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import com.sismics.docs.core.constant.AclTargetType;
import com.sismics.docs.core.constant.AclType;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.jpa.dto.AclDto;
@@ -16,6 +9,13 @@ import com.sismics.docs.core.model.jpa.Acl;
import com.sismics.docs.core.util.AuditLogUtil;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* ACL DAO.
*
@@ -65,19 +65,20 @@ public class AclDao {
* @return ACL DTO list
*/
@SuppressWarnings("unchecked")
public List<AclDto> getBySourceId(String sourceId) {
public List<AclDto> getBySourceId(String sourceId, AclType type) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
StringBuilder sb = new StringBuilder("select a.ACL_ID_C, a.ACL_PERM_C, a.ACL_TARGETID_C, ");
sb.append(" u.USE_USERNAME_C, s.SHA_ID_C, s.SHA_NAME_C, g.GRP_NAME_C ");
sb.append(" from T_ACL a ");
sb.append(" left join T_USER u on u.USE_ID_C = a.ACL_TARGETID_C ");
sb.append(" left join T_SHARE s on s.SHA_ID_C = a.ACL_TARGETID_C ");
sb.append(" left join T_GROUP g on g.GRP_ID_C = a.ACL_TARGETID_C ");
sb.append(" where a.ACL_DELETEDATE_D is null and a.ACL_SOURCEID_C = :sourceId ");
StringBuilder sb = new StringBuilder("select a.ACL_ID_C, a.ACL_PERM_C, a.ACL_TARGETID_C, ")
.append(" u.USE_USERNAME_C, s.SHA_ID_C, s.SHA_NAME_C, g.GRP_NAME_C ")
.append(" from T_ACL a ")
.append(" left join T_USER u on u.USE_ID_C = a.ACL_TARGETID_C ")
.append(" left join T_SHARE s on s.SHA_ID_C = a.ACL_TARGETID_C ")
.append(" left join T_GROUP g on g.GRP_ID_C = a.ACL_TARGETID_C ")
.append(" where a.ACL_DELETEDATE_D is null and a.ACL_SOURCEID_C = :sourceId and a.ACL_TYPE_C = :type ");
// Perform the query
Query q = em.createNativeQuery(sb.toString());
q.setParameter("sourceId", sourceId);
q.setParameter("type", type.name());
List<Object[]> l = q.getResultList();
// Assemble results
@@ -141,26 +142,29 @@ public class AclDao {
* @param perm Permission
* @param targetId Target ID
* @param userId User ID
* @param type Type
*/
@SuppressWarnings("unchecked")
public void delete(String sourceId, PermType perm, String targetId, String userId) {
public void delete(String sourceId, PermType perm, String targetId, String userId, AclType type) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Create audit log
Query q = em.createQuery("from Acl a where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId");
Query q = em.createQuery("from Acl a where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId and a.type = :type and a.deleteDate is null");
q.setParameter("sourceId", sourceId);
q.setParameter("perm", perm);
q.setParameter("targetId", targetId);
q.setParameter("type", type);
List<Acl> aclList = q.getResultList();
for (Acl acl : aclList) {
AuditLogUtil.create(acl, AuditLogType.DELETE, userId);
}
// Soft delete the ACLs
q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId");
q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId and a.type = :type and a.deleteDate is null");
q.setParameter("sourceId", sourceId);
q.setParameter("perm", perm);
q.setParameter("targetId", targetId);
q.setParameter("type", type);
q.setParameter("dateNow", new Date());
q.executeUpdate();
}

View File

@@ -1,18 +1,5 @@
package com.sismics.docs.core.dao.jpa;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.sismics.docs.core.constant.AuditLogType;
@@ -28,6 +15,12 @@ import com.sismics.docs.core.util.jpa.QueryParam;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import java.sql.Timestamp;
import java.util.*;
/**
* Document DAO.
*
@@ -224,7 +217,7 @@ public class DocumentDao {
if (!Strings.isNullOrEmpty(criteria.getSearch()) || !Strings.isNullOrEmpty(criteria.getFullSearch())) {
LuceneDao luceneDao = new LuceneDao();
Set<String> documentIdList = luceneDao.search(criteria.getSearch(), criteria.getFullSearch());
if (documentIdList.size() == 0) {
if (documentIdList.isEmpty()) {
// If the search doesn't find any document, the request should return nothing
documentIdList.add(UUID.randomUUID().toString());
}
@@ -301,25 +294,36 @@ public class DocumentDao {
// Get the document
Query q = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null");
q.setParameter("id", document.getId());
Document documentFromDb = (Document) q.getSingleResult();
Document documentDb = (Document) q.getSingleResult();
// Update the document
documentFromDb.setTitle(document.getTitle());
documentFromDb.setDescription(document.getDescription());
documentFromDb.setSubject(document.getSubject());
documentFromDb.setIdentifier(document.getIdentifier());
documentFromDb.setPublisher(document.getPublisher());
documentFromDb.setFormat(document.getFormat());
documentFromDb.setSource(document.getSource());
documentFromDb.setType(document.getType());
documentFromDb.setCoverage(document.getCoverage());
documentFromDb.setRights(document.getRights());
documentFromDb.setCreateDate(document.getCreateDate());
documentFromDb.setLanguage(document.getLanguage());
documentDb.setTitle(document.getTitle());
documentDb.setDescription(document.getDescription());
documentDb.setSubject(document.getSubject());
documentDb.setIdentifier(document.getIdentifier());
documentDb.setPublisher(document.getPublisher());
documentDb.setFormat(document.getFormat());
documentDb.setSource(document.getSource());
documentDb.setType(document.getType());
documentDb.setCoverage(document.getCoverage());
documentDb.setRights(document.getRights());
documentDb.setCreateDate(document.getCreateDate());
documentDb.setLanguage(document.getLanguage());
// Create audit log
AuditLogUtil.create(documentFromDb, AuditLogType.UPDATE, userId);
AuditLogUtil.create(documentDb, AuditLogType.UPDATE, userId);
return documentFromDb;
return documentDb;
}
/**
* Returns the number of documents.
*
* @return Number of documents
*/
public long getDocumentCount() {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query query = em.createNativeQuery("select count(d.DOC_ID_C) from T_DOCUMENT d where d.DOC_DELETEDATE_D is null");
return ((Number) query.getSingleResult()).longValue();
}
}

View File

@@ -1,18 +1,17 @@
package com.sismics.docs.core.dao.jpa;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.AuditLogUtil;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* File DAO.
*
@@ -137,13 +136,13 @@ public class FileDao {
// Get the file
Query q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null");
q.setParameter("id", file.getId());
File fileFromDb = (File) q.getSingleResult();
File fileDb = (File) q.getSingleResult();
// Update the file
fileFromDb.setDocumentId(file.getDocumentId());
fileFromDb.setContent(file.getContent());
fileFromDb.setOrder(file.getOrder());
fileFromDb.setMimeType(file.getMimeType());
fileDb.setDocumentId(file.getDocumentId());
fileDb.setContent(file.getContent());
fileDb.setOrder(file.getOrder());
fileDb.setMimeType(file.getMimeType());
return file;
}

View File

@@ -1,18 +1,5 @@
package com.sismics.docs.core.dao.jpa;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import com.google.common.base.Joiner;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria;
@@ -25,6 +12,11 @@ import com.sismics.docs.core.util.jpa.QueryUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import java.util.*;
/**
* Group DAO.
*
@@ -266,16 +258,16 @@ public class GroupDao {
// Get the group
Query q = em.createQuery("select g from Group g where g.id = :id and g.deleteDate is null");
q.setParameter("id", group.getId());
Group groupFromDb = (Group) q.getSingleResult();
Group groupDb = (Group) q.getSingleResult();
// Update the group
groupFromDb.setName(group.getName());
groupFromDb.setParentId(group.getParentId());
groupDb.setName(group.getName());
groupDb.setParentId(group.getParentId());
// Create audit log
AuditLogUtil.create(groupFromDb, AuditLogType.UPDATE, userId);
AuditLogUtil.create(groupDb, AuditLogType.UPDATE, userId);
return groupFromDb;
return groupDb;
}
}

View File

@@ -0,0 +1,68 @@
package com.sismics.docs.core.dao.jpa;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.model.jpa.PasswordRecovery;
import com.sismics.util.context.ThreadLocalContext;
import org.joda.time.DateTime;
import org.joda.time.DurationFieldType;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import java.util.Date;
import java.util.UUID;
/**
* Password recovery DAO.
*
* @author jtremeaux
*/
public class PasswordRecoveryDao {
/**
* Create a new password recovery request.
*
* @param passwordRecovery Password recovery
* @return Unique identifier
*/
public String create(PasswordRecovery passwordRecovery) {
passwordRecovery.setId(UUID.randomUUID().toString());
passwordRecovery.setCreateDate(new Date());
EntityManager em = ThreadLocalContext.get().getEntityManager();
em.persist(passwordRecovery);
return passwordRecovery.getId();
}
/**
* Search an active password recovery by unique identifier.
*
* @param id Unique identifier
* @return Password recovery
*/
public PasswordRecovery getActiveById(String id) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
try {
Query q = em.createQuery("select r from PasswordRecovery r where r.id = :id and r.createDate > :createDateMin and r.deleteDate is null");
q.setParameter("id", id);
q.setParameter("createDateMin", new DateTime().withFieldAdded(DurationFieldType.hours(), -1 * Constants.PASSWORD_RECOVERY_EXPIRATION_HOUR).toDate());
return (PasswordRecovery) q.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
/**
* Deletes active password recovery by username.
*
* @param username Username
*/
public void deleteActiveByLogin(String username) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query q = em.createQuery("update PasswordRecovery r set r.deleteDate = :deleteDate where r.username = :username and r.createDate > :createDateMin and r.deleteDate is null");
q.setParameter("username", username);
q.setParameter("deleteDate", new Date());
q.setParameter("createDateMin", new DateTime().withFieldAdded(DurationFieldType.hours(), -1 * Constants.PASSWORD_RECOVERY_EXPIRATION_HOUR).toDate());
q.executeUpdate();
}
}

View File

@@ -0,0 +1,108 @@
package com.sismics.docs.core.dao.jpa;
import com.google.common.base.Joiner;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.dao.jpa.criteria.RouteCriteria;
import com.sismics.docs.core.dao.jpa.dto.RouteDto;
import com.sismics.docs.core.model.jpa.Route;
import com.sismics.docs.core.util.AuditLogUtil;
import com.sismics.docs.core.util.jpa.QueryParam;
import com.sismics.docs.core.util.jpa.QueryUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import java.sql.Timestamp;
import java.util.*;
/**
* Route DAO.
*
* @author bgamard
*/
public class RouteDao {
/**
* Creates a new route.
*
* @param route Route
* @param userId User ID
* @return New ID
*/
public String create(Route route, String userId) {
// Create the UUID
route.setId(UUID.randomUUID().toString());
// Create the route
EntityManager em = ThreadLocalContext.get().getEntityManager();
route.setCreateDate(new Date());
em.persist(route);
// Create audit log
AuditLogUtil.create(route, AuditLogType.CREATE, userId);
return route.getId();
}
/**
* Returns the list of all routes.
*
* @param criteria Search criteria
* @param sortCriteria Sort criteria
* @return List of routes
*/
public List<RouteDto> findByCriteria(RouteCriteria criteria, SortCriteria sortCriteria) {
Map<String, Object> parameterMap = new HashMap<String, Object>();
List<String> criteriaList = new ArrayList<>();
StringBuilder sb = new StringBuilder("select r.RTE_ID_C c0, r.RTE_NAME_C c1, r.RTE_CREATEDATE_D c2");
sb.append(" from T_ROUTE r ");
// Add search criterias
if (criteria.getDocumentId() != null) {
criteriaList.add("r.RTE_IDDOCUMENT_C = :documentId");
parameterMap.put("documentId", criteria.getDocumentId());
}
criteriaList.add("r.RTE_DELETEDATE_D is null");
if (!criteriaList.isEmpty()) {
sb.append(" where ");
sb.append(Joiner.on(" and ").join(criteriaList));
}
// Perform the search
QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria);
@SuppressWarnings("unchecked")
List<Object[]> l = QueryUtil.getNativeQuery(queryParam).getResultList();
// Assemble results
List<RouteDto> dtoList = new ArrayList<>();
for (Object[] o : l) {
int i = 0;
RouteDto dto = new RouteDto();
dto.setId((String) o[i++]);
dto.setName((String) o[i++]);
dto.setCreateTimestamp(((Timestamp) o[i++]).getTime());
dtoList.add(dto);
}
return dtoList;
}
/**
* Deletes a route and the associated steps.
*
* @param routeId Route ID
*/
public void deleteRoute(String routeId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
em.createNativeQuery("update T_ROUTE_STEP rs set rs.RTP_DELETEDATE_D = :dateNow where rs.RTP_IDROUTE_C = :routeId and rs.RTP_DELETEDATE_D is null")
.setParameter("routeId", routeId)
.setParameter("dateNow", new Date())
.executeUpdate();
em.createNativeQuery("update T_ROUTE r set r.RTE_DELETEDATE_D = :dateNow where r.RTE_ID_C = :routeId and r.RTE_DELETEDATE_D is null")
.setParameter("routeId", routeId)
.setParameter("dateNow", new Date())
.executeUpdate();
}
}

View File

@@ -0,0 +1,151 @@
package com.sismics.docs.core.dao.jpa;
import com.google.common.base.Joiner;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.dao.jpa.criteria.RouteModelCriteria;
import com.sismics.docs.core.dao.jpa.dto.RouteModelDto;
import com.sismics.docs.core.model.jpa.RouteModel;
import com.sismics.docs.core.util.AuditLogUtil;
import com.sismics.docs.core.util.jpa.QueryParam;
import com.sismics.docs.core.util.jpa.QueryUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import java.sql.Timestamp;
import java.util.*;
/**
* Route model DAO.
*
* @author bgamard
*/
public class RouteModelDao {
/**
* Creates a new route model.
*
* @param routeModel Route model
* @param userId User ID
* @return New ID
*/
public String create(RouteModel routeModel, String userId) {
// Create the UUID
routeModel.setId(UUID.randomUUID().toString());
// Create the route model
EntityManager em = ThreadLocalContext.get().getEntityManager();
routeModel.setCreateDate(new Date());
em.persist(routeModel);
// Create audit log
AuditLogUtil.create(routeModel, AuditLogType.CREATE, userId);
return routeModel.getId();
}
/**
* Update a route model.
*
* @param routeModel Route model to update
* @param userId User ID
* @return Updated route model
*/
public RouteModel update(RouteModel routeModel, String userId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the route model
Query q = em.createQuery("select r from RouteModel r where r.id = :id and r.deleteDate is null");
q.setParameter("id", routeModel.getId());
RouteModel routeModelDb = (RouteModel) q.getSingleResult();
// Update the group
routeModelDb.setName(routeModel.getName());
routeModelDb.setSteps(routeModel.getSteps());
// Create audit log
AuditLogUtil.create(routeModelDb, AuditLogType.UPDATE, userId);
return routeModelDb;
}
/**
* Gets an active route model by its ID.
*
* @param id Route model ID
* @return Route model
*/
public RouteModel getActiveById(String id) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
try {
Query q = em.createQuery("select r from RouteModel r where r.id = :id and r.deleteDate is null");
q.setParameter("id", id);
return (RouteModel) q.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
/**
* Deletes a route model.
*
* @param id Route model ID
* @param userId User ID
*/
public void delete(String id, String userId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the route model
Query q = em.createQuery("select r from RouteModel r where r.id = :id and r.deleteDate is null");
q.setParameter("id", id);
RouteModel routeModelDb = (RouteModel) q.getSingleResult();
// Delete the route model
Date dateNow = new Date();
routeModelDb.setDeleteDate(dateNow);
// Create audit log
AuditLogUtil.create(routeModelDb, AuditLogType.DELETE, userId);
}
/**
* Returns the list of all route models.
*
* @param criteria Search criteria
* @param sortCriteria Sort criteria
* @return List of route models
*/
public List<RouteModelDto> findByCriteria(RouteModelCriteria criteria, SortCriteria sortCriteria) {
Map<String, Object> parameterMap = new HashMap<String, Object>();
List<String> criteriaList = new ArrayList<>();
StringBuilder sb = new StringBuilder("select rm.RTM_ID_C c0, rm.RTM_NAME_C c1, rm.RTM_CREATEDATE_D c2");
sb.append(" from T_ROUTE_MODEL rm ");
// Add search criterias
criteriaList.add("rm.RTM_DELETEDATE_D is null");
if (!criteriaList.isEmpty()) {
sb.append(" where ");
sb.append(Joiner.on(" and ").join(criteriaList));
}
// Perform the search
QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria);
@SuppressWarnings("unchecked")
List<Object[]> l = QueryUtil.getNativeQuery(queryParam).getResultList();
// Assemble results
List<RouteModelDto> dtoList = new ArrayList<>();
for (Object[] o : l) {
int i = 0;
RouteModelDto dto = new RouteModelDto();
dto.setId((String) o[i++]);
dto.setName((String) o[i++]);
dto.setCreateTimestamp(((Timestamp) o[i++]).getTime());
dtoList.add(dto);
}
return dtoList;
}
}

View File

@@ -0,0 +1,155 @@
package com.sismics.docs.core.dao.jpa;
import com.google.common.base.Joiner;
import com.sismics.docs.core.constant.AclTargetType;
import com.sismics.docs.core.constant.RouteStepTransition;
import com.sismics.docs.core.constant.RouteStepType;
import com.sismics.docs.core.dao.jpa.criteria.RouteStepCriteria;
import com.sismics.docs.core.dao.jpa.dto.RouteStepDto;
import com.sismics.docs.core.model.jpa.RouteStep;
import com.sismics.docs.core.util.jpa.QueryParam;
import com.sismics.docs.core.util.jpa.QueryUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.sql.Timestamp;
import java.util.*;
/**
* Route step DAO.
*
* @author bgamard
*/
public class RouteStepDao {
/**
* Creates a new route step.
*
* @param routeStep Route step
* @return New ID
*/
public String create(RouteStep routeStep) {
// Create the UUID
routeStep.setId(UUID.randomUUID().toString());
// Create the route step
EntityManager em = ThreadLocalContext.get().getEntityManager();
routeStep.setCreateDate(new Date());
em.persist(routeStep);
return routeStep.getId();
}
/**
* Get the current route step from a document.
*
* @param documentId Document ID
* @return Current route step
*/
public RouteStepDto getCurrentStep(String documentId) {
List<RouteStepDto> routeStepDtoList = findByCriteria(new RouteStepCriteria()
.setDocumentId(documentId)
.setEndDateIsNull(true), new SortCriteria(6, true));
if (routeStepDtoList.isEmpty()) {
return null;
}
return routeStepDtoList.get(0);
}
/**
* Returns the list of all route steps.
*
* @param criteria Search criteria
* @param sortCriteria Sort criteria
* @return List of route steps
*/
public List<RouteStepDto> findByCriteria(RouteStepCriteria criteria, SortCriteria sortCriteria) {
Map<String, Object> parameterMap = new HashMap<>();
List<String> criteriaList = new ArrayList<>();
StringBuilder sb = new StringBuilder("select rs.RTP_ID_C, rs.RTP_NAME_C c0, rs.RTP_TYPE_C c1, rs.RTP_TRANSITION_C c2, rs.RTP_COMMENT_C c3, rs.RTP_IDTARGET_C c4, u.USE_USERNAME_C as targetUsername, g.GRP_NAME_C, rs.RTP_ENDDATE_D c5, uv.USE_USERNAME_C as validatorUsername, rs.RTP_IDROUTE_C, rs.RTP_ORDER_N c6")
.append(" from T_ROUTE_STEP rs ")
.append(" join T_ROUTE r on r.RTE_ID_C = rs.RTP_IDROUTE_C ")
.append(" left join T_USER uv on uv.USE_ID_C = rs.RTP_IDVALIDATORUSER_C ")
.append(" left join T_USER u on u.USE_ID_C = rs.RTP_IDTARGET_C ")
.append(" left join T_SHARE s on s.SHA_ID_C = rs.RTP_IDTARGET_C ")
.append(" left join T_GROUP g on g.GRP_ID_C = rs.RTP_IDTARGET_C ");
// Add search criterias
if (criteria.getDocumentId() != null) {
criteriaList.add("r.RTE_IDDOCUMENT_C = :documentId");
parameterMap.put("documentId", criteria.getDocumentId());
}
if (criteria.getRouteId() != null) {
criteriaList.add("rs.RTP_IDROUTE_C = :routeId");
parameterMap.put("routeId", criteria.getRouteId());
}
if (criteria.getEndDateIsNull() != null) {
criteriaList.add("RTP_ENDDATE_D is " + (criteria.getEndDateIsNull() ? "" : "not") + " null");
}
criteriaList.add("rs.RTP_DELETEDATE_D is null");
if (!criteriaList.isEmpty()) {
sb.append(" where ");
sb.append(Joiner.on(" and ").join(criteriaList));
}
// Perform the search
QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria);
@SuppressWarnings("unchecked")
List<Object[]> l = QueryUtil.getNativeQuery(queryParam).getResultList();
// Assemble results
List<RouteStepDto> dtoList = new ArrayList<>();
for (Object[] o : l) {
int i = 0;
RouteStepDto dto = new RouteStepDto();
dto.setId((String) o[i++]);
dto.setName((String) o[i++]);
dto.setType(RouteStepType.valueOf((String) o[i++]));
dto.setTransition((String) o[i++]);
dto.setComment((String) o[i++]);
dto.setTargetId((String) o[i++]);
String userName = (String) o[i++];
String groupName = (String) o[i++];
if (userName != null) {
dto.setTargetName(userName);
dto.setTargetType(AclTargetType.USER.name());
}
if (groupName != null) {
dto.setTargetName(groupName);
dto.setTargetType(AclTargetType.GROUP.name());
}
Timestamp endDateTimestamp = (Timestamp) o[i++];
dto.setEndDateTimestamp(endDateTimestamp == null ? null : endDateTimestamp.getTime());
dto.setValidatorUserName((String) o[i++]);
dto.setRouteId((String) o[i]);
dtoList.add(dto);
}
return dtoList;
}
/**
* End a route step.
*
* @param id Route step ID
* @param transition Transition
* @param comment Comment
* @param validatorUserId Validator user ID
*/
public void endRouteStep(String id, RouteStepTransition transition, String comment, String validatorUserId) {
StringBuilder sb = new StringBuilder("update T_ROUTE_STEP r ");
sb.append(" set r.RTP_ENDDATE_D = :endDate, r.RTP_TRANSITION_C = :transition, r.RTP_COMMENT_C = :comment, r.RTP_IDVALIDATORUSER_C = :validatorUserId ");
sb.append(" where r.RTP_ID_C = :id");
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query q = em.createNativeQuery(sb.toString());
q.setParameter("endDate", new Date());
q.setParameter("transition", transition.name());
q.setParameter("comment", comment);
q.setParameter("validatorUserId", validatorUserId);
q.setParameter("id", id);
q.executeUpdate();
}
}

View File

@@ -1,17 +1,5 @@
package com.sismics.docs.core.dao.jpa;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import com.google.common.base.Joiner;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.dao.jpa.criteria.TagCriteria;
@@ -24,6 +12,11 @@ import com.sismics.docs.core.util.jpa.QueryUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import java.util.*;
/**
* Tag DAO.
*
@@ -155,17 +148,17 @@ public class TagDao {
// Get the tag
Query q = em.createQuery("select t from Tag t where t.id = :id and t.deleteDate is null");
q.setParameter("id", tag.getId());
Tag tagFromDb = (Tag) q.getSingleResult();
Tag tagDb = (Tag) q.getSingleResult();
// Update the tag
tagFromDb.setName(tag.getName());
tagFromDb.setColor(tag.getColor());
tagFromDb.setParentId(tag.getParentId());
tagDb.setName(tag.getName());
tagDb.setColor(tag.getColor());
tagDb.setParentId(tag.getParentId());
// Create audit log
AuditLogUtil.create(tagFromDb, AuditLogType.UPDATE, userId);
AuditLogUtil.create(tagDb, AuditLogType.UPDATE, userId);
return tagFromDb;
return tagDb;
}
/**
@@ -204,7 +197,7 @@ public class TagDao {
}
if (criteria.getNameLike() != null) {
criteriaList.add("t.TAG_NAME_C like :nameLike");
parameterMap.put("nameLike", "%" + criteria.getNameLike() + "%");
parameterMap.put("nameLike", criteria.getNameLike() + "%");
}
criteriaList.add("t.TAG_DELETEDATE_D is null");

View File

@@ -1,19 +1,5 @@
package com.sismics.docs.core.dao.jpa;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import org.mindrot.jbcrypt.BCrypt;
import com.google.common.base.Joiner;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.dao.jpa.criteria.UserCriteria;
@@ -24,6 +10,14 @@ import com.sismics.docs.core.util.jpa.QueryParam;
import com.sismics.docs.core.util.jpa.QueryUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import org.joda.time.DateTime;
import org.mindrot.jbcrypt.BCrypt;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import java.sql.Timestamp;
import java.util.*;
/**
* User DAO.
@@ -44,7 +38,7 @@ public class UserDao {
q.setParameter("username", username);
try {
User user = (User) q.getSingleResult();
if (!BCrypt.checkpw(password, user.getPassword())) {
if (!BCrypt.checkpw(password, user.getPassword()) || user.getDisableDate() != null) {
return null;
}
return user;
@@ -59,7 +53,7 @@ public class UserDao {
* @param user User to create
* @param userId User ID
* @return User ID
* @throws Exception
* @throws Exception e
*/
public String create(User user, String userId) throws Exception {
// Create the user UUID
@@ -98,16 +92,17 @@ public class UserDao {
// Get the user
Query q = em.createQuery("select u from User u where u.id = :id and u.deleteDate is null");
q.setParameter("id", user.getId());
User userFromDb = (User) q.getSingleResult();
User userDb = (User) q.getSingleResult();
// Update the user (except password)
userFromDb.setEmail(user.getEmail());
userFromDb.setStorageQuota(user.getStorageQuota());
userFromDb.setStorageCurrent(user.getStorageCurrent());
userFromDb.setTotpKey(user.getTotpKey());
userDb.setEmail(user.getEmail());
userDb.setStorageQuota(user.getStorageQuota());
userDb.setStorageCurrent(user.getStorageCurrent());
userDb.setTotpKey(user.getTotpKey());
userDb.setDisableDate(user.getDisableDate());
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId);
AuditLogUtil.create(userDb, AuditLogType.UPDATE, userId);
return user;
}
@@ -116,20 +111,17 @@ public class UserDao {
* Updates a user's quota.
*
* @param user User to update
* @return Updated user
*/
public User updateQuota(User user) {
public void updateQuota(User user) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the user
Query q = em.createQuery("select u from User u where u.id = :id and u.deleteDate is null");
q.setParameter("id", user.getId());
User userFromDb = (User) q.getSingleResult();
User userDb = (User) q.getSingleResult();
// Update the user
userFromDb.setStorageQuota(user.getStorageQuota());
return user;
userDb.setStorageCurrent(user.getStorageCurrent());
}
/**
@@ -145,13 +137,33 @@ public class UserDao {
// Get the user
Query q = em.createQuery("select u from User u where u.id = :id and u.deleteDate is null");
q.setParameter("id", user.getId());
User userFromDb = (User) q.getSingleResult();
User userDb = (User) q.getSingleResult();
// Update the user
userFromDb.setPassword(hashPassword(user.getPassword()));
userDb.setPassword(hashPassword(user.getPassword()));
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId);
AuditLogUtil.create(userDb, AuditLogType.UPDATE, userId);
return user;
}
/**
* Update the hashed password silently.
*
* @param user User to update
* @return Updated user
*/
public User updateHashedPassword(User user) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the user
Query q = em.createQuery("select u from User u where u.id = :id and u.deleteDate is null");
q.setParameter("id", user.getId());
User userDb = (User) q.getSingleResult();
// Update the user
userDb.setPassword(user.getPassword());
return user;
}
@@ -200,39 +212,39 @@ public class UserDao {
// Get the user
Query q = em.createQuery("select u from User u where u.username = :username and u.deleteDate is null");
q.setParameter("username", username);
User userFromDb = (User) q.getSingleResult();
User userDb = (User) q.getSingleResult();
// Delete the user
Date dateNow = new Date();
userFromDb.setDeleteDate(dateNow);
userDb.setDeleteDate(dateNow);
// Delete linked data
q = em.createQuery("delete from AuthenticationToken at where at.userId = :userId");
q.setParameter("userId", userFromDb.getId());
q.setParameter("userId", userDb.getId());
q.executeUpdate();
q = em.createQuery("update Document d set d.deleteDate = :dateNow where d.userId = :userId and d.deleteDate is null");
q.setParameter("userId", userFromDb.getId());
q.setParameter("userId", userDb.getId());
q.setParameter("dateNow", dateNow);
q.executeUpdate();
q = em.createQuery("update File f set f.deleteDate = :dateNow where f.userId = :userId and f.deleteDate is null");
q.setParameter("userId", userFromDb.getId());
q.setParameter("userId", userDb.getId());
q.setParameter("dateNow", dateNow);
q.executeUpdate();
q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.targetId = :userId and a.deleteDate is null");
q.setParameter("userId", userFromDb.getId());
q.setParameter("userId", userDb.getId());
q.setParameter("dateNow", dateNow);
q.executeUpdate();
q = em.createQuery("update Comment c set c.deleteDate = :dateNow where c.userId = :userId and c.deleteDate is null");
q.setParameter("userId", userFromDb.getId());
q.setParameter("userId", userDb.getId());
q.setParameter("dateNow", dateNow);
q.executeUpdate();
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.DELETE, userId);
AuditLogUtil.create(userDb, AuditLogType.DELETE, userId);
}
/**
@@ -256,7 +268,7 @@ public class UserDao {
Map<String, Object> parameterMap = new HashMap<>();
List<String> criteriaList = new ArrayList<>();
StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3, u.USE_STORAGECURRENT_N as c4, u.USE_STORAGEQUOTA_N as c5");
StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3, u.USE_STORAGECURRENT_N as c4, u.USE_STORAGEQUOTA_N as c5, u.USE_TOTPKEY_C as c6, u.USE_DISABLEDATE_D as c7");
sb.append(" from T_USER u ");
// Add search criterias
@@ -264,7 +276,14 @@ public class UserDao {
criteriaList.add("lower(u.USE_USERNAME_C) like lower(:search)");
parameterMap.put("search", "%" + criteria.getSearch() + "%");
}
if (criteria.getUserId() != null) {
criteriaList.add("u.USE_ID_C = :userId");
parameterMap.put("userId", criteria.getUserId());
}
if (criteria.getUserName() != null) {
criteriaList.add("u.USE_USERNAME_C = :userName");
parameterMap.put("userName", criteria.getUserName());
}
if (criteria.getGroupId() != null) {
sb.append(" join T_USER_GROUP ug on ug.UGP_IDUSER_C = u.USE_ID_C and ug.UGP_IDGROUP_C = :groupId and ug.UGP_DELETEDATE_D is null ");
parameterMap.put("groupId", criteria.getGroupId());
@@ -292,9 +311,39 @@ public class UserDao {
userDto.setEmail((String) o[i++]);
userDto.setCreateTimestamp(((Timestamp) o[i++]).getTime());
userDto.setStorageCurrent(((Number) o[i++]).longValue());
userDto.setStorageQuota(((Number) o[i]).longValue());
userDto.setStorageQuota(((Number) o[i++]).longValue());
userDto.setTotpKey((String) o[i++]);
if (o[i] != null) {
userDto.setDisableTimestamp(((Timestamp) o[i]).getTime());
}
userDtoList.add(userDto);
}
return userDtoList;
}
/**
* Returns the global storage used by all users.
*
* @return Current global storage
*/
public long getGlobalStorageCurrent() {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query query = em.createNativeQuery("select sum(u.USE_STORAGECURRENT_N) from T_USER u where u.USE_DELETEDATE_D is null");
return ((Number) query.getSingleResult()).longValue();
}
/**
* Returns the number of active users.
*
* @return Number of active users
*/
public long getActiveUserCount() {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query query = em.createNativeQuery("select count(u.USE_ID_C) from T_USER u where u.USE_DELETEDATE_D is null and (u.USE_DISABLEDATE_D is null or u.USE_DISABLEDATE_D >= :fromDate and u.USE_DISABLEDATE_D < :toDate)");
DateTime fromDate = DateTime.now().minusMonths(1).dayOfMonth().withMinimumValue().withTimeAtStartOfDay();
DateTime toDate = fromDate.plusMonths(1);
query.setParameter("fromDate", fromDate.toDate());
query.setParameter("toDate", toDate.toDate());
return ((Number) query.getSingleResult()).longValue();
}
}

View File

@@ -1,14 +1,13 @@
package com.sismics.docs.core.dao.jpa;
import java.util.List;
import java.util.UUID;
import com.sismics.docs.core.model.jpa.Vocabulary;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import com.sismics.docs.core.model.jpa.Vocabulary;
import com.sismics.util.context.ThreadLocalContext;
import java.util.List;
import java.util.UUID;
/**
* Vocabulary DAO.
@@ -76,14 +75,14 @@ public class VocabularyDao {
// Get the vocabulary entry
Query q = em.createQuery("select v from Vocabulary v where v.id = :id");
q.setParameter("id", vocabulary.getId());
Vocabulary vocabularyFromDb = (Vocabulary) q.getSingleResult();
Vocabulary vocabularyDb = (Vocabulary) q.getSingleResult();
// Update the vocabulary entry
vocabularyFromDb.setName(vocabulary.getName());
vocabularyFromDb.setValue(vocabulary.getValue());
vocabularyFromDb.setOrder(vocabulary.getOrder());
vocabularyDb.setName(vocabulary.getName());
vocabularyDb.setValue(vocabulary.getValue());
vocabularyDb.setOrder(vocabulary.getOrder());
return vocabularyFromDb;
return vocabularyDb;
}
/**

View File

@@ -1,7 +1,6 @@
package com.sismics.docs.core.dao.jpa.criteria;
/**
* Audit log criteria.
*

View File

@@ -0,0 +1,23 @@
package com.sismics.docs.core.dao.jpa.criteria;
/**
* Route criteria.
*
* @author bgamard
*/
public class RouteCriteria {
/**
* Document ID.
*/
private String documentId;
public String getDocumentId() {
return documentId;
}
public RouteCriteria setDocumentId(String documentId) {
this.documentId = documentId;
return this;
}
}

View File

@@ -0,0 +1,10 @@
package com.sismics.docs.core.dao.jpa.criteria;
/**
* Route model criteria.
*
* @author bgamard
*/
public class RouteModelCriteria {
}

View File

@@ -0,0 +1,51 @@
package com.sismics.docs.core.dao.jpa.criteria;
/**
* Route step criteria.
*
* @author bgamard
*/
public class RouteStepCriteria {
/**
* Document ID.
*/
private String documentId;
/**
* Route ID.
*/
private String routeId;
/**
* End date is null.
*/
private Boolean endDateIsNull;
public String getDocumentId() {
return documentId;
}
public RouteStepCriteria setDocumentId(String documentId) {
this.documentId = documentId;
return this;
}
public String getRouteId() {
return routeId;
}
public RouteStepCriteria setRouteId(String routeId) {
this.routeId = routeId;
return this;
}
public Boolean getEndDateIsNull() {
return endDateIsNull;
}
public RouteStepCriteria setEndDateIsNull(Boolean endDateIsNull) {
this.endDateIsNull = endDateIsNull;
return this;
}
}

View File

@@ -16,6 +16,16 @@ public class UserCriteria {
*/
private String groupId;
/**
* User ID.
*/
private String userId;
/**
* Username.
*/
private String userName;
public String getSearch() {
return search;
}
@@ -33,4 +43,22 @@ public class UserCriteria {
this.groupId = groupId;
return this;
}
public String getUserId() {
return userId;
}
public UserCriteria setUserId(String userId) {
this.userId = userId;
return this;
}
public String getUserName() {
return userName;
}
public UserCriteria setUserName(String userName) {
this.userName = userName;
return this;
}
}

View File

@@ -0,0 +1,50 @@
package com.sismics.docs.core.dao.jpa.dto;
/**
* Route DTO.
*
* @author bgamard
*/
public class RouteDto {
/**
* Route ID.
*/
private String id;
/**
* Name.
*/
private String name;
/**
* Creation date.
*/
private Long createTimestamp;
public String getId() {
return id;
}
public RouteDto setId(String id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public RouteDto setName(String name) {
this.name = name;
return this;
}
public Long getCreateTimestamp() {
return createTimestamp;
}
public RouteDto setCreateTimestamp(Long createTimestamp) {
this.createTimestamp = createTimestamp;
return this;
}
}

View File

@@ -0,0 +1,50 @@
package com.sismics.docs.core.dao.jpa.dto;
/**
* Route model DTO.
*
* @author bgamard
*/
public class RouteModelDto {
/**
* Route model ID.
*/
private String id;
/**
* Name.
*/
private String name;
/**
* Creation date.
*/
private Long createTimestamp;
public String getId() {
return id;
}
public RouteModelDto setId(String id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public RouteModelDto setName(String name) {
this.name = name;
return this;
}
public Long getCreateTimestamp() {
return createTimestamp;
}
public RouteModelDto setCreateTimestamp(Long createTimestamp) {
this.createTimestamp = createTimestamp;
return this;
}
}

View File

@@ -0,0 +1,187 @@
package com.sismics.docs.core.dao.jpa.dto;
import com.sismics.docs.core.constant.RouteStepType;
import com.sismics.util.JsonUtil;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
/**
* Route step DTO.
*
* @author bgamard
*/
public class RouteStepDto {
/**
* Route step ID.
*/
private String id;
/**
* Name.
*/
private String name;
/**
* Type.
*/
private RouteStepType type;
/**
* Transition.
*/
private String transition;
/**
* Comment.
*/
private String comment;
/**
* Target ID (user or group).
*/
private String targetId;
/**
* Target name.
*/
private String targetName;
/**
* Target type.
*/
private String targetType;
/**
* End date.
*/
private Long endDateTimestamp;
/**
* Validator's username.
*/
private String validatorUserName;
/**
* Route ID.
*/
private String routeId;
public String getId() {
return id;
}
public RouteStepDto setId(String id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public RouteStepDto setName(String name) {
this.name = name;
return this;
}
public RouteStepType getType() {
return type;
}
public RouteStepDto setType(RouteStepType type) {
this.type = type;
return this;
}
public String getTransition() {
return transition;
}
public RouteStepDto setTransition(String transition) {
this.transition = transition;
return this;
}
public String getComment() {
return comment;
}
public RouteStepDto setComment(String comment) {
this.comment = comment;
return this;
}
public String getTargetId() {
return targetId;
}
public RouteStepDto setTargetId(String targetId) {
this.targetId = targetId;
return this;
}
public String getTargetName() {
return targetName;
}
public RouteStepDto setTargetName(String targetName) {
this.targetName = targetName;
return this;
}
public String getTargetType() {
return targetType;
}
public RouteStepDto setTargetType(String targetType) {
this.targetType = targetType;
return this;
}
public Long getEndDateTimestamp() {
return endDateTimestamp;
}
public RouteStepDto setEndDateTimestamp(Long endDateTimestamp) {
this.endDateTimestamp = endDateTimestamp;
return this;
}
public String getValidatorUserName() {
return validatorUserName;
}
public RouteStepDto setValidatorUserName(String validatorUserName) {
this.validatorUserName = validatorUserName;
return this;
}
public String getRouteId() {
return routeId;
}
public RouteStepDto setRouteId(String routeId) {
this.routeId = routeId;
return this;
}
/**
* Transform in JSON.
*
* @return JSON object builder
*/
public JsonObjectBuilder toJson() {
return Json.createObjectBuilder()
.add("name", getName())
.add("type", getType().name())
.add("comment", JsonUtil.nullable(getComment()))
.add("end_date", JsonUtil.nullable(getEndDateTimestamp()))
.add("validator_username", JsonUtil.nullable(getValidatorUserName()))
.add("target", Json.createObjectBuilder()
.add("id", getTargetId())
.add("name", JsonUtil.nullable(getTargetName()))
.add("type", getTargetType()))
.add("transition", JsonUtil.nullable(getTransition()));
}
}

View File

@@ -1,5 +1,7 @@
package com.sismics.docs.core.dao.jpa.dto;
import com.google.common.base.MoreObjects;
/**
* User DTO.
*
@@ -26,6 +28,11 @@ public class UserDto {
*/
private Long createTimestamp;
/**
* Disable date of this user.
*/
private Long disableTimestamp;
/**
* Storage quota.
*/
@@ -36,6 +43,11 @@ public class UserDto {
*/
private Long storageCurrent;
/**
* TOTP key.
*/
private String totpKey;
public String getId() {
return id;
}
@@ -68,6 +80,15 @@ public class UserDto {
this.createTimestamp = createTimestamp;
}
public Long getDisableTimestamp() {
return disableTimestamp;
}
public UserDto setDisableTimestamp(Long disableTimestamp) {
this.disableTimestamp = disableTimestamp;
return this;
}
public Long getStorageQuota() {
return storageQuota;
}
@@ -83,4 +104,21 @@ public class UserDto {
public void setStorageCurrent(Long storageCurrent) {
this.storageCurrent = storageCurrent;
}
public String getTotpKey() {
return totpKey;
}
public void setTotpKey(String totpKey) {
this.totpKey = totpKey;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("username", username)
.add("email", email)
.toString();
}
}

View File

@@ -34,7 +34,7 @@ public class LuceneDao {
/**
* Destroy and rebuild index.
*
* @param fileList
* @param fileList List of files
*/
public void rebuildIndex(final List<Document> documentList, final List<File> fileList) {
LuceneUtil.handle(new LuceneRunnable() {
@@ -103,21 +103,6 @@ public class LuceneDao {
});
}
/**
* Update file index.
*
* @param file Updated file
*/
public void updateFile(final File file) {
LuceneUtil.handle(new LuceneRunnable() {
@Override
public void run(IndexWriter indexWriter) throws Exception {
org.apache.lucene.document.Document luceneDocument = getDocumentFromFile(file);
indexWriter.updateDocument(new Term("id", file.getId()), luceneDocument);
}
});
}
/**
* Delete document from the index.
*
@@ -166,7 +151,7 @@ public class LuceneDao {
// Search
DirectoryReader directoryReader = AppContext.getInstance().getIndexingService().getDirectoryReader();
Set<String> documentIdList = new HashSet<String>();
Set<String> documentIdList = new HashSet<>();
if (directoryReader == null) {
// The directory reader is not yet initialized (probably because there is nothing indexed)
return documentIdList;
@@ -176,8 +161,8 @@ public class LuceneDao {
ScoreDoc[] docs = topDocs.scoreDocs;
// Extract document IDs
for (int i = 0; i < docs.length; i++) {
org.apache.lucene.document.Document document = searcher.doc(docs[i].doc);
for (ScoreDoc doc : docs) {
org.apache.lucene.document.Document document = searcher.doc(doc.doc);
String type = document.get("doctype");
String documentId = null;
if (type.equals("document")) {
@@ -194,7 +179,7 @@ public class LuceneDao {
/**
* Build Lucene document from database document.
*
* @param documentDto Document
* @param document Document
* @return Document
*/
private org.apache.lucene.document.Document getDocumentFromDocument(Document document) {

View File

@@ -1,6 +1,7 @@
package com.sismics.docs.core.event;
import java.io.InputStream;
import java.nio.file.Path;
import com.google.common.base.MoreObjects;
import com.sismics.docs.core.model.jpa.File;
@@ -22,16 +23,16 @@ public class FileCreatedAsyncEvent extends UserEvent {
private String language;
/**
* Unencrypted input stream containing the file.
* Unencrypted original file.
*/
private InputStream inputStream;
private Path unencryptedFile;
/**
* Unencrypted input stream containing a PDF representation
* of the file. May be null if the PDF conversion is not
* Unencrypted file containing PDF representation
* of the original file. May be null if the PDF conversion is not
* necessary or not possible.
*/
private InputStream pdfInputStream;
private Path unencryptedPdfFile;
public File getFile() {
return file;
@@ -49,20 +50,22 @@ public class FileCreatedAsyncEvent extends UserEvent {
this.language = language;
}
public InputStream getInputStream() {
return inputStream;
public Path getUnencryptedFile() {
return unencryptedFile;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
public FileCreatedAsyncEvent setUnencryptedFile(Path unencryptedFile) {
this.unencryptedFile = unencryptedFile;
return this;
}
public InputStream getPdfInputStream() {
return pdfInputStream;
public Path getUnencryptedPdfFile() {
return unencryptedPdfFile;
}
public void setPdfInputStream(InputStream pdfInputStream) {
this.pdfInputStream = pdfInputStream;
public FileCreatedAsyncEvent setUnencryptedPdfFile(Path unencryptedPdfFile) {
this.unencryptedPdfFile = unencryptedPdfFile;
return this;
}
@Override

View File

@@ -0,0 +1,46 @@
package com.sismics.docs.core.event;
import com.google.common.base.MoreObjects;
import com.sismics.docs.core.dao.jpa.dto.UserDto;
import com.sismics.docs.core.model.jpa.PasswordRecovery;
/**
* Event fired on user's password lost event.
*
* @author jtremeaux
*/
public class PasswordLostEvent {
/**
* User.
*/
private UserDto user;
/**
* Password recovery request.
*/
private PasswordRecovery passwordRecovery;
public UserDto getUser() {
return user;
}
public void setUser(UserDto user) {
this.user = user;
}
public PasswordRecovery getPasswordRecovery() {
return passwordRecovery;
}
public void setPasswordRecovery(PasswordRecovery passwordRecovery) {
this.passwordRecovery = passwordRecovery;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("user", user)
.add("passwordRecovery", "**hidden**")
.toString();
}
}

View File

@@ -0,0 +1,47 @@
package com.sismics.docs.core.event;
import com.google.common.base.MoreObjects;
import com.sismics.docs.core.dao.jpa.dto.UserDto;
import com.sismics.docs.core.model.jpa.Document;
/**
* Event fired on route step validation event.
*
* @author bgamard
*/
public class RouteStepValidateEvent {
/**
* User.
*/
private UserDto user;
/**
* Document linked to the route.
*/
private Document document;
public UserDto getUser() {
return user;
}
public void setUser(UserDto user) {
this.user = user;
}
public Document getDocument() {
return document;
}
public RouteStepValidateEvent setDocument(Document document) {
this.document = document;
return this;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("user", user)
.add("document", document)
.toString();
}
}

View File

@@ -0,0 +1,35 @@
package com.sismics.docs.core.event;
import com.google.common.base.MoreObjects;
import com.sismics.docs.core.model.jpa.File;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;
/**
* Cleanup temporary files event.
*
* @author bgamard
*/
public class TemporaryFileCleanupAsyncEvent {
/**
* Temporary files.
*/
private List<Path> fileList;
public TemporaryFileCleanupAsyncEvent(List<Path> fileList) {
this.fileList = fileList;
}
public List<Path> getFileList() {
return fileList;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("files", fileList)
.toString();
}
}

View File

@@ -1,14 +1,13 @@
package com.sismics.docs.core.listener.async;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.sismics.docs.core.dao.jpa.ContributorDao;
import com.sismics.docs.core.dao.lucene.LuceneDao;
import com.sismics.docs.core.event.DocumentCreatedAsyncEvent;
import com.sismics.docs.core.model.jpa.Contributor;
import com.sismics.docs.core.util.TransactionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Listener on document created.
@@ -25,10 +24,9 @@ public class DocumentCreatedAsyncListener {
* Document created.
*
* @param event Document created event
* @throws Exception
*/
@Subscribe
public void on(final DocumentCreatedAsyncEvent event) throws Exception {
public void on(final DocumentCreatedAsyncEvent event) {
if (log.isInfoEnabled()) {
log.info("Document created event: " + event.toString());
}

View File

@@ -1,10 +1,5 @@
package com.sismics.docs.core.listener.async;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.sismics.docs.core.dao.jpa.ContributorDao;
import com.sismics.docs.core.dao.jpa.DocumentDao;
@@ -12,6 +7,10 @@ import com.sismics.docs.core.dao.lucene.LuceneDao;
import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent;
import com.sismics.docs.core.model.jpa.Contributor;
import com.sismics.docs.core.util.TransactionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* Listener on document updated.
@@ -31,7 +30,7 @@ public class DocumentUpdatedAsyncListener {
* @throws Exception
*/
@Subscribe
public void on(final DocumentUpdatedAsyncEvent event) throws Exception {
public void on(final DocumentUpdatedAsyncEvent event) {
if (log.isInfoEnabled()) {
log.info("Document updated event: " + event.toString());
}

View File

@@ -1,10 +1,5 @@
package com.sismics.docs.core.listener.async;
import java.text.MessageFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.sismics.docs.core.dao.jpa.FileDao;
import com.sismics.docs.core.dao.lucene.LuceneDao;
@@ -12,6 +7,10 @@ import com.sismics.docs.core.event.FileCreatedAsyncEvent;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.FileUtil;
import com.sismics.docs.core.util.TransactionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.MessageFormat;
/**
* Listener on file created.
@@ -28,7 +27,7 @@ public class FileCreatedAsyncListener {
* File created.
*
* @param fileCreatedAsyncEvent File created event
* @throws Exception
* @throws Exception e
*/
@Subscribe
public void on(final FileCreatedAsyncEvent fileCreatedAsyncEvent) throws Exception {
@@ -42,11 +41,7 @@ public class FileCreatedAsyncListener {
// Extract text content from the file
long startTime = System.currentTimeMillis();
final String content = FileUtil.extractContent(fileCreatedAsyncEvent.getLanguage(), file,
fileCreatedAsyncEvent.getInputStream(), fileCreatedAsyncEvent.getPdfInputStream());
fileCreatedAsyncEvent.getInputStream().close();
if (fileCreatedAsyncEvent.getPdfInputStream() != null) {
fileCreatedAsyncEvent.getPdfInputStream().close();
}
fileCreatedAsyncEvent.getUnencryptedFile(), fileCreatedAsyncEvent.getUnencryptedPdfFile());
log.info(MessageFormat.format("File content extracted in {0}ms", System.currentTimeMillis() - startTime));
// Store the text content in the database

View File

@@ -0,0 +1,53 @@
package com.sismics.docs.core.listener.async;
import com.google.common.eventbus.Subscribe;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.dto.UserDto;
import com.sismics.docs.core.event.PasswordLostEvent;
import com.sismics.docs.core.model.jpa.PasswordRecovery;
import com.sismics.docs.core.util.TransactionUtil;
import com.sismics.util.EmailUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* Listener for password recovery requests.
*
* @author jtremeaux
*/
public class PasswordLostAsyncListener {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(PasswordLostAsyncListener.class);
/**
* Handle events.
*
* @param passwordLostEvent Event
*/
@Subscribe
public void onPasswordLost(final PasswordLostEvent passwordLostEvent) {
if (log.isInfoEnabled()) {
log.info("Password lost event: " + passwordLostEvent.toString());
}
TransactionUtil.handle(new Runnable() {
@Override
public void run() {
final UserDto user = passwordLostEvent.getUser();
final PasswordRecovery passwordRecovery = passwordLostEvent.getPasswordRecovery();
// Send the password recovery email
Map<String, Object> paramRootMap = new HashMap<>();
paramRootMap.put("user_name", user.getUsername());
paramRootMap.put("password_recovery_key", passwordRecovery.getId());
EmailUtil.sendEmail(Constants.EMAIL_TEMPLATE_PASSWORD_RECOVERY, user, paramRootMap);
}
});
}
}

View File

@@ -0,0 +1,52 @@
package com.sismics.docs.core.listener.async;
import com.google.common.eventbus.Subscribe;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.dto.UserDto;
import com.sismics.docs.core.event.RouteStepValidateEvent;
import com.sismics.docs.core.util.TransactionUtil;
import com.sismics.util.EmailUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* Listener for route step validate.
*
* @author bgamard
*/
public class RouteStepValidateAsyncListener {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(RouteStepValidateAsyncListener.class);
/**
* Handle events.
*
* @param routeStepValidateEvent Event
*/
@Subscribe
public void onRouteStepValidate(final RouteStepValidateEvent routeStepValidateEvent) {
if (log.isInfoEnabled()) {
log.info("Route step validate event: " + routeStepValidateEvent.toString());
}
TransactionUtil.handle(new Runnable() {
@Override
public void run() {
final UserDto user = routeStepValidateEvent.getUser();
// Send the password recovery email
Map<String, Object> paramRootMap = new HashMap<>();
paramRootMap.put("user_name", user.getUsername());
paramRootMap.put("document_id", routeStepValidateEvent.getDocument().getId());
paramRootMap.put("document_title", routeStepValidateEvent.getDocument().getTitle());
EmailUtil.sendEmail(Constants.EMAIL_TEMPLATE_ROUTE_STEP_VALIDATE, user, paramRootMap);
}
});
}
}

View File

@@ -0,0 +1,38 @@
package com.sismics.docs.core.listener.async;
import com.google.common.eventbus.Subscribe;
import com.sismics.docs.core.event.TemporaryFileCleanupAsyncEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Listener to cleanup temporary files created during a request.
*
* @author bgamard
*/
public class TemporaryFileCleanupAsyncListener {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(FileCreatedAsyncListener.class);
/**
* Cleanup temporary files.
*
* @param event Temporary file cleanup event
* @throws Exception
*/
@Subscribe
public void on(final TemporaryFileCleanupAsyncEvent event) throws Exception {
if (log.isInfoEnabled()) {
log.info("Cleanup temporary files event: " + event.toString());
}
for (Path file : event.getFileList()) {
Files.delete(file);
}
}
}

View File

@@ -1,5 +1,20 @@
package com.sismics.docs.core.model.context;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.ConfigDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.listener.async.*;
import com.sismics.docs.core.listener.sync.DeadEventListener;
import com.sismics.docs.core.model.jpa.Config;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.service.InboxService;
import com.sismics.docs.core.service.IndexingService;
import com.sismics.docs.core.util.PdfUtil;
import com.sismics.util.EnvironmentUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
@@ -7,21 +22,6 @@ import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.dao.jpa.ConfigDao;
import com.sismics.docs.core.listener.async.DocumentCreatedAsyncListener;
import com.sismics.docs.core.listener.async.DocumentDeletedAsyncListener;
import com.sismics.docs.core.listener.async.DocumentUpdatedAsyncListener;
import com.sismics.docs.core.listener.async.FileCreatedAsyncListener;
import com.sismics.docs.core.listener.async.FileDeletedAsyncListener;
import com.sismics.docs.core.listener.async.RebuildIndexAsyncListener;
import com.sismics.docs.core.listener.sync.DeadEventListener;
import com.sismics.docs.core.model.jpa.Config;
import com.sismics.docs.core.service.IndexingService;
import com.sismics.util.EnvironmentUtil;
/**
* Global application context.
*
@@ -43,11 +43,21 @@ public class AppContext {
*/
private EventBus asyncEventBus;
/**
* Asynchronous bus for email sending.
*/
private EventBus mailEventBus;
/**
* Indexing service.
*/
private IndexingService indexingService;
/**
* Inbox scanning service.
*/
private InboxService inboxService;
/**
* Asynchronous executors.
*/
@@ -59,10 +69,29 @@ public class AppContext {
private AppContext() {
resetEventBus();
// Start indexing service
ConfigDao configDao = new ConfigDao();
Config luceneStorageConfig = configDao.getById(ConfigType.LUCENE_DIRECTORY_STORAGE);
indexingService = new IndexingService(luceneStorageConfig != null ? luceneStorageConfig.getValue() : null);
indexingService.startAsync();
// Start inbox service
inboxService = new InboxService();
inboxService.startAsync();
// Register fonts
PdfUtil.registerFonts();
// Change the admin password if needed
String envAdminPassword = System.getenv(Constants.ADMIN_PASSWORD_INIT_ENV);
if (envAdminPassword != null) {
UserDao userDao = new UserDao();
User adminUser = userDao.getById("admin");
if (Constants.DEFAULT_ADMIN_PASSWORD.equals(adminUser.getPassword())) {
adminUser.setPassword(envAdminPassword);
userDao.updateHashedPassword(adminUser);
}
}
}
/**
@@ -81,6 +110,11 @@ public class AppContext {
asyncEventBus.register(new DocumentUpdatedAsyncListener());
asyncEventBus.register(new DocumentDeletedAsyncListener());
asyncEventBus.register(new RebuildIndexAsyncListener());
asyncEventBus.register(new TemporaryFileCleanupAsyncListener());
mailEventBus = newAsyncEventBus();
mailEventBus.register(new PasswordLostAsyncListener());
mailEventBus.register(new RouteStepValidateAsyncListener());
}
/**
@@ -127,6 +161,7 @@ public class AppContext {
if (EnvironmentUtil.isUnitTest()) {
return new EventBus();
} else {
// /!\ Don't add more threads because a cleanup event is fired at the end of each request
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
@@ -135,30 +170,23 @@ public class AppContext {
}
}
/**
* Getter of eventBus.
*
* @return eventBus
*/
public EventBus getEventBus() {
return eventBus;
}
/**
* Getter of asyncEventBus.
*
* @return asyncEventBus
*/
public EventBus getAsyncEventBus() {
return asyncEventBus;
}
/**
* Getter of indexingService.
*
* @return indexingService
*/
public EventBus getMailEventBus() {
return mailEventBus;
}
public IndexingService getIndexingService() {
return indexingService;
}
public InboxService getInboxService() {
return inboxService;
}
}

View File

@@ -1,17 +1,12 @@
package com.sismics.docs.core.model.jpa;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Table;
import com.google.common.base.MoreObjects;
import com.sismics.docs.core.constant.AclType;
import com.sismics.docs.core.constant.PermType;
import javax.persistence.*;
import java.util.Date;
/**
* ACL entity.
*
@@ -34,6 +29,13 @@ public class Acl implements Loggable {
@Enumerated(EnumType.STRING)
private PermType perm;
/**
* ACL type.
*/
@Column(name = "ACL_TYPE_C", length = 30, nullable = false)
@Enumerated(EnumType.STRING)
private AclType type;
/**
* ACL source ID.
*/
@@ -84,6 +86,15 @@ public class Acl implements Loggable {
this.targetId = targetId;
}
public AclType getType() {
return type;
}
public Acl setType(AclType type) {
this.type = type;
return this;
}
@Override
public Date getDeleteDate() {
return deleteDate;
@@ -100,6 +111,7 @@ public class Acl implements Loggable {
.add("perm", perm)
.add("sourceId", sourceId)
.add("targetId", targetId)
.add("type", type)
.toString();
}

View File

@@ -1,15 +1,11 @@
package com.sismics.docs.core.model.jpa;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Transient;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.sismics.util.mime.MimeTypeUtil;
import javax.persistence.*;
import java.util.Date;
/**
* File entity.
@@ -38,6 +34,12 @@ public class File implements Loggable {
@Column(name = "FIL_IDUSER_C", length = 36, nullable = false)
private String userId;
/**
* Name.
*/
@Column(name = "FIL_NAME_C", length = 200)
private String name;
/**
* MIME type.
*/
@@ -92,6 +94,15 @@ public class File implements Loggable {
this.documentId = documentId;
}
public String getName() {
return name;
}
public File setName(String name) {
this.name = name;
return this;
}
public String getMimeType() {
return mimeType;
}
@@ -153,11 +164,26 @@ public class File implements Loggable {
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("name", name)
.toString();
}
@Override
public String toMessage() {
return documentId;
// Attached document ID and name concatenated
return (documentId == null ? Strings.repeat(" ", 36) : documentId) + name;
}
/**
* Build the full file name.
*
* @param def Default name if the file doesn't have one.
* @return File name
*/
public String getFullName(String def) {
if (Strings.isNullOrEmpty(name)) {
return def + "." + MimeTypeUtil.getFileExtension(mimeType);
}
return name;
}
}

View File

@@ -14,12 +14,12 @@ public interface Loggable {
*
* @return Entity message
*/
public String toMessage();
String toMessage();
/**
* Loggable are soft deletable.
*
* @return deleteDate
*/
public Date getDeleteDate();
Date getDeleteDate();
}

View File

@@ -0,0 +1,82 @@
package com.sismics.docs.core.model.jpa;
import com.google.common.base.MoreObjects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* Password recovery entity.
*
* @author jtremeaux
*/
@Entity
@Table(name = "T_PASSWORD_RECOVERY")
public class PasswordRecovery {
/**
* Identifier.
*/
@Id
@Column(name = "PWR_ID_C", length = 36)
private String id;
/**
* Username.
*/
@Column(name = "PWR_USERNAME_C", nullable = false, length = 50)
private String username;
/**
* Creation date.
*/
@Column(name = "PWR_CREATEDATE_D", nullable = false)
private Date createDate;
/**
* Delete date.
*/
@Column(name = "PWR_DELETEDATE_D")
private Date deleteDate;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public Date getDeleteDate() {
return deleteDate;
}
public void setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.toString();
}
}

View File

@@ -0,0 +1,109 @@
package com.sismics.docs.core.model.jpa;
import com.google.common.base.MoreObjects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* Route.
*
* @author bgamard
*/
@Entity
@Table(name = "T_ROUTE")
public class Route implements Loggable {
/**
* Route ID.
*/
@Id
@Column(name = "RTE_ID_C", length = 36)
private String id;
/**
* Document ID.
*/
@Column(name = "RTE_IDDOCUMENT_C", nullable = false, length = 36)
private String documentId;
/**
* Name.
*/
@Column(name = "RTE_NAME_C", nullable = false, length = 50)
private String name;
/**
* Creation date.
*/
@Column(name = "RTE_CREATEDATE_D", nullable = false)
private Date createDate;
/**
* Deletion date.
*/
@Column(name = "RTE_DELETEDATE_D")
private Date deleteDate;
public String getId() {
return id;
}
public Route setId(String id) {
this.id = id;
return this;
}
public String getDocumentId() {
return documentId;
}
public Route setDocumentId(String documentId) {
this.documentId = documentId;
return this;
}
public String getName() {
return name;
}
public Route setName(String name) {
this.name = name;
return this;
}
public Date getCreateDate() {
return createDate;
}
public Route setCreateDate(Date createDate) {
this.createDate = createDate;
return this;
}
public Date getDeleteDate() {
return deleteDate;
}
public Route setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate;
return this;
}
@Override
public String toMessage() {
return documentId;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("name", name)
.add("documentId", documentId)
.add("createDate", createDate)
.toString();
}
}

View File

@@ -0,0 +1,107 @@
package com.sismics.docs.core.model.jpa;
import com.google.common.base.MoreObjects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* Route model.
*
* @author bgamard
*/
@Entity
@Table(name = "T_ROUTE_MODEL")
public class RouteModel implements Loggable {
/**
* Route model ID.
*/
@Id
@Column(name = "RTM_ID_C", length = 36)
private String id;
/**
* Name.
*/
@Column(name = "RTM_NAME_C", nullable = false, length = 50)
private String name;
/**
* Data.
*/
@Column(name = "RTM_STEPS_C", nullable = false, length = 5000)
private String steps;
/**
* Creation date.
*/
@Column(name = "RTM_CREATEDATE_D", nullable = false)
private Date createDate;
/**
* Deletion date.
*/
@Column(name = "RTM_DELETEDATE_D")
private Date deleteDate;
public String getId() {
return id;
}
public RouteModel setId(String id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public RouteModel setName(String name) {
this.name = name;
return this;
}
public String getSteps() {
return steps;
}
public RouteModel setSteps(String steps) {
this.steps = steps;
return this;
}
public Date getCreateDate() {
return createDate;
}
public RouteModel setCreateDate(Date createDate) {
this.createDate = createDate;
return this;
}
public Date getDeleteDate() {
return deleteDate;
}
public RouteModel setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate;
return this;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("name", name)
.toString();
}
@Override
public String toMessage() {
return name;
}
}

View File

@@ -0,0 +1,216 @@
package com.sismics.docs.core.model.jpa;
import com.google.common.base.MoreObjects;
import com.sismics.docs.core.constant.RouteStepTransition;
import com.sismics.docs.core.constant.RouteStepType;
import javax.persistence.*;
import java.util.Date;
/**
* Route step.
*
* @author bgamard
*/
@Entity
@Table(name = "T_ROUTE_STEP")
public class RouteStep {
/**
* Route step ID.
*/
@Id
@Column(name = "RTP_ID_C", length = 36)
private String id;
/**
* Route ID.
*/
@Column(name = "RTP_IDROUTE_C", nullable = false, length = 36)
private String routeId;
/**
* Name.
*/
@Column(name = "RTP_NAME_C", nullable = false, length = 200)
private String name;
/**
* Type.
*/
@Column(name = "RTP_TYPE_C", nullable = false, length = 50)
@Enumerated(EnumType.STRING)
private RouteStepType type;
/**
* Transition.
*/
@Column(name = "RTP_TRANSITION_C", length = 50)
@Enumerated(EnumType.STRING)
private RouteStepTransition transition;
/**
* Comment.
*/
@Column(name = "RTP_COMMENT_C", length = 500)
private String comment;
/**
* Target ID (user or group).
*/
@Column(name = "RTP_IDTARGET_C", nullable = false, length = 36)
private String targetId;
/**
* Validator user ID.
*/
@Column(name = "RTP_IDVALIDATORUSER_C", length = 36)
private String validatorUserId;
/**
* Order.
*/
@Column(name = "RTP_ORDER_N", nullable = false)
private Integer order;
/**
* Creation date.
*/
@Column(name = "RTP_CREATEDATE_D", nullable = false)
private Date createDate;
/**
* End date.
*/
@Column(name = "RTP_ENDDATE_D")
private Date endDate;
/**
* Deletion date.
*/
@Column(name = "RTP_DELETEDATE_D")
private Date deleteDate;
public String getId() {
return id;
}
public RouteStep setId(String id) {
this.id = id;
return this;
}
public String getRouteId() {
return routeId;
}
public RouteStep setRouteId(String routeId) {
this.routeId = routeId;
return this;
}
public String getName() {
return name;
}
public RouteStep setName(String name) {
this.name = name;
return this;
}
public RouteStepType getType() {
return type;
}
public RouteStep setType(RouteStepType type) {
this.type = type;
return this;
}
public RouteStepTransition getTransition() {
return transition;
}
public RouteStep setTransition(RouteStepTransition transition) {
this.transition = transition;
return this;
}
public String getComment() {
return comment;
}
public RouteStep setComment(String comment) {
this.comment = comment;
return this;
}
public String getTargetId() {
return targetId;
}
public RouteStep setTargetId(String targetId) {
this.targetId = targetId;
return this;
}
public Integer getOrder() {
return order;
}
public RouteStep setOrder(Integer order) {
this.order = order;
return this;
}
public String getValidatorUserId() {
return validatorUserId;
}
public RouteStep setValidatorUserId(String validatorUserId) {
this.validatorUserId = validatorUserId;
return this;
}
public Date getCreateDate() {
return createDate;
}
public RouteStep setCreateDate(Date createDate) {
this.createDate = createDate;
return this;
}
public Date getEndDate() {
return endDate;
}
public RouteStep setEndDate(Date endDate) {
this.endDate = endDate;
return this;
}
public Date getDeleteDate() {
return deleteDate;
}
public RouteStep setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate;
return this;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("routeId", routeId)
.add("name", name)
.add("type", type)
.add("transition", transition)
.add("comment", comment)
.add("targetId", targetId)
.add("order", order)
.add("createDate", createDate)
.add("endDate", endDate)
.toString();
}
}

View File

@@ -1,13 +1,12 @@
package com.sismics.docs.core.model.jpa;
import java.util.Date;
import com.google.common.base.MoreObjects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import com.google.common.base.MoreObjects;
import java.util.Date;
/**
* User entity.
@@ -84,6 +83,12 @@ public class User implements Loggable {
@Column(name = "USE_DELETEDATE_D")
private Date deleteDate;
/**
* Disable date.
*/
@Column(name = "USE_DISABLEDATE_D")
private Date disableDate;
public String getId() {
return id;
}
@@ -148,6 +153,15 @@ public class User implements Loggable {
return this;
}
public Date getDisableDate() {
return disableDate;
}
public User setDisableDate(Date disableDate) {
this.disableDate = disableDate;
return this;
}
public String getPrivateKey() {
return privateKey;
}
@@ -189,6 +203,7 @@ public class User implements Loggable {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("username", username)
.add("email", email)
.toString();
}

View File

@@ -0,0 +1,236 @@
package com.sismics.docs.core.service;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.AbstractScheduledService;
import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.dao.jpa.TagDao;
import com.sismics.docs.core.event.DocumentCreatedAsyncEvent;
import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.Tag;
import com.sismics.docs.core.util.ConfigUtil;
import com.sismics.docs.core.util.DocumentUtil;
import com.sismics.docs.core.util.FileUtil;
import com.sismics.docs.core.util.TransactionUtil;
import com.sismics.util.EmailUtil;
import com.sismics.util.context.ThreadLocalContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.mail.*;
import javax.mail.search.FlagTerm;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
/**
* Inbox scanning service.
*
* @author bgamard
*/
public class InboxService extends AbstractScheduledService {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(InboxService.class);
/**
* Last synchronization data.
*/
private Date lastSyncDate;
private int lastSyncMessageCount = 0;
private String lastSyncError;
public InboxService() {
}
@Override
protected void startUp() {
}
@Override
protected void shutDown() {
}
@Override
protected void runOneIteration() {
syncInbox();
}
/**
* Synchronize the inbox.
*/
public void syncInbox() {
TransactionUtil.handle(new Runnable() {
@Override
public void run() {
Boolean enabled = ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_ENABLED);
if (!enabled) {
return;
}
Folder inbox = null;
lastSyncError = null;
lastSyncDate = new Date();
lastSyncMessageCount = 0;
try {
inbox = openInbox();
int count = inbox.getMessageCount();
Message[] messages = inbox.getMessages(1, count);
for (Message message : messages) {
if (!message.getFlags().contains(Flags.Flag.SEEN)) {
importMessage(message);
lastSyncMessageCount++;
}
}
} catch (Exception e) {
log.error("Error synching the inbox", e);
lastSyncError = e.getMessage();
} finally {
try {
if (inbox != null) {
inbox.close(false);
inbox.getStore().close();
}
} catch (Exception e) {
// NOP
}
}
}
});
}
/**
* Test the inbox configuration.
*
* @return Number of messages currently in the remote inbox
*/
public int testInbox() {
Boolean enabled = ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_ENABLED);
if (!enabled) {
return -1;
}
Folder inbox = null;
try {
inbox = openInbox();
return inbox.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false)).length;
} catch (Exception e) {
log.error("Error testing inbox", e);
return -1;
} finally {
try {
if (inbox != null) {
inbox.close(false);
inbox.getStore().close();
}
} catch (Exception e) {
// NOP
}
}
}
@Override
protected Scheduler scheduler() {
return Scheduler.newFixedDelaySchedule(0, 15, TimeUnit.MINUTES);
}
/**
* Open the remote inbox.
*
* @return Opened inbox folder
* @throws Exception e
*/
private Folder openInbox() throws Exception {
Properties properties = new Properties();
String port = ConfigUtil.getConfigStringValue(ConfigType.INBOX_PORT);
properties.put("mail.imap.host", ConfigUtil.getConfigStringValue(ConfigType.INBOX_HOSTNAME));
properties.put("mail.imap.port", port);
boolean isSsl = "993".equals(port);
properties.put("mail.imap.ssl.enable", String.valueOf(isSsl));
properties.setProperty("mail.imap.socketFactory.class",
isSsl ? "javax.net.ssl.SSLSocketFactory" : "javax.net.DefaultSocketFactory");
properties.setProperty("mail.imap.socketFactory.fallback", "true");
properties.setProperty("mail.imap.socketFactory.port", port);
Session session = Session.getDefaultInstance(properties);
Store store = session.getStore("imap");
store.connect(ConfigUtil.getConfigStringValue(ConfigType.INBOX_USERNAME),
ConfigUtil.getConfigStringValue(ConfigType.INBOX_PASSWORD));
Folder inbox = store.getFolder("INBOX");
inbox.open(Folder.READ_WRITE);
return inbox;
}
/**
* Import an email.
*
* @param message Message
* @throws Exception e
*/
private void importMessage(Message message) throws Exception {
// Parse the mail
EmailUtil.MailContent mailContent = new EmailUtil.MailContent();
mailContent.setSubject(message.getSubject());
mailContent.setDate(message.getSentDate());
EmailUtil.parseMailContent(message, mailContent);
// Create the document
Document document = new Document();
document.setUserId("admin");
if (mailContent.getSubject() == null) {
document.setTitle("Imported email from EML file");
} else {
document.setTitle(StringUtils.abbreviate(mailContent.getSubject(), 100));
}
document.setDescription(StringUtils.abbreviate(mailContent.getMessage(), 4000));
document.setSubject(StringUtils.abbreviate(mailContent.getSubject(), 500));
document.setFormat("EML");
document.setSource("Inbox");
document.setLanguage(ConfigUtil.getConfigStringValue(ConfigType.DEFAULT_LANGUAGE));
if (mailContent.getDate() == null) {
document.setCreateDate(new Date());
} else {
document.setCreateDate(mailContent.getDate());
}
// Save the document, create the base ACLs
document = DocumentUtil.createDocument(document, "admin");
// Add the tag
String tagId = ConfigUtil.getConfigStringValue(ConfigType.INBOX_TAG);
if (tagId != null) {
TagDao tagDao = new TagDao();
Tag tag = tagDao.getById(tagId);
if (tag != null) {
tagDao.updateTagList(document.getId(), Sets.newHashSet(tagId));
}
}
// Raise a document created event
DocumentCreatedAsyncEvent documentCreatedAsyncEvent = new DocumentCreatedAsyncEvent();
documentCreatedAsyncEvent.setUserId("admin");
documentCreatedAsyncEvent.setDocument(document);
ThreadLocalContext.get().addAsyncEvent(documentCreatedAsyncEvent);
// Add files to the document
for (EmailUtil.FileContent fileContent : mailContent.getFileContentList()) {
FileUtil.createFile(fileContent.getName(), fileContent.getFile(), fileContent.getSize(), "eng", "admin", document.getId());
}
}
public Date getLastSyncDate() {
return lastSyncDate;
}
public int getLastSyncMessageCount() {
return lastSyncMessageCount;
}
public String getLastSyncError() {
return lastSyncError;
}
}

View File

@@ -1,9 +1,11 @@
package com.sismics.docs.core.service;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import com.google.common.util.concurrent.AbstractScheduledService;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.event.RebuildIndexAsyncEvent;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.core.util.TransactionUtil;
import org.apache.lucene.index.CheckIndex;
import org.apache.lucene.index.CheckIndex.Status;
import org.apache.lucene.index.CheckIndex.Status.SegmentInfoStatus;
@@ -16,12 +18,9 @@ import org.apache.lucene.util.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.util.concurrent.AbstractScheduledService;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.event.RebuildIndexAsyncEvent;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.core.util.TransactionUtil;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
/**
* Indexing service.
@@ -115,7 +114,7 @@ public class IndexingService extends AbstractScheduledService {
}
@Override
protected void runOneIteration() throws Exception {
protected void runOneIteration() {
TransactionUtil.handle(new Runnable() {
@Override
public void run() {
@@ -129,16 +128,6 @@ public class IndexingService extends AbstractScheduledService {
return Scheduler.newFixedDelaySchedule(0, 1, TimeUnit.HOURS);
}
/**
* Destroy and rebuild Lucene index.
*
* @throws Exception
*/
public void rebuildIndex() throws Exception {
RebuildIndexAsyncEvent rebuildIndexAsyncEvent = new RebuildIndexAsyncEvent();
AppContext.getInstance().getAsyncEventBus().post(rebuildIndexAsyncEvent);
}
/**
* Getter of directory.
*

View File

@@ -1,13 +1,13 @@
package com.sismics.docs.core.util;
import javax.persistence.EntityManager;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.dao.jpa.AuditLogDao;
import com.sismics.docs.core.model.jpa.AuditLog;
import com.sismics.docs.core.model.jpa.Loggable;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
/**
* Audit log utilities.
*
@@ -17,10 +17,15 @@ public class AuditLogUtil {
/**
* Create an audit log.
*
* @param entity Entity
* @param loggable Loggable
* @param type Audit log type
* @param userId User ID
*/
public static void create(Loggable loggable, AuditLogType type, String userId) {
if (userId == null) {
userId = "admin";
}
// Get the entity ID
EntityManager em = ThreadLocalContext.get().getEntityManager();
String entityId = (String) em.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(loggable);

View File

@@ -0,0 +1,46 @@
package com.sismics.docs.core.util;
import com.sismics.docs.core.constant.AclType;
import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.jpa.AclDao;
import com.sismics.docs.core.dao.jpa.DocumentDao;
import com.sismics.docs.core.model.jpa.Acl;
import com.sismics.docs.core.model.jpa.Document;
/**
* Document utilities.
*
* @author bgamard
*/
public class DocumentUtil {
/**
* Create a document and add the base ACLs.
*
* @param document Document
* @param userId User creating the document
* @return Created document
*/
public static Document createDocument(Document document, String userId) {
DocumentDao documentDao = new DocumentDao();
String documentId = documentDao.create(document, userId);
// Create read ACL
AclDao aclDao = new AclDao();
Acl acl = new Acl();
acl.setPerm(PermType.READ);
acl.setType(AclType.USER);
acl.setSourceId(documentId);
acl.setTargetId(userId);
aclDao.create(acl, userId);
// Create write ACL
acl = new Acl();
acl.setPerm(PermType.WRITE);
acl.setType(AclType.USER);
acl.setSourceId(documentId);
acl.setTargetId(userId);
aclDao.create(acl, userId);
return document;
}
}

View File

@@ -1,20 +1,22 @@
package com.sismics.docs.core.util;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import com.google.common.base.Strings;
import com.sismics.util.context.ThreadLocalContext;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import com.google.common.base.Strings;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
/**
* Encryption utilities.
@@ -22,7 +24,6 @@ import com.google.common.base.Strings;
* @author bgamard
*/
public class EncryptionUtil {
/**
* Salt.
*/
@@ -56,6 +57,27 @@ public class EncryptionUtil {
return new CipherInputStream(is, getCipher(privateKey, Cipher.DECRYPT_MODE));
}
/**
* Decrypt a file to a temporary file using the specified private key.
*
* @param file Encrypted file
* @param privateKey Private key
* @return Decrypted temporary file
* @throws Exception
*/
public static Path decryptFile(Path file, String privateKey) throws Exception {
if (privateKey == null) {
// For unit testing
return file;
}
Path tmpFile = ThreadLocalContext.get().createTemporaryFile();
try (InputStream is = Files.newInputStream(file)) {
Files.copy(new CipherInputStream(is, getCipher(privateKey, Cipher.DECRYPT_MODE)), tmpFile, StandardCopyOption.REPLACE_EXISTING);
}
return tmpFile;
}
/**
* Return an encryption cipher.
*

View File

@@ -1,17 +0,0 @@
package com.sismics.docs.core.util;
import com.sismics.util.context.ThreadLocalContext;
/**
* Entity manager utils.
*
* @author jtremeaux
*/
public class EntityManagerUtil {
/**
* Flush the entity manager session.
*/
public static void flush() {
ThreadLocalContext.get().getEntityManager().flush();
}
}

View File

@@ -1,5 +1,27 @@
package com.sismics.docs.core.util;
import com.google.common.base.Strings;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.FileDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent;
import com.sismics.docs.core.event.FileCreatedAsyncEvent;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.tess4j.Tesseract;
import com.sismics.util.ImageDeskew;
import com.sismics.util.ImageUtil;
import com.sismics.util.Scalr;
import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.mime.MimeTypeUtil;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
@@ -7,21 +29,6 @@ import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.imageio.ImageIO;
import org.imgscalr.Scalr;
import org.imgscalr.Scalr.Method;
import org.imgscalr.Scalr.Mode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.tess4j.Tesseract;
import com.sismics.util.ImageUtil;
/**
* File entity utilities.
*
@@ -38,43 +45,47 @@ public class FileUtil {
*
* @param language Language to extract
* @param file File to extract
* @param inputStream Unencrypted input stream
* @param pdfInputStream Unencrypted PDF input stream
* @param unencryptedFile Unencrypted file
* @param unencryptedPdfFile Unencrypted PDF file
* @return Content extract
*/
public static String extractContent(String language, File file, InputStream inputStream, InputStream pdfInputStream) {
public static String extractContent(String language, File file, Path unencryptedFile, Path unencryptedPdfFile) {
String content = null;
if (ImageUtil.isImage(file.getMimeType())) {
content = ocrFile(inputStream, language);
} else if (pdfInputStream != null) {
content = PdfUtil.extractPdf(pdfInputStream);
content = ocrFile(unencryptedFile, language);
} else if (unencryptedPdfFile != null) {
content = PdfUtil.extractPdf(unencryptedPdfFile);
}
return content;
}
/**
* Optical character recognition on a stream.
* Optical character recognition on a file.
*
* @param inputStream Unencrypted input stream
* @param unecryptedFile Unencrypted file
* @param language Language to OCR
* @return Content extracted
*/
private static String ocrFile(InputStream inputStream, String language) {
private static String ocrFile(Path unecryptedFile, String language) {
Tesseract instance = Tesseract.getInstance();
String content = null;
BufferedImage image = null;
try {
BufferedImage image;
try (InputStream inputStream = Files.newInputStream(unecryptedFile)) {
image = ImageIO.read(inputStream);
} catch (IOException e) {
log.error("Error reading the image", e);
return null;
}
// Upscale and grayscale the image
BufferedImage resizedImage = Scalr.resize(image, Method.AUTOMATIC, Mode.AUTOMATIC, 3500, Scalr.OP_ANTIALIAS, Scalr.OP_GRAYSCALE);
// Upscale, grayscale and deskew the image
BufferedImage resizedImage = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 3500, Scalr.OP_ANTIALIAS, Scalr.OP_GRAYSCALE);
image.flush();
image = resizedImage;
ImageDeskew imageDeskew = new ImageDeskew(resizedImage);
BufferedImage deskewedImage = Scalr.rotate(resizedImage, - imageDeskew.getSkewAngle(), Scalr.OP_ANTIALIAS, Scalr.OP_GRAYSCALE);
resizedImage.flush();
image = deskewedImage;
// OCR the file
try {
@@ -91,46 +102,45 @@ public class FileUtil {
/**
* Save a file on the storage filesystem.
*
* @param inputStream Unencrypted input stream
* @param pdf
* @param unencryptedFile Unencrypted file
* @param unencryptedPdfFile Unencrypted PDF file
* @param file File to save
* @param privateKey Private key used for encryption
* @throws Exception
*/
public static void save(InputStream inputStream, InputStream pdfInputStream, File file, String privateKey) throws Exception {
public static void save(Path unencryptedFile, Path unencryptedPdfFile, File file, String privateKey) throws Exception {
Cipher cipher = EncryptionUtil.getEncryptionCipher(privateKey);
Path path = DirectoryUtil.getStorageDirectory().resolve(file.getId());
Files.copy(new CipherInputStream(inputStream, cipher), path);
inputStream.reset();
try (InputStream inputStream = Files.newInputStream(unencryptedFile)) {
Files.copy(new CipherInputStream(inputStream, cipher), path);
}
// Generate file variations
saveVariations(file, inputStream, pdfInputStream, cipher);
saveVariations(file, unencryptedFile, unencryptedPdfFile, cipher);
}
/**
* Generate file variations.
*
* @param file File from database
* @param inputStream Unencrypted input stream
* @param pdfInputStream Unencrypted PDF input stream
* @param unencryptedFile Unencrypted file
* @param unencryptedPdfFile Unencrypted PDF file
* @param cipher Cipher to use for encryption
* @throws Exception
*/
public static void saveVariations(File file, InputStream inputStream, InputStream pdfInputStream, Cipher cipher) throws Exception {
private static void saveVariations(File file, Path unencryptedFile, Path unencryptedPdfFile, Cipher cipher) throws Exception {
BufferedImage image = null;
if (ImageUtil.isImage(file.getMimeType())) {
image = ImageIO.read(inputStream);
inputStream.reset();
} else if(pdfInputStream != null) {
try (InputStream inputStream = Files.newInputStream(unencryptedFile)) {
image = ImageIO.read(inputStream);
}
} else if (unencryptedPdfFile != null) {
// Generate preview from the first page of the PDF
image = PdfUtil.renderFirstPage(pdfInputStream);
pdfInputStream.reset();
image = PdfUtil.renderFirstPage(unencryptedPdfFile);
}
if (image != null) {
// Generate thumbnails from image
BufferedImage web = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 1280, Scalr.OP_ANTIALIAS);
BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 256, Scalr.OP_ANTIALIAS);
BufferedImage web = Scalr.resize(image, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.AUTOMATIC, 1280);
BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.AUTOMATIC, 256);
image.flush();
// Write "web" encrypted image
@@ -151,7 +161,6 @@ public class FileUtil {
* Remove a file from the storage filesystem.
*
* @param file File to delete
* @throws IOException
*/
public static void delete(File file) throws IOException {
Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId());
@@ -168,4 +177,92 @@ public class FileUtil {
Files.delete(thumbnailFile);
}
}
/**
* Create a new file.
*
* @param name File name, can be null
* @param unencryptedFile Path to the unencrypted file
* @param fileSize File size
* @param language File language, can be null if associated to no document
* @param userId User ID creating the file
* @param documentId Associated document ID or null if no document
* @return File ID
* @throws Exception e
*/
public static String createFile(String name, Path unencryptedFile, long fileSize, String language, String userId, String documentId) throws Exception {
// Validate mime type
String mimeType;
try {
mimeType = MimeTypeUtil.guessMimeType(unencryptedFile, name);
} catch (IOException e) {
throw new IOException("ErrorGuessMime", e);
}
// Validate user quota
UserDao userDao = new UserDao();
User user = userDao.getById(userId);
if (user.getStorageCurrent() + fileSize > user.getStorageQuota()) {
throw new IOException("QuotaReached");
}
// Validate global quota
String globalStorageQuotaStr = System.getenv(Constants.GLOBAL_QUOTA_ENV);
if (!Strings.isNullOrEmpty(globalStorageQuotaStr)) {
long globalStorageQuota = Long.valueOf(globalStorageQuotaStr);
long globalStorageCurrent = userDao.getGlobalStorageCurrent();
if (globalStorageCurrent + fileSize > globalStorageQuota) {
throw new IOException("QuotaReached");
}
}
// Get files of this document
FileDao fileDao = new FileDao();
int order = 0;
if (documentId != null) {
for (File file : fileDao.getByDocumentId(userId, documentId)) {
file.setOrder(order++);
}
}
// Create the file
File file = new File();
file.setOrder(order);
file.setDocumentId(documentId);
file.setName(StringUtils.abbreviate(name, 200));
file.setMimeType(mimeType);
file.setUserId(userId);
String fileId = fileDao.create(file, userId);
// Guess the mime type a second time, for open document format (first detected as simple ZIP file)
file.setMimeType(MimeTypeUtil.guessOpenDocumentFormat(file, unencryptedFile));
// Convert to PDF if necessary (for thumbnail and text extraction)
java.nio.file.Path unencryptedPdfFile = PdfUtil.convertToPdf(file, unencryptedFile);
// Save the file
FileUtil.save(unencryptedFile, unencryptedPdfFile, file, user.getPrivateKey());
// Update the user quota
user.setStorageCurrent(user.getStorageCurrent() + fileSize);
userDao.updateQuota(user);
// Raise a new file created event and document updated event if we have a document
if (documentId != null) {
FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent();
fileCreatedAsyncEvent.setUserId(userId);
fileCreatedAsyncEvent.setLanguage(language);
fileCreatedAsyncEvent.setFile(file);
fileCreatedAsyncEvent.setUnencryptedFile(unencryptedFile);
fileCreatedAsyncEvent.setUnencryptedPdfFile(unencryptedPdfFile);
ThreadLocalContext.get().addAsyncEvent(fileCreatedAsyncEvent);
DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent();
documentUpdatedAsyncEvent.setUserId(userId);
documentUpdatedAsyncEvent.setDocumentId(documentId);
ThreadLocalContext.get().addAsyncEvent(documentUpdatedAsyncEvent);
}
return fileId;
}
}

View File

@@ -1,18 +1,18 @@
package com.sismics.docs.core.util;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import javax.imageio.ImageIO;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.common.io.Resources;
import com.lowagie.text.*;
import com.lowagie.text.pdf.PdfWriter;
import com.sismics.docs.core.dao.jpa.dto.DocumentDto;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.pdf.PdfPage;
import com.sismics.util.ImageUtil;
import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.mime.MimeType;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -32,13 +32,17 @@ import org.odftoolkit.odfdom.doc.OdfTextDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.common.io.Closer;
import com.sismics.docs.core.dao.jpa.dto.DocumentDto;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.pdf.PdfPage;
import com.sismics.util.ImageUtil;
import com.sismics.util.mime.MimeType;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* PDF utilities.
@@ -54,17 +58,17 @@ public class PdfUtil {
/**
* Extract text from a PDF.
*
* @param inputStream Unencrypted input stream
* @param unencryptedPdfFile Unencrypted PDF file
* @return Content extracted
*/
public static String extractPdf(InputStream inputStream) {
public static String extractPdf(Path unencryptedPdfFile) {
String content = null;
PDDocument pdfDocument = null;
try {
try (InputStream inputStream = Files.newInputStream(unencryptedPdfFile)) {
PDFTextStripper stripper = new PDFTextStripper();
pdfDocument = PDDocument.load(inputStream);
content = stripper.getText(pdfDocument);
} catch (IOException e) {
} catch (Exception e) {
log.error("Error while extracting text from the PDF", e);
} finally {
if (pdfDocument != null) {
@@ -83,65 +87,86 @@ public class PdfUtil {
* Convert a file to PDF if necessary.
*
* @param file File
* @param inputStream InputStream
* @param reset Reset the stream after usage
* @return PDF input stream
* @throws Exception
* @param unencryptedFile Unencrypted file
* @return PDF temporary file
*/
public static InputStream convertToPdf(File file, InputStream inputStream, boolean reset) throws Exception {
public static Path convertToPdf(File file, Path unencryptedFile) throws Exception {
if (file.getMimeType().equals(MimeType.APPLICATION_PDF)) {
// It's already PDF, just return the input
return inputStream;
// It's already PDF, just return the file
return unencryptedFile;
}
if (file.getMimeType().equals(MimeType.OFFICE_DOCUMENT)) {
return convertOfficeDocument(inputStream, reset);
return convertOfficeDocument(unencryptedFile);
}
if (file.getMimeType().equals(MimeType.OPEN_DOCUMENT_TEXT)) {
return convertOpenDocumentText(inputStream, reset);
return convertOpenDocumentText(unencryptedFile);
}
if (file.getMimeType().equals(MimeType.TEXT_PLAIN) || file.getMimeType().equals(MimeType.TEXT_CSV)) {
return convertTextPlain(unencryptedFile);
}
// PDF conversion not necessary/possible
return null;
}
/**
* Convert a text plain document to PDF.
*
* @param unencryptedFile Unencrypted file
* @return PDF file
*/
private static Path convertTextPlain(Path unencryptedFile) throws Exception {
Document output = new Document(PageSize.A4, 40, 40, 40, 40);
Path tempFile = ThreadLocalContext.get().createTemporaryFile();
OutputStream pdfOutputStream = Files.newOutputStream(tempFile);
PdfWriter.getInstance(output, pdfOutputStream);
output.open();
String content = new String(Files.readAllBytes(unencryptedFile), Charsets.UTF_8);
Font font = FontFactory.getFont("LiberationMono-Regular");
Paragraph paragraph = new Paragraph(content, font);
paragraph.setAlignment(Element.ALIGN_LEFT);
output.add(paragraph);
output.close();
return tempFile;
}
/**
* Convert an open document text file to PDF.
*
* @param inputStream Unencrypted input stream
* @param reset Reset the stream after usage
* @return PDF input stream
* @throws Exception
* @param unencryptedFile Unencrypted file
* @return PDF file
*/
private static InputStream convertOpenDocumentText(InputStream inputStream, boolean reset) throws Exception {
ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
OdfTextDocument document = OdfTextDocument.loadDocument(inputStream);
PdfOptions options = PdfOptions.create();
PdfConverter.getInstance().convert(document, pdfOutputStream, options);
if (reset) {
inputStream.reset();
private static Path convertOpenDocumentText(Path unencryptedFile) throws Exception {
Path tempFile = ThreadLocalContext.get().createTemporaryFile();
try (InputStream inputStream = Files.newInputStream(unencryptedFile);
OutputStream outputStream = Files.newOutputStream(tempFile)) {
OdfTextDocument document = OdfTextDocument.loadDocument(inputStream);
PdfOptions options = PdfOptions.create();
PdfConverter.getInstance().convert(document, outputStream, options);
}
return new ByteArrayInputStream(pdfOutputStream.toByteArray());
return tempFile;
}
/**
* Convert an Office document to PDF.
*
* @param inputStream Unencrypted input stream
* @param reset Reset the stream after usage
* @return PDF input stream
* @throws Exception
* @param unencryptedFile Unencrypted file
* @return PDF file
*/
private static InputStream convertOfficeDocument(InputStream inputStream, boolean reset) throws Exception {
ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
XWPFDocument document = new XWPFDocument(inputStream);
org.apache.poi.xwpf.converter.pdf.PdfOptions options = org.apache.poi.xwpf.converter.pdf.PdfOptions.create();
org.apache.poi.xwpf.converter.pdf.PdfConverter.getInstance().convert(document, pdfOutputStream, options);
if (reset) {
inputStream.reset();
private static Path convertOfficeDocument(Path unencryptedFile) throws Exception {
Path tempFile = ThreadLocalContext.get().createTemporaryFile();
try (InputStream inputStream = Files.newInputStream(unencryptedFile);
OutputStream outputStream = Files.newOutputStream(tempFile)) {
XWPFDocument document = new XWPFDocument(inputStream);
org.apache.poi.xwpf.converter.pdf.PdfOptions options = org.apache.poi.xwpf.converter.pdf.PdfOptions.create();
org.apache.poi.xwpf.converter.pdf.PdfConverter.getInstance().convert(document, outputStream, options);
}
return new ByteArrayInputStream(pdfOutputStream.toByteArray());
return tempFile;
}
/**
@@ -152,11 +177,10 @@ public class PdfUtil {
* @param fitImageToPage Fit images to the page
* @param metadata Add a page with metadata
* @param margin Margins in millimeters
* @return PDF input stream
* @throws IOException
* @param outputStream Output stream to write to, will be closed
*/
public static InputStream convertToPdf(DocumentDto documentDto, List<File> fileList,
boolean fitImageToPage, boolean metadata, int margin) throws Exception {
public static void convertToPdf(DocumentDto documentDto, List<File> fileList,
boolean fitImageToPage, boolean metadata, int margin, OutputStream outputStream) throws Exception {
// Setup PDFBox
Closer closer = Closer.create();
MemoryUsageSetting memUsageSettings = MemoryUsageSetting.setupMixed(1000000); // 1MB max memory usage
@@ -211,83 +235,94 @@ public class PdfUtil {
// Add files
for (File file : fileList) {
Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId());
try (InputStream storedFileInputStream = file.getPrivateKey() == null ? // Try to decrypt the file if we have a private key available
Files.newInputStream(storedFile) : EncryptionUtil.decryptInputStream(Files.newInputStream(storedFile), file.getPrivateKey())) {
if (ImageUtil.isImage(file.getMimeType())) {
PDPage page = new PDPage(PDRectangle.A4); // Images into A4 pages
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
// Read the image using the correct handler. PDFBox can't do it because it relies wrongly on file extension
PDImageXObject pdImage = null;
if (file.getMimeType().equals(MimeType.IMAGE_JPEG)) {
pdImage = JPEGFactory.createFromStream(doc, storedFileInputStream);
} else if (file.getMimeType().equals(MimeType.IMAGE_GIF) || file.getMimeType().equals(MimeType.IMAGE_PNG)) {
BufferedImage bim = ImageIO.read(storedFileInputStream);
pdImage = LosslessFactory.createFromImage(doc, bim);
}
// Do we want to fill the page with the image?
if (fitImageToPage) {
// Fill the page with the image
float widthAvailable = page.getMediaBox().getWidth() - 2 * margin * mmPerInch;
float heightAvailable = page.getMediaBox().getHeight() - 2 * margin * mmPerInch;
// Decrypt the file to a temporary file
Path unencryptedFile = EncryptionUtil.decryptFile(storedFile, file.getPrivateKey());
// Compare page format and image format
if (widthAvailable / heightAvailable < (float) pdImage.getWidth() / (float) pdImage.getHeight()) {
float imageHeight = widthAvailable / pdImage.getWidth() * pdImage.getHeight();
contentStream.drawImage(pdImage, margin * mmPerInch, heightAvailable + margin * mmPerInch - imageHeight,
widthAvailable, imageHeight);
} else {
float imageWidth = heightAvailable / pdImage.getHeight() * pdImage.getWidth();
contentStream.drawImage(pdImage, margin * mmPerInch, margin * mmPerInch,
imageWidth, heightAvailable);
}
if (ImageUtil.isImage(file.getMimeType())) {
PDPage page = new PDPage(PDRectangle.A4); // Images into A4 pages
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page);
InputStream storedFileInputStream = Files.newInputStream(unencryptedFile)) {
// Read the image using the correct handler. PDFBox can't do it because it relies wrongly on file extension
PDImageXObject pdImage = null;
if (file.getMimeType().equals(MimeType.IMAGE_JPEG)) {
pdImage = JPEGFactory.createFromStream(doc, storedFileInputStream);
} else if (file.getMimeType().equals(MimeType.IMAGE_GIF) || file.getMimeType().equals(MimeType.IMAGE_PNG)) {
BufferedImage bim = ImageIO.read(storedFileInputStream);
pdImage = LosslessFactory.createFromImage(doc, bim);
}
// Do we want to fill the page with the image?
if (fitImageToPage) {
// Fill the page with the image
float widthAvailable = page.getMediaBox().getWidth() - 2 * margin * mmPerInch;
float heightAvailable = page.getMediaBox().getHeight() - 2 * margin * mmPerInch;
// Compare page format and image format
if (widthAvailable / heightAvailable < (float) pdImage.getWidth() / (float) pdImage.getHeight()) {
float imageHeight = widthAvailable / pdImage.getWidth() * pdImage.getHeight();
contentStream.drawImage(pdImage, margin * mmPerInch, heightAvailable + margin * mmPerInch - imageHeight,
widthAvailable, imageHeight);
} else {
// Draw the image as is
contentStream.drawImage(pdImage, margin * mmPerInch,
page.getMediaBox().getHeight() - pdImage.getHeight() - margin * mmPerInch);
float imageWidth = heightAvailable / pdImage.getHeight() * pdImage.getWidth();
contentStream.drawImage(pdImage, margin * mmPerInch, margin * mmPerInch,
imageWidth, heightAvailable);
}
} else {
// Draw the image as is
contentStream.drawImage(pdImage, margin * mmPerInch,
page.getMediaBox().getHeight() - pdImage.getHeight() - margin * mmPerInch);
}
doc.addPage(page);
} else {
// Try to convert the file to PDF
InputStream pdfInputStream = convertToPdf(file, storedFileInputStream, false);
if (pdfInputStream != null) {
// This file is convertible to PDF, just add it to the end
try {
PDDocument mergeDoc = PDDocument.load(pdfInputStream, memUsageSettings);
closer.register(mergeDoc);
PDFMergerUtility pdfMergerUtility = new PDFMergerUtility();
pdfMergerUtility.appendDocument(doc, mergeDoc);
} finally {
pdfInputStream.close();
}
}
// All other non-PDF-convertible files are ignored
}
doc.addPage(page);
} else {
// Try to convert the file to PDF
Path unencryptedPdfFile = convertToPdf(file, unencryptedFile);
if (unencryptedPdfFile != null) {
// This file is convertible to PDF, just add it to the end
PDDocument mergeDoc = PDDocument.load(unencryptedPdfFile.toFile(), memUsageSettings);
closer.register(mergeDoc);
PDFMergerUtility pdfMergerUtility = new PDFMergerUtility();
pdfMergerUtility.appendDocument(doc, mergeDoc);
}
// All other non-PDF-convertible files are ignored
}
}
// Save to a temporary file
try (TemporaryFileStream temporaryFileStream = new TemporaryFileStream()) {
doc.save(temporaryFileStream.openWriteStream());
closer.close(); // Close all remaining opened PDF
return temporaryFileStream.openReadStream();
}
doc.save(outputStream); // Write to the output stream
closer.close(); // Close all remaining opened PDF
}
}
/**
* Render the first page of a PDF.
*
* @param inputStream PDF document
* @param unencryptedFile PDF document
* @return Render of the first page
* @throws IOException
*/
public static BufferedImage renderFirstPage(InputStream inputStream) throws IOException {
try (PDDocument pdfDocument = PDDocument.load(inputStream)) {
public static BufferedImage renderFirstPage(Path unencryptedFile) throws IOException {
try (InputStream inputStream = Files.newInputStream(unencryptedFile);
PDDocument pdfDocument = PDDocument.load(inputStream)) {
PDFRenderer renderer = new PDFRenderer(pdfDocument);
return renderer.renderImage(0);
}
}
/**
* Register fonts.
*/
public static void registerFonts() {
URL url = Resources.getResource("fonts/LiberationMono-Regular.ttf");
try (InputStream is = url.openStream()) {
Path file = Files.createTempFile("sismics_docs_font_mono", ".ttf");
try (OutputStream os = Files.newOutputStream(file)) {
ByteStreams.copy(is, os);
}
FontFactory.register(file.toAbsolutePath().toString(), "LiberationMono-Regular");
FontFactory.registerDirectories();
} catch (IOException e) {
log.error("Error loading font", e);
}
}
}

View File

@@ -0,0 +1,76 @@
package com.sismics.docs.core.util;
import com.google.common.collect.Lists;
import com.sismics.docs.core.constant.AclTargetType;
import com.sismics.docs.core.constant.AclType;
import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.jpa.AclDao;
import com.sismics.docs.core.dao.jpa.DocumentDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.dao.jpa.criteria.UserCriteria;
import com.sismics.docs.core.dao.jpa.dto.RouteStepDto;
import com.sismics.docs.core.dao.jpa.dto.UserDto;
import com.sismics.docs.core.event.RouteStepValidateEvent;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.Acl;
import com.sismics.docs.core.model.jpa.Document;
import java.util.List;
/**
* Routing utilities.
*
* @author bgamard
*/
public class RoutingUtil {
/**
* Update routing ACLs according to the current route step.
*
* @param sourceId Source ID
* @param currentStep Current route step
* @param previousStep Previous route step
* @param userId User ID
*/
public static void updateAcl(String sourceId, RouteStepDto currentStep, RouteStepDto previousStep, String userId) {
AclDao aclDao = new AclDao();
if (previousStep != null) {
// Remove the previous ACL
aclDao.delete(sourceId, PermType.READ, previousStep.getTargetId(), userId, AclType.ROUTING);
}
if (currentStep != null) {
// Create a temporary READ ACL
Acl acl = new Acl();
acl.setPerm(PermType.READ);
acl.setType(AclType.ROUTING);
acl.setSourceId(sourceId);
acl.setTargetId(currentStep.getTargetId());
aclDao.create(acl, userId);
}
}
public static void sendRouteStepEmail(String documentId, RouteStepDto routeStepDto) {
DocumentDao documentDao = new DocumentDao();
Document document = documentDao.getById(documentId);
List<UserDto> userDtoList = Lists.newArrayList();
UserDao userDao = new UserDao();
switch (AclTargetType.valueOf(routeStepDto.getTargetType())) {
case USER:
userDtoList.addAll(userDao.findByCriteria(new UserCriteria().setUserId(routeStepDto.getTargetId()), null));
break;
case GROUP:
userDtoList.addAll(userDao.findByCriteria(new UserCriteria().setGroupId(routeStepDto.getTargetId()), null));
break;
}
// Fire route step validate events
for (UserDto userDto : userDtoList) {
RouteStepValidateEvent routeStepValidateEvent = new RouteStepValidateEvent();
routeStepValidateEvent.setUser(userDto);
routeStepValidateEvent.setDocument(document);
AppContext.getInstance().getMailEventBus().post(routeStepValidateEvent);
}
}
}

View File

@@ -0,0 +1,40 @@
package com.sismics.docs.core.util;
import com.sismics.docs.core.constant.AclTargetType;
import com.sismics.docs.core.dao.jpa.GroupDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.model.jpa.Group;
import com.sismics.docs.core.model.jpa.User;
/**
* Security utilities.
*
* @author bgamard
*/
public class SecurityUtil {
/**
* Get an ACL target ID from an object name and type.
*
* @param name Object name
* @param type Object type
* @return Target ID
*/
public static String getTargetIdFromName(String name, AclTargetType type) {
switch (type) {
case USER:
UserDao userDao = new UserDao();
User user = userDao.getActiveByUsername(name);
if (user != null) {
return user.getId();
}
case GROUP:
GroupDao groupDao = new GroupDao();
Group group = groupDao.getActiveByName(name);
if (group != null) {
return group.getId();
}
}
return null;
}
}

View File

@@ -1,33 +0,0 @@
package com.sismics.docs.core.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.zip.GZIPInputStream;
/**
* Stream utilities.
*
* @author bgamard
*/
public class StreamUtil {
/**
* Detects if the stream is gzipped, and returns a uncompressed stream according to this.
*
* @param is InputStream
* @return InputStream
* @throws IOException
*/
public static InputStream detectGzip(InputStream is) throws IOException {
PushbackInputStream pb = new PushbackInputStream(is, 2);
byte [] signature = new byte[2];
pb.read(signature);
pb.unread(signature);
if(signature[0] == (byte) GZIPInputStream.GZIP_MAGIC && signature[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)) {
return new GZIPInputStream(pb);
} else {
return pb;
}
}
}

View File

@@ -1,55 +0,0 @@
package com.sismics.docs.core.util;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
/**
* Utilities for writing and reading to a temporary file.
*
* @author bgamard
*/
public class TemporaryFileStream implements Closeable {
/**
* Temporary file.
*/
private Path tempFile;
/**
* Construct a temporary file.
*
* @throws IOException
*/
public TemporaryFileStream() throws IOException {
tempFile = Files.createTempFile(UUID.randomUUID().toString(), ".tmp");
}
/**
* Open a stream for writing.
*
* @return OutputStream
* @throws IOException
*/
public OutputStream openWriteStream() throws IOException {
return Files.newOutputStream(tempFile);
}
/**
* Open a stream for reading.
*
* @return InputStream
* @throws IOException
*/
public InputStream openReadStream() throws IOException {
return Files.newInputStream(tempFile);
}
@Override
public void close() throws IOException {
Files.delete(tempFile);
}
}

View File

@@ -22,12 +22,12 @@ public class TransactionUtil {
/**
* Encapsulate a process into a transactionnal context.
*
* @param runnable
* @param runnable Runnable
*/
public static void handle(Runnable runnable) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
if (em != null) {
if (em != null && em.isOpen()) {
// We are already in a transactional context, nothing to do
runnable.run();
return;

View File

@@ -1,20 +0,0 @@
package com.sismics.docs.core.util;
import com.sismics.docs.core.model.jpa.User;
/**
* Utilitaires sur les utilisateurs.
*
* @author jtremeaux
*/
public class UserUtil {
/**
* Retourne the user's username.
*
* @param user User
* @return User name
*/
public static String getUserName(User user) {
return user.getUsername();
}
}

View File

@@ -1,227 +0,0 @@
package com.sismics.util;
import com.google.common.collect.ImmutableMap;
import java.util.Map.Entry;
/**
* Date utilities.
*
* @author jtremeaux
*/
public class DateUtil {
private final static ImmutableMap<String, String> TIMEZONE_CODE_MAP = new ImmutableMap.Builder<String, String>()
.put(" ACDT", " +10:30")
.put(" ACST", " +09:30")
.put(" ACT", " +08")
.put(" ADT", " 03")
.put(" AEDT", " +11")
.put(" AEST", " +10")
.put(" AFT", " +04:30")
.put(" AKDT", " 08")
.put(" AKST", " 09")
.put(" AMST", " +05")
.put(" AMT", " +04")
.put(" ART", " 03")
// .put(" AST", " +03")
.put(" AST", " 04")
.put(" AWDT", " +09")
.put(" AWST", " +08")
.put(" AZOST", " 01")
.put(" AZT", " +04")
.put(" BDT", " +08")
.put(" BIOT", " +06")
.put(" BIT", " 12")
.put(" BOT", " 04")
.put(" BRT", " 03")
// .put(" BST", " +06")
.put(" BST", " +01")
.put(" BTT", " +06")
.put(" CAT", " +02")
.put(" CCT", " +06:30")
.put(" CDT", " 05")
// .put(" CDT", " 04")
.put(" CEDT", " +02")
.put(" CEST", " +02")
.put(" CET", " +01")
.put(" CHADT", " +13:45")
.put(" CHAST", " +12:45")
.put(" CHOT", " 08")
.put(" ChST", " +10")
.put(" CHUT", " +10")
.put(" CIST", " 08")
.put(" CIT", " +08")
.put(" CKT", " 10")
.put(" CLST", " 03")
.put(" CLT", " 04")
.put(" COST", " 04")
.put(" COT", " 05")
.put(" CST", " 06")
// .put(" CST", " +08")
// .put(" CST", " +09:30")
// .put(" CST", " +10:30")
// .put(" CST", " 05")
.put(" CT", " +08")
.put(" CVT", " 01")
.put(" CWST", " +08:45")
.put(" CXT", " +07")
.put(" DAVT", " +07")
.put(" DDUT", " +10")
.put(" DFT", " +01")
.put(" EASST", " 05")
.put(" EAST", " 06")
.put(" EAT", " +03")
// .put(" ECT", " 04")
.put(" ECT", " 05")
.put(" EDT", " 04")
.put(" EEDT", " +03")
.put(" EEST", " +03")
.put(" EET", " +02")
.put(" EGST", " +00")
.put(" EGT", " 01")
.put(" EIT", " +09")
.put(" EST", " 05")
// .put(" EST", " +10")
.put(" FET", " +03")
.put(" FJT", " +12")
.put(" FKST", " 03")
.put(" FKT", " 04")
.put(" FNT", " 02")
.put(" GALT", " 06")
.put(" GAMT", " 09")
.put(" GET", " +04")
.put(" GFT", " 03")
.put(" GILT", " +12")
.put(" GIT", " 09")
.put(" GMT", " ")
// .put(" GST", " 02")
.put(" GST", " +04")
.put(" GYT", " 04")
.put(" HADT", " 09")
.put(" HAEC", " +02")
.put(" HAST", " 10")
.put(" HKT", " +08")
.put(" HMT", " +05")
.put(" HOVT", " +07")
.put(" HST", " 10")
.put(" ICT", " +07")
.put(" IDT", " +03")
.put(" IOT", " +03")
.put(" IRDT", " +08")
.put(" IRKT", " +09")
.put(" IRST", " +03:30")
.put(" IST", " +05:30")
// .put(" IST", " +01")
// .put(" IST", " +02")
// .put(" JST", " +09")
.put(" KGT", " +06")
.put(" KOST", " +11")
.put(" KRAT", " +07")
.put(" KST", " +09")
.put(" LHST", " +10:30")
// .put(" LHST", " +11")
.put(" LINT", " +14")
.put(" MAGT", " +12")
.put(" MART", " 09:30")
.put(" MAWT", " +05")
.put(" MDT", " 06")
.put(" MET", " +01")
.put(" MEST", " +02")
.put(" MHT", " +12")
.put(" MIST", " +11")
.put(" MIT", " 09:30")
.put(" MMT", " +06:30")
.put(" MSK", " +04")
// .put(" MST", " +08")
.put(" MST", " 07")
// .put(" MST", " +06:30")
.put(" MUT", " +04")
.put(" MVT", " +05")
.put(" MYT", " +08")
.put(" NCT", " +11")
.put(" NDT", " 02:30")
.put(" NFT", " +11:30")
.put(" NPT", " +05:45")
.put(" NST", " 03:30")
.put(" NT", " 03:30")
.put(" NUT", " 11:30")
.put(" NZDT", " +13")
.put(" NZST", " +12")
.put(" OMST", " +06")
.put(" ORAT", " +05")
.put(" PDT", " 07")
.put(" PET", " 05")
.put(" PETT", " +12")
.put(" PGT", " +10")
.put(" PHOT", " +13")
.put(" PHT", " +08")
.put(" PKT", " +05")
.put(" PMDT", " 02")
.put(" PMST", " 03")
.put(" PONT", " +11")
.put(" PST", " 08")
// .put(" PST", " +08")
.put(" RET", " +04")
.put(" ROTT", " 03")
.put(" SAKT", " +11")
.put(" SAMT", " +04")
.put(" SAST", " +02")
.put(" SBT", " +11")
.put(" SCT", " +04")
.put(" SGT", " +08")
.put(" SLT", " +05:30")
.put(" SRT", " 03")
.put(" SST", " 11")
// .put(" SST", " +08")
.put(" SYOT", " +03")
.put(" TAHT", " 10")
.put(" THA", " +07")
.put(" TFT", " +05")
.put(" TJT", " +05")
.put(" TKT", " +14")
.put(" TLT", " +09")
.put(" TMT", " +05")
.put(" TOT", " +13")
.put(" TVT", " +12")
.put(" UCT", " ")
.put(" ULAT", " +08")
.put(" UTC", " ")
.put(" UYST", " 02")
.put(" UYT", " 03")
.put(" UZT", " +05")
.put(" VET", " 04:30")
.put(" VLAT", " +10")
.put(" VOLT", " +04")
.put(" VOST", " +06")
.put(" VUT", " +11")
.put(" WAKT", " +12")
.put(" WAST", " +02")
.put(" WAT", " +01")
.put(" WEDT", " +01")
.put(" WEST", " +01")
.put(" WET", " ")
.put(" WST", " +08")
.put(" YAKT", " +09")
.put(" YEKT", " +05")
.build();
/**
* Try to guess the timezone code, and replace it with a timezone offset.
* Note: JodaTime can guess a few codes already, they didn't include all codes since they are not standardized.
* This method should only be used in last resort.
*
* @param date Formated date, supposedly ending with a timezone code
* @return Date with the code replaced by its offset if there is a match
*/
public static String guessTimezoneOffset(String date) {
for (Entry<String, String> entry : TIMEZONE_CODE_MAP.entrySet()) {
String code = entry.getKey();
String offset = entry.getValue();
if (date.endsWith(code)) {
return date.substring(0, date.length() - code.length()) + offset;
}
}
return date;
}
}

View File

@@ -0,0 +1,286 @@
package com.sismics.util;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.ConfigDao;
import com.sismics.docs.core.dao.jpa.dto.UserDto;
import com.sismics.docs.core.model.jpa.Config;
import com.sismics.docs.core.util.ConfigUtil;
import com.sismics.util.context.ThreadLocalContext;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.Template;
import org.apache.commons.mail.HtmlEmail;
import org.jsoup.Jsoup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.MimeBodyPart;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Emails utilities.
*
* @author jtremeaux
*/
public class EmailUtil {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(EmailUtil.class);
/**
* Returns an email content as string.
* The content is formatted from the given Freemarker template and parameters.
*
* @param templateName Template name
* @param paramRootMap Map of Freemarker parameters
* @param locale Locale
* @return Template as string
* @throws Exception e
*/
private static String getFormattedHtml(String templateName, Map<String, Object> paramRootMap, Locale locale) throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
cfg.setClassForTemplateLoading(EmailUtil.class, "/email_template");
cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_23).build());
Template template = cfg.getTemplate(templateName + "/template.ftl");
paramRootMap.put("messages", new ResourceBundleModel(MessageUtil.getMessage(locale),
new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_23).build()));
StringWriter sw = new StringWriter();
template.process(paramRootMap, sw);
return sw.toString();
}
/**
* Sending an email to a user.
*
* @param templateName Template name
* @param recipientUser Recipient user
* @param subject Email subject
* @param paramMap Email parameters
*/
private static void sendEmail(String templateName, UserDto recipientUser, String subject, Map<String, Object> paramMap) {
if (log.isInfoEnabled()) {
log.info("Sending email from template=" + templateName + " to user " + recipientUser);
}
try {
// Build email headers
HtmlEmail email = new HtmlEmail();
email.setCharset("UTF-8");
ConfigDao configDao = new ConfigDao();
// Hostname
String envHostname = System.getenv(Constants.SMTP_HOSTNAME_ENV);
if (envHostname == null) {
email.setHostName(ConfigUtil.getConfigStringValue(ConfigType.SMTP_HOSTNAME));
} else {
email.setHostName(envHostname);
}
// Port
String envPort = System.getenv(Constants.SMTP_PORT_ENV);
if (envPort == null) {
email.setSmtpPort(ConfigUtil.getConfigIntegerValue(ConfigType.SMTP_PORT));
} else {
email.setSmtpPort(Integer.valueOf(envPort));
}
// Username and password
String envUsername = System.getenv(Constants.SMTP_USERNAME_ENV);
String envPassword = System.getenv(Constants.SMTP_PASSWORD_ENV);
if (envUsername == null || envPassword == null) {
Config usernameConfig = configDao.getById(ConfigType.SMTP_USERNAME);
Config passwordConfig = configDao.getById(ConfigType.SMTP_PASSWORD);
if (usernameConfig != null && passwordConfig != null) {
email.setAuthentication(usernameConfig.getValue(), passwordConfig.getValue());
}
} else {
email.setAuthentication(envUsername, envPassword);
}
// Recipient
email.addTo(recipientUser.getEmail(), recipientUser.getUsername());
// Application name
Config themeConfig = configDao.getById(ConfigType.THEME);
String appName = "Sismics Docs";
if (themeConfig != null) {
try (JsonReader reader = Json.createReader(new StringReader(themeConfig.getValue()))) {
JsonObject themeJson = reader.readObject();
appName = themeJson.getString("name", "Sismics Docs");
}
}
// From email address (defined only by configuration value in the database)
email.setFrom(ConfigUtil.getConfigStringValue(ConfigType.SMTP_FROM), appName);
// Locale (defined only by environment variable)
java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV));
// Subject and content
email.setSubject(appName + " - " + subject);
email.setTextMsg(MessageUtil.getMessage(userLocale, "email.no_html.error"));
// Add automatic parameters
String baseUrl = System.getenv(Constants.BASE_URL_ENV);
if (Strings.isNullOrEmpty(baseUrl)) {
log.error("DOCS_BASE_URL environnement variable needs to be set for proper email links");
baseUrl = ""; // At least the mail will be sent...
}
paramMap.put("base_url", baseUrl);
paramMap.put("app_name", appName);
// Build HTML content from Freemarker template
String htmlEmailTemplate = getFormattedHtml(templateName, paramMap, userLocale);
email.setHtmlMsg(htmlEmailTemplate);
// Send the email
email.send();
} catch (Exception e) {
log.error("Error sending email with template=" + templateName + " to user " + recipientUser, e);
}
}
/**
* Sending an email to a user.
*
* @param templateName Template name
* @param recipientUser Recipient user
* @param paramMap Email parameters
*/
public static void sendEmail(String templateName, UserDto recipientUser, Map<String, Object> paramMap) {
java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV));
String subject = MessageUtil.getMessage(userLocale, "email.template." + templateName + ".subject");
sendEmail(templateName, recipientUser, subject, paramMap);
}
/**
* Parse an email content to be imported.
*
* @param part Email part
* @param mailContent Mail content modified by side-effect
*
* @throws MessagingException e
* @throws IOException e
*/
public static void parseMailContent(Part part, MailContent mailContent) throws MessagingException, IOException {
Object content = part.getContent();
if (content instanceof Multipart) {
Multipart multiPart = (Multipart) content;
int partCount = multiPart.getCount();
for (int partIndex = 0; partIndex < partCount; partIndex++) {
MimeBodyPart subPart = (MimeBodyPart) multiPart.getBodyPart(partIndex);
String disposition = subPart.getDisposition();
if (Part.ATTACHMENT.equalsIgnoreCase(disposition)) {
FileContent fileContent = new FileContent();
fileContent.name = subPart.getFileName();
fileContent.file = ThreadLocalContext.get().createTemporaryFile();
Files.copy(subPart.getInputStream(), fileContent.file, StandardCopyOption.REPLACE_EXISTING);
fileContent.size = Files.size(fileContent.file);
mailContent.fileContentList.add(fileContent);
} else {
parseMailContent(subPart, mailContent);
}
}
} else if (content instanceof Message) {
// An email attached to an email, traverse its content
parseMailContent((Message) content, mailContent);
} else if (content instanceof String) {
if (mailContent.message == null) {
// Do not overwrite the content
if (part.isMimeType("text/plain")) {
mailContent.message = (String) content;
} else if (part.isMimeType("text/html")) {
// Convert HTML to plain text
mailContent.message = new HtmlToPlainText().getPlainText(Jsoup.parse((String) content));
}
}
} else if (content instanceof InputStream) {
FileContent fileContent = new FileContent();
fileContent.file = ThreadLocalContext.get().createTemporaryFile();
Files.copy((InputStream) content, fileContent.file, StandardCopyOption.REPLACE_EXISTING);
fileContent.size = Files.size(fileContent.file);
mailContent.fileContentList.add(fileContent);
}
}
/**
* Structure defining a parsed email to be imported.
*/
public static class MailContent {
private String subject;
private String message;
private Date date;
List<FileContent> fileContentList = Lists.newArrayList();
public String getSubject() {
return subject;
}
public String getMessage() {
return message;
}
public List<FileContent> getFileContentList() {
return fileContentList;
}
public MailContent setSubject(String subject) {
this.subject = subject;
return this;
}
public Date getDate() {
return date;
}
public MailContent setDate(Date date) {
this.date = date;
return this;
}
}
/**
* Structure defining a file from an email to be imported.
*/
public static class FileContent {
private String name;
private Path file;
private long size;
public String getName() {
return name;
}
public Path getFile() {
return file;
}
public long getSize() {
return size;
}
}
}

View File

@@ -28,7 +28,7 @@ public class EnvironmentUtil {
* @return Running under Microsoft Windows
*/
public static boolean isWindows() {
return OS.indexOf("win") >= 0;
return OS.contains("win");
}
/**
@@ -37,7 +37,7 @@ public class EnvironmentUtil {
* @return Running under Mac OS
*/
public static boolean isMacOs() {
return OS.indexOf("mac") >= 0;
return OS.contains("mac");
}
/**
@@ -46,7 +46,7 @@ public class EnvironmentUtil {
* @return Running under UNIX
*/
public static boolean isUnix() {
return OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0;
return OS.contains("nix") || OS.contains("nux") || OS.contains("aix");
}
/**

View File

@@ -0,0 +1,130 @@
package com.sismics.util;
import org.jsoup.Jsoup;
import org.jsoup.helper.StringUtil;
import org.jsoup.helper.Validate;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import java.io.IOException;
/**
* HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a
* scrape.
* <p>
* Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
* </p>
* <p>
* To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:</p>
* <p><code>java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]</code></p>
* where <i>url</i> is the URL to fetch, and <i>selector</i> is an optional CSS selector.
*
* @author Jonathan Hedley, jonathan@hedley.net
*/
public class HtmlToPlainText {
private static final String userAgent = "Mozilla/5.0 (jsoup)";
private static final int timeout = 5 * 1000;
public static void main(String... args) throws IOException {
Validate.isTrue(args.length == 1 || args.length == 2, "usage: java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]");
final String url = args[0];
final String selector = args.length == 2 ? args[1] : null;
// fetch the specified URL and parse to a HTML DOM
Document doc = Jsoup.connect(url).userAgent(userAgent).timeout(timeout).get();
HtmlToPlainText formatter = new HtmlToPlainText();
if (selector != null) {
Elements elements = doc.select(selector); // get each element that matches the CSS selector
for (Element element : elements) {
String plainText = formatter.getPlainText(element); // format that element to plain text
System.out.println(plainText);
}
} else { // format the whole doc
String plainText = formatter.getPlainText(doc);
System.out.println(plainText);
}
}
/**
* Format an Element to plain-text
* @param element the root element to format
* @return formatted text
*/
public String getPlainText(Element element) {
FormattingVisitor formatter = new FormattingVisitor();
NodeTraversor.traverse(formatter, element); // walk the DOM, and call .head() and .tail() for each node
return formatter.toString();
}
// the formatting rules, implemented in a breadth-first DOM traverse
private class FormattingVisitor implements NodeVisitor {
private static final int maxWidth = 80;
private int width = 0;
private StringBuilder accum = new StringBuilder(); // holds the accumulated text
// hit when the node is first seen
public void head(Node node, int depth) {
String name = node.nodeName();
if (node instanceof TextNode)
append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM.
else if (name.equals("li"))
append("\n * ");
else if (name.equals("dt"))
append(" ");
else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5", "tr"))
append("\n");
}
// hit when all of the node's children (if any) have been visited
public void tail(Node node, int depth) {
String name = node.nodeName();
if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5"))
append("\n");
else if (name.equals("a"))
append(String.format(" <%s>", node.absUrl("href")));
}
// appends text to the string builder with a simple word wrap method
private void append(String text) {
if (text.startsWith("\n"))
width = 0; // reset counter if starts with a newline. only from formats above, not in natural text
if (text.equals(" ") &&
(accum.length() == 0 || StringUtil.in(accum.substring(accum.length() - 1), " ", "\n")))
return; // don't accumulate long runs of empty spaces
if (text.length() + width > maxWidth) { // won't fit, needs to wrap
String words[] = text.split("\\s+");
for (int i = 0; i < words.length; i++) {
String word = words[i];
boolean last = i == words.length - 1;
if (!last) // insert a space if not the last word
word = word + " ";
if (word.length() + width > maxWidth) { // wrap and reset counter
accum.append("\n").append(word);
width = word.length();
} else {
accum.append(word);
width += word.length();
}
}
} else { // fits as is, without need to wrap text
accum.append(text);
width += text.length();
}
}
@Override
public String toString() {
return accum.toString();
}
}
}

View File

@@ -1,14 +1,8 @@
package com.sismics.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* HTTP request utilities.
@@ -17,77 +11,17 @@ import java.net.URLConnection;
*/
public class HttpUtil {
/**
* Logger.
* Format of the expires header.
*/
private static final Logger log = LoggerFactory.getLogger(HttpUtil.class);
private static final SimpleDateFormat EXPIRES_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
/**
* Loads the content of an URL into a string.
* Build an Expires HTTP header.
*
* @param url URL to load
* @return Contents of the resource
* @param futureTime Expire interval
* @return Formatted header value
*/
public static String readUrlIntoString(URL url) {
URLConnection connection;
BufferedReader in = null;
try {
connection = url.openConnection();
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder sb = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
sb.append(inputLine);
}
return sb.toString();
} catch (IOException e) {
if (log.isErrorEnabled()) {
log.error("Error reading URL", e);
}
return null;
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
// NOP
}
}
}
}
public static String postUrl(URL url, String data) throws IOException {
OutputStreamWriter wr = null;
BufferedReader rd = null;
try {
URLConnection conn = url.openConnection();
conn.setDoOutput(true);
wr = new OutputStreamWriter(conn.getOutputStream());
wr.write(data);
wr.flush();
// Get the response
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = rd.readLine()) != null) {
sb.append(line).append("\n");
}
return sb.toString();
} finally {
if (wr != null) {
try {
wr.close();
} catch (IOException e) {
// NOP
}
}
if (rd != null) {
try {
rd.close();
} catch (IOException e) {
// NOP
}
}
}
public static String buildExpiresHeader(long futureTime) {
return EXPIRES_FORMAT.format(new Date().getTime() + futureTime);
}
}

View File

@@ -0,0 +1,167 @@
package com.sismics.util;
import java.awt.image.BufferedImage;
/**
* <a url=http://www.jdeskew.com/>JDeskew</a>
*/
public class ImageDeskew {
/**
* Representation of a line in the image.
*/
public class HoughLine {
// count of points in the line
public int count = 0;
// index in matrix.
public int index = 0;
// the line is represented as all x, y that solve y * cos(alpha) - x *
// sin(alpha) = d
public double alpha;
public double d;
}
// the source image
private BufferedImage cImage;
// the range of angles to search for lines
private double cAlphaStart = -20;
private double cAlphaStep = 0.2;
private int cSteps = 40 * 5;
// pre-calculation of sin and cos
private double[] cSinA;
private double[] cCosA;
// range of d
private double cDMin;
private double cDStep = 1.0;
private int cDCount;
// count of points that fit in a line
private int[] cHMatrix;
// constructor
public ImageDeskew(BufferedImage image) {
this.cImage = image;
}
// calculate the skew angle of the image cImage
public double getSkewAngle() {
ImageDeskew.HoughLine[] hl;
double sum = 0.0;
int count = 0;
// perform Hough Transformation
calc();
// top 20 of the detected lines in the image
hl = getTop(20);
if (hl.length >= 20) {
// average angle of the lines
for (int i = 0; i < 19; i++) {
sum += hl[i].alpha;
count++;
}
return (sum / count);
} else {
return 0.0d;
}
}
// calculate the count lines in the image with most points
private ImageDeskew.HoughLine[] getTop(int count) {
ImageDeskew.HoughLine[] hl = new ImageDeskew.HoughLine[count];
for (int i = 0; i < count; i++) {
hl[i] = new ImageDeskew.HoughLine();
}
ImageDeskew.HoughLine tmp;
for (int i = 0; i < (this.cHMatrix.length - 1); i++) {
if (this.cHMatrix[i] > hl[count - 1].count) {
hl[count - 1].count = this.cHMatrix[i];
hl[count - 1].index = i;
int j = count - 1;
while ((j > 0) && (hl[j].count > hl[j - 1].count)) {
tmp = hl[j];
hl[j] = hl[j - 1];
hl[j - 1] = tmp;
j--;
}
}
}
int alphaIndex;
int dIndex;
for (int i = 0; i < count; i++) {
dIndex = hl[i].index / cSteps; // integer division, no
// remainder
alphaIndex = hl[i].index - dIndex * cSteps;
hl[i].alpha = getAlpha(alphaIndex);
hl[i].d = dIndex + cDMin;
}
return hl;
}
// Hough Transformation
private void calc() {
int hMin = (int) ((this.cImage.getHeight()) / 4.0);
int hMax = (int) ((this.cImage.getHeight()) * 3.0 / 4.0);
init();
for (int y = hMin; y < hMax; y++) {
for (int x = 1; x < (this.cImage.getWidth() - 2); x++) {
// only lower edges are considered
if (ImageUtil.isBlack(this.cImage, x, y)) {
if (!ImageUtil.isBlack(this.cImage, x, y + 1)) {
calc(x, y);
}
}
}
}
}
// calculate all lines through the point (x,y)
private void calc(int x, int y) {
double d;
int dIndex;
int index;
for (int alpha = 0; alpha < (this.cSteps - 1); alpha++) {
d = y * this.cCosA[alpha] - x * this.cSinA[alpha];
dIndex = (int) (d - this.cDMin);
index = dIndex * this.cSteps + alpha;
try {
this.cHMatrix[index] += 1;
} catch (Exception ex) {
System.out.println(ex.toString());
}
}
}
private void init() {
double angle;
// pre-calculation of sin and cos
this.cSinA = new double[this.cSteps - 1];
this.cCosA = new double[this.cSteps - 1];
for (int i = 0; i < (this.cSteps - 1); i++) {
angle = getAlpha(i) * Math.PI / 180.0;
this.cSinA[i] = Math.sin(angle);
this.cCosA[i] = Math.cos(angle);
}
// range of d
this.cDMin = -this.cImage.getWidth();
this.cDCount = (int) (2.0 * ((this.cImage.getWidth() + this.cImage.getHeight())) / this.cDStep);
this.cHMatrix = new int[this.cDCount * this.cSteps];
}
private double getAlpha(int index) {
return this.cAlphaStart + (index * this.cAlphaStep);
}
}

View File

@@ -1,19 +1,20 @@
package com.sismics.util;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import com.sismics.util.mime.MimeType;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import com.sismics.util.mime.MimeType;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
/**
* Image processing utilities.
@@ -34,12 +35,23 @@ public class ImageUtil {
ImageWriter writer = null;
ImageOutputStream imageOutputStream = null;
try {
writer = (ImageWriter) iter.next();
writer = iter.next();
ImageWriteParam iwp = writer.getDefaultWriteParam();
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionQuality(1.f);
imageOutputStream = ImageIO.createImageOutputStream(outputStream);
writer.setOutput(imageOutputStream);
if (image.getColorModel().hasAlpha()) {
// Strip alpha channel
BufferedImage noAlphaImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics graphics = noAlphaImage.getGraphics();
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.drawImage(image, 0, 0, null);
image = noAlphaImage;
}
IIOImage iioImage = new IIOImage(image, null, null);
writer.write(null, iioImage, iwp);
} finally {
@@ -69,7 +81,7 @@ public class ImageUtil {
* Compute Gravatar hash.
* See https://en.gravatar.com/site/implement/hash/.
*
* @param email
* @param email Email
* @return Gravatar hash
*/
public static String computeGravatar(String email) {
@@ -81,4 +93,40 @@ public class ImageUtil {
email.trim().toLowerCase(), Charsets.UTF_8)
.toString();
}
public static boolean isBlack(BufferedImage image, int x, int y) {
if (image.getType() == BufferedImage.TYPE_BYTE_BINARY) {
WritableRaster raster = image.getRaster();
int pixelRGBValue = raster.getSample(x, y, 0);
return pixelRGBValue == 0;
}
int luminanceValue = 140;
return isBlack(image, x, y, luminanceValue);
}
public static boolean isBlack(BufferedImage image, int x, int y, int luminanceCutOff) {
int pixelRGBValue;
int r;
int g;
int b;
double luminance = 0.0;
// return white on areas outside of image boundaries
if (x < 0 || y < 0 || x > image.getWidth() || y > image.getHeight()) {
return false;
}
try {
pixelRGBValue = image.getRGB(x, y);
r = (pixelRGBValue >> 16) & 0xff;
g = (pixelRGBValue >> 8) & 0xff;
b = (pixelRGBValue) & 0xff;
luminance = (r * 0.299) + (g * 0.587) + (b * 0.114);
} catch (Exception e) {
// ignore.
}
return luminance < luminanceCutOff;
}
}

View File

@@ -1,4 +1,4 @@
package com.sismics.rest.util;
package com.sismics.util;
import javax.json.Json;
import javax.json.JsonValue;
@@ -34,4 +34,17 @@ public class JsonUtil {
}
return Json.createObjectBuilder().add("_", value).build().get("_");
}
/**
* Returns a JsonValue from an Long.
*
* @param value Value
* @return JsonValue
*/
public static JsonValue nullable(Long value) {
if (value == null) {
return JsonValue.NULL;
}
return Json.createObjectBuilder().add("_", value).build().get("_");
}
}

View File

@@ -0,0 +1,36 @@
package com.sismics.util;
import com.google.common.base.Strings;
import java.util.Locale;
/**
* Locale utilities.
*
* @author jtremeaux
*/
public class LocaleUtil {
/**
* Returns a locale from the language / country / variation code (ex: fr_FR).
*
* @param localeCode Locale code
* @return Locale instance
*/
public static Locale getLocale(String localeCode) {
if (Strings.isNullOrEmpty(localeCode)) {
return Locale.ENGLISH;
}
String[] localeCodeArray = localeCode.split("_");
String language = localeCodeArray[0];
String country = "";
String variant = "";
if (localeCodeArray.length >= 2) {
country = localeCodeArray[1];
}
if (localeCodeArray.length >= 3) {
variant = localeCodeArray[2];
}
return new Locale(language, country, variant);
}
}

View File

@@ -0,0 +1,43 @@
package com.sismics.util;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
/**
* Messages utilities.
*
* @author jtremeaux
*/
public class MessageUtil {
/**
* Returns a localized message in the specified language.
* Returns **key** if no message exists for this key.
*
* @param locale Locale
* @param key Message key
* @param args Arguments to format
* @return Formatted message
*/
public static String getMessage(Locale locale, String key, Object... args) {
ResourceBundle resources = ResourceBundle.getBundle("messages", locale);
String message;
try {
message = resources.getString(key);
} catch (MissingResourceException e) {
message = "**" + key + "**";
}
return MessageFormat.format(message, args);
}
/**
* Returns the resource bundle corresponding to the specified language.
*
* @param locale Locale
* @return Resource bundle
*/
public static ResourceBundle getMessage(Locale locale) {
return ResourceBundle.getBundle("messages", locale);
}
}

View File

@@ -0,0 +1,55 @@
package com.sismics.util;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.StringModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import java.util.Iterator;
import java.util.List;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
/**
* Override of {@link freemarker.ext.beans.ResourceBundleModel}
* to threat single quotes uniformely.
*
* @author bgamard
*/
public class ResourceBundleModel extends freemarker.ext.beans.ResourceBundleModel {
/**
* Default constructor.
*
* @param bundle Resource bundle
* @param wrapper Beans wrapper
*/
public ResourceBundleModel(ResourceBundle bundle, BeansWrapper wrapper) {
super(bundle, wrapper);
}
@SuppressWarnings("rawtypes")
@Override
public Object exec(List arguments) throws TemplateModelException {
// Must have at least one argument - the key
if (arguments.size() < 1)
throw new TemplateModelException("No message key was specified");
// Read it
Iterator it = arguments.iterator();
String key = unwrap((TemplateModel) it.next()).toString();
try {
// Copy remaining arguments into an Object[]
int args = arguments.size() - 1;
Object[] params = new Object[args];
for (int i = 0; i < args; ++i)
params[i] = unwrap((TemplateModel) it.next());
// Invoke format
return new StringModel(format(key, params), wrapper);
} catch (MissingResourceException e) {
throw new TemplateModelException("No such key: " + key);
} catch (Exception e) {
throw new TemplateModelException(e.getMessage());
}
}
}

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