From 566c56378602c11570be96f7f6e0143bb651d48e Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 10 May 2015 14:44:45 +0200 Subject: [PATCH 01/10] Android: metadata in right drawer --- .../com/sismics/docs/MainApplication.java | 2 +- .../docs/activity/DocumentViewActivity.java | 11 ++ .../res/drawable-xhdpi/ic_info_white_24dp.png | Bin 0 -> 530 bytes .../drawable-xxhdpi/ic_info_white_24dp.png | Bin 0 -> 736 bytes .../res/layout/document_view_activity.xml | 173 ++++++++++-------- .../main/res/menu/document_view_activity.xml | 7 + .../app/src/main/res/values/strings.xml | 1 + 7 files changed, 117 insertions(+), 77 deletions(-) create mode 100644 docs-android/app/src/main/res/drawable-xhdpi/ic_info_white_24dp.png create mode 100644 docs-android/app/src/main/res/drawable-xxhdpi/ic_info_white_24dp.png diff --git a/docs-android/app/src/main/java/com/sismics/docs/MainApplication.java b/docs-android/app/src/main/java/com/sismics/docs/MainApplication.java index 232f224b..340abdbf 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/MainApplication.java +++ b/docs-android/app/src/main/java/com/sismics/docs/MainApplication.java @@ -20,7 +20,7 @@ public class MainApplication extends Application { JSONObject json = PreferenceUtil.getCachedJson(getApplicationContext(), PreferenceUtil.PREF_CACHED_USER_INFO_JSON); ApplicationContext.getInstance().setUserInfo(getApplicationContext(), json); - // TODO Fullscreen preview + // TODO google docs app: right drawer with all actions, with acls, with deep metadatas // TODO Provide documents to intent action get content super.onCreate(); diff --git a/docs-android/app/src/main/java/com/sismics/docs/activity/DocumentViewActivity.java b/docs-android/app/src/main/java/com/sismics/docs/activity/DocumentViewActivity.java index f927e9ae..82c17698 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/activity/DocumentViewActivity.java +++ b/docs-android/app/src/main/java/com/sismics/docs/activity/DocumentViewActivity.java @@ -12,7 +12,9 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.support.v4.app.DialogFragment; +import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewPager; +import android.support.v4.widget.DrawerLayout; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.text.TextUtils; @@ -202,6 +204,15 @@ public class DocumentViewActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { + case R.id.info: + DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + if (drawerLayout.isDrawerVisible(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } else { + drawerLayout.openDrawer(GravityCompat.END); + } + return true; + case R.id.download_file: downloadCurrentFile(); return true; diff --git a/docs-android/app/src/main/res/drawable-xhdpi/ic_info_white_24dp.png b/docs-android/app/src/main/res/drawable-xhdpi/ic_info_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bee33abb780f054d0a6a66240da9a1f96c9c3c39 GIT binary patch literal 530 zcmV+t0`2{YP)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00D|gL_t(o!|m8lPQpMKhvAB4Av*(Mp~5X7a1at%a1Q<) z0F*6+G(k^5!p4wzj;0ro;6|u!X*aJ6V$@9AnNEl3f?v0E9%yI2`34K?xR-STOaM8W z3|Qjf6B6R%v7}FfTy~(!m{;Pm;f{(vP-aeuECCKB9iYQIs_|*31M)nQHV(EbP~aJz ztSG1edClPuge?ai$Pz~ebjX_9B%sVkW?)A#9+*pR{`y>UW^q7Ox~$ZIfa_>rEdBXv z0F0u5TtdxB3#>U4fhHzX7lDCMpeF)LqrgH0Jfnas0=~j+sxKL~A`lt{fDusl2nZS!oad#JO7YyV4x~LbZLjH6GMd{PxKQkT$y%GpD6< zgeo#4kPg0Q@_S;YI|SY^Vx3TPhCf%W{xP;rj|DDU0s^+UEa+1EE@`K~68k literal 0 HcmV?d00001 diff --git a/docs-android/app/src/main/res/drawable-xxhdpi/ic_info_white_24dp.png b/docs-android/app/src/main/res/drawable-xxhdpi/ic_info_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..185d18d1a98cde6a127feb1839064d4d92a9e1da GIT binary patch literal 736 zcmV<60w4W}P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00LM^L_t(&-tF4YN*hrW$MI|$t7%-BMtqc3qS&g-LbLEG z6m+BHAzG+~cBO_^Xm^S(3JRkJLy*u%2-$~0+&Potw~JB`>z%p(Cx!XVI>U#|+&gE^ znF58;Itn=`2j!r@M~P(`H0jVMBqk=LPlqNAmYDDfs;X5YTXWAxcg&RzBP2CP?jL+E0Hbqm=Bmp*a!ZQ<4g`e0?mx?ZE z(gEiWsjLGUvwX4c=Tv+zi9w%mnGdR<6 - - - - - - - - - - - - - - - - + + android:layout_height="match_parent"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs-android/app/src/main/res/menu/document_view_activity.xml b/docs-android/app/src/main/res/menu/document_view_activity.xml index 1a3a4d20..3c943f47 100644 --- a/docs-android/app/src/main/res/menu/document_view_activity.xml +++ b/docs-android/app/src/main/res/menu/document_view_activity.xml @@ -2,6 +2,13 @@ + + + Before date Search tags All languages + Toggle informations From 060e5e8e24ebaf15a2e14d795ed84d3659cccf90 Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 10 May 2015 21:44:39 +0200 Subject: [PATCH 02/10] Android: ACLs in right drawer --- .../docs/activity/DocumentViewActivity.java | 98 ++++++++++------ .../sismics/docs/adapter/AclListAdapter.java | 77 +++++++++++++ .../drawable-xhdpi/ic_create_grey600_24dp.png | Bin 0 -> 379 bytes .../drawable-xhdpi/ic_create_white_24dp.png | Bin 378 -> 0 bytes .../ic_file_download_grey600_24dp.png | Bin 0 -> 283 bytes .../ic_file_upload_grey600_24dp.png | Bin 0 -> 275 bytes .../drawable-xhdpi/ic_people_grey600_24dp.png | Bin 0 -> 476 bytes .../ic_create_grey600_24dp.png | Bin 0 -> 493 bytes .../drawable-xxhdpi/ic_create_white_24dp.png | Bin 490 -> 0 bytes .../ic_file_download_grey600_24dp.png | Bin 0 -> 353 bytes .../ic_file_upload_grey600_24dp.png | Bin 0 -> 339 bytes .../ic_people_grey600_24dp.png | Bin 0 -> 623 bytes .../app/src/main/res/layout/acl_list_item.xml | 65 +++++++++++ .../res/layout/document_view_activity.xml | 108 +++++++++++++++++- .../main/res/menu/document_view_activity.xml | 32 ------ .../app/src/main/res/values/strings.xml | 9 +- 16 files changed, 319 insertions(+), 70 deletions(-) create mode 100644 docs-android/app/src/main/java/com/sismics/docs/adapter/AclListAdapter.java create mode 100644 docs-android/app/src/main/res/drawable-xhdpi/ic_create_grey600_24dp.png delete mode 100644 docs-android/app/src/main/res/drawable-xhdpi/ic_create_white_24dp.png create mode 100644 docs-android/app/src/main/res/drawable-xhdpi/ic_file_download_grey600_24dp.png create mode 100644 docs-android/app/src/main/res/drawable-xhdpi/ic_file_upload_grey600_24dp.png create mode 100644 docs-android/app/src/main/res/drawable-xhdpi/ic_people_grey600_24dp.png create mode 100644 docs-android/app/src/main/res/drawable-xxhdpi/ic_create_grey600_24dp.png delete mode 100644 docs-android/app/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png create mode 100644 docs-android/app/src/main/res/drawable-xxhdpi/ic_file_download_grey600_24dp.png create mode 100644 docs-android/app/src/main/res/drawable-xxhdpi/ic_file_upload_grey600_24dp.png create mode 100644 docs-android/app/src/main/res/drawable-xxhdpi/ic_people_grey600_24dp.png create mode 100644 docs-android/app/src/main/res/layout/acl_list_item.xml diff --git a/docs-android/app/src/main/java/com/sismics/docs/activity/DocumentViewActivity.java b/docs-android/app/src/main/java/com/sismics/docs/activity/DocumentViewActivity.java index 82c17698..5ecc3dee 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/activity/DocumentViewActivity.java +++ b/docs-android/app/src/main/java/com/sismics/docs/activity/DocumentViewActivity.java @@ -23,11 +23,14 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.Button; import android.widget.ImageView; +import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.sismics.docs.R; +import com.sismics.docs.adapter.AclListAdapter; import com.sismics.docs.adapter.FilePagerAdapter; import com.sismics.docs.event.DocumentDeleteEvent; import com.sismics.docs.event.DocumentEditEvent; @@ -186,6 +189,58 @@ public class DocumentViewActivity extends AppCompatActivity { ImageView sharedImageView = (ImageView) findViewById(R.id.sharedImageView); sharedImageView.setVisibility(shared ? View.VISIBLE : View.GONE); + // Action edit document + Button button = (Button) findViewById(R.id.actionEditDocument); + button.setOnClickListener(new View.OnClickListener() { + @Override + 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); + } + }); + + // Action upload file + button = (Button) findViewById(R.id.actionUploadFile); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT) + .setType("*/*") + .putExtra("android.intent.extra.ALLOW_MULTIPLE", true) + .addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(Intent.createChooser(intent, getText(R.string.upload_from)), REQUEST_CODE_ADD_FILE); + } + }); + + // Action download document + button = (Button) findViewById(R.id.actionDownload); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + downloadZip(); + } + }); + + // Action delete document + button = (Button) findViewById(R.id.actionDelete); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + deleteDocument(); + } + }); + + // Action share + button = (Button) findViewById(R.id.actionSharing); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + DialogFragment dialog = DocShareFragment.newInstance(DocumentViewActivity.this.document.optString("id")); + dialog.show(getSupportFragmentManager(), "DocShareFragment"); + } + }); + // Grab the attached files updateFiles(); @@ -217,37 +272,10 @@ public class DocumentViewActivity extends AppCompatActivity { downloadCurrentFile(); return true; - case R.id.download_document: - downloadZip(); - return true; - - case R.id.share: - DialogFragment dialog = DocShareFragment.newInstance(document.optString("id")); - dialog.show(getSupportFragmentManager(), "DocShareFragment"); - return true; - - case R.id.upload_file: - Intent intent = new Intent(Intent.ACTION_GET_CONTENT) - .setType("*/*") - .putExtra("android.intent.extra.ALLOW_MULTIPLE", true) - .addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(Intent.createChooser(intent, getText(R.string.upload_from)), REQUEST_CODE_ADD_FILE); - return true; - - case R.id.edit: - intent = new Intent(this, DocumentEditActivity.class); - intent.putExtra("document", document.toString()); - startActivityForResult(intent, REQUEST_CODE_EDIT_DOCUMENT); - return true; - case R.id.delete_file: deleteCurrentFile(); return true; - case R.id.delete_document: - deleteDocument(); - return true; - case android.R.id.home: finish(); return true; @@ -525,17 +553,21 @@ public class DocumentViewActivity extends AppCompatActivity { DocumentResource.get(this, document.optString("id"), new JsonHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { - boolean writable = response.optBoolean("writable"); + document = response; + boolean writable = document.optBoolean("writable"); if (menu != null) { - menu.findItem(R.id.share).setVisible(writable); - menu.findItem(R.id.upload_file).setVisible(writable); - menu.findItem(R.id.edit).setVisible(writable); menu.findItem(R.id.delete_file).setVisible(writable); - menu.findItem(R.id.delete_document).setVisible(writable); } - // TODO Show the ACLs in a sliding panel from the right + 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); + + // ACLs + ListView aclListView = (ListView) findViewById(R.id.aclListView); + aclListView.setAdapter(new AclListAdapter(document.optJSONArray("acls"))); } }); } diff --git a/docs-android/app/src/main/java/com/sismics/docs/adapter/AclListAdapter.java b/docs-android/app/src/main/java/com/sismics/docs/adapter/AclListAdapter.java new file mode 100644 index 00000000..f7aabce7 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/adapter/AclListAdapter.java @@ -0,0 +1,77 @@ +package com.sismics.docs.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import com.sismics.docs.R; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * ACL list adapter. + * + * @author bgamard. + */ +public class AclListAdapter extends BaseAdapter { + /** + * Shares. + */ + private List acls; + + /** + * ACL list adapter. + * + * @param acls ACLs + */ + public AclListAdapter(JSONArray acls) { + this.acls = new ArrayList<>(); + + // Extract only share ACLs + for (int i = 0; i < acls.length(); i++) { + JSONObject acl = acls.optJSONObject(i); + this.acls.add(acl); + } + } + + @Override + public int getCount() { + return acls.size(); + } + + @Override + public JSONObject getItem(int position) { + return acls.get(position); + } + + @Override + public long getItemId(int position) { + return getItem(position).optString("id").hashCode(); + } + + @Override + public View getView(int position, View view, final ViewGroup parent) { + if (view == null) { + LayoutInflater vi = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = vi.inflate(R.layout.acl_list_item, parent, false); + } + + // Fill the view + final JSONObject acl = getItem(position); + TextView typeTextView = (TextView) view.findViewById(R.id.typeTextView); + typeTextView.setText(acl.optString("type")); + TextView nameTextView = (TextView) view.findViewById(R.id.nameTextView); + nameTextView.setText(acl.optString("name")); + TextView permTextView = (TextView) view.findViewById(R.id.permTextView); + permTextView.setText(acl.optString("perm")); + + return view; + } +} diff --git a/docs-android/app/src/main/res/drawable-xhdpi/ic_create_grey600_24dp.png b/docs-android/app/src/main/res/drawable-xhdpi/ic_create_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4c95bd5770afb117fc5363e259ee6232d445b2af GIT binary patch literal 379 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tg=CK)Uj~LMH3o);76yi2K%s^g z3=E|}g|8AA7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+;1OBOz`!jG!i)^F=12eq zKYO}3hD02Gd;K8iAqRohhpevy4EJ`huNIucmE*YPK-ivJjw0OxN4gg&sqEfty}MF> zd+NLY?2{E${O;^{tQF8WC-Ow(yrKywEgY21D{G&x5ZqJKlJG2)iFsRGoZwiq2=cf$URo!}Gi#t-jyA5l9qoiS>k zzr*`Q5&ztE0u`o)m&dd5oO{mbV8`C@$&BHduEN~?H-4HpoLJ7_Wyd$+lVQWO>x@Zp z%xnJ0{NAMeytZN9cgCuF>@R-CJg_pE&(82uhT+t0h8f!#EatOu{QNaNlJVFc#>L;y VrKz=kF}I4okhJ31{w9syt6I zFaG|YeX_TQR>*0;&<3U_XZZMQ3J+&VE^urAXCk)nG&@J?Jr#=wJpV0Ns$IpKcW5<8 zwJUsl)+i-wXTjEC&HYDyt?!nI;sXW6HH(h6-Bv0$Jl-;WKG%G-7RFl{?y;ytFI zpSs^u)z9s2uzAn)X%F{_&pQuf=h^Wqe6nPi>d%~F$9e&zY5M*%Od8MF#f4XFO!+Mg P3LpkgS3j3^P6ISpu0q6J#WFGGw$6zf_`@QTn!&So0$7b*ip`!-AwS= z#bz_L1{=Fj8%~Y$J-X#gXNrq2x*n)3`|yfo;&b)gMfYa;+VfwzIjiA1LvAqRe^kSK|#PJ@b8OxoAtLZP|zwbR}*P+yKubPA;!Qz^UWzHmOJ5=-xL-s zsk9bvOK7m2Z^6WV&c=1Y0aJegi4SKEvoLM>tRTo-Ca0uy;5>5+|B;^)J}~IW3z#)L zk3Gb+V~_I|M!S5W8w)~TH&y#?Vqj!qdGYzT=YoA}IbX27n921+d;=3-zoX*O?rTqh P&SLO%^>bP0l+XkK004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00C4~gho!--OQP4<@D-_2O zZwXu!bTR|(BJK)Y6!bF#{D=nv7X`tr1x{wbZDB47nwbGdg}ErG@{}5Ps1)Xf9@=AK z+-0Q{FR&`AoYG~$fP3m0k4WNXUC*_k#x57M@$eZk#OIkd=j>3M=se_G%TofI18#B_ zfkN@cHT#LrWse^%j|nQA@R1uGuXpM!M000)K5pB}S;2YLWivY1i;jm)tGtzpS14Xt z)_zYT9=2SSj#nryM}ln~4_iK`68O>5(D1NDO004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00Cu5L_t(&-tF2wP6I&<1>j9Xb}vFou7h$9cN~HOQE-j{ zl08$3P(TYsKuXvdkL~R9Y+?BVpEUk?#dOpytGWOMC_n*1)rwZj0rm7kU-U`Ov|a*e zppm|RYe#)+&^bpy^5#@Y{+tS_1&1XeLE9pSAz`2Fp+Z{wH#JXs@&OIB5t2SUKm(0* zpgR%ihzI>mmW=d>2AvOjos{SX3;Mrzcv8wATI?DulTzvKqqlONl*%y#enbszxe8W4nI2fbrBRuGJ10|7Y}5R{__fjL?boTCFda5Nwn jPJAQ?P=Epyph^7zYj6*RQp!$^00000NkvXXu0mjf2004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00Cl2L_t(&-tF44Q3Npz1<<{O+ie6w+F_`LA_y1)>M(F@ zO#~1?`U#kuIaph^bo=EEBsK76Y)e1zfMHub1SmiO3J|Mz*lhZa0*QNMcNNm!|5D4O2OrRct47kN2WY~KcU(7- zKKP*H%W5Otvq7g1y-rHF;DY|o9hsE!i;V+3LDM_hlo%l72 z)~p-k2OM zh|bZ0bev}kM+0)Dtspl}8_1E<0&?ZlK_d>HoEpfTBZG9Dmo+#Ns0*hI>clC5 zx^eQLj+{KGBPS2)$T<&0;G6@ZaO|L04#x_jacm$W#{!~q^dK@v3!-y$AP0^HI(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g$s z6ujx_;uw-~@9mA-T+IdoEf0TO*qit3Nmb&)j~?o pd?x=N&j&hZ5g#MSX$!a;#jYGkGF(%6b`sE&44$rjF6*2UngI3mg3tf} literal 0 HcmV?d00001 diff --git a/docs-android/app/src/main/res/drawable-xxhdpi/ic_file_upload_grey600_24dp.png b/docs-android/app/src/main/res/drawable-xxhdpi/ic_file_upload_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..77d80081f558af8d8114ae1b6de13fbd8017dd4e GIT binary patch literal 339 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g$s z6g=(e;uw-~@9outT!$QZS{}Z2uHI%>DHddTU!Cx2f%Wp1DXg|dTppGejMaTlHN+K9QjykWGSMrF6ndaYD$u67GXYWnHa?Kq_9av_DQjmis+QbEFMxC=O=n{t!Pu8 z^lO1tpUbPpEK8=7wd=Y!&7c40TLS|l6AK5(32e-=Vh#=e__oLx3Iq9f6dgcvUtAYx Ym+&VUzL)oz3G^0&r>mdKI;Vst0C~!D%>V!Z literal 0 HcmV?d00001 diff --git a/docs-android/app/src/main/res/drawable-xxhdpi/ic_people_grey600_24dp.png b/docs-android/app/src/main/res/drawable-xxhdpi/ic_people_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..198a57955e0afe4540423cec9fd2232251939e1a GIT binary patch literal 623 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g&? zz`*$2)5S3)YwVQJj64FYZots%bRjj>UweI`$^E9S>$jo2b%9=V)Lqz(+ zhq*gLpPY1>dEi^vBrluD6$-Kwf*+l9GJV;gt~yWW%0#{l&5tRHlPwO&YzqjrdJx#D z&i!;Lv)`18O#x9>39+61AG~YjZ~Swcsm^`$6opT)WzrXv5`TJpg$Ms>uD&A^t)6Y3SU;&Eexc~bpJq$7-UVy5KV_U7WM{oZ zKA$HmP;I3}u+Vgu{Qb33@9QkvPoFR<(=&K2S>*cg?)ha4*Cam`ojq^&8~=)XJ71S> zc@bf_@oCU5zVo+LjVmrMOJ3TT*?u9e;+~%6l~u`44=8RCnYZ2h)veZ5d<#22?1{Z} zRqxB&U;Zj4`P09&2|Pb;Wpdt0=ZMo{p+|FPA%fp+O8hsa7j3Wn=eq*a0fVQjpUXO@ GgeCxbQW9bS literal 0 HcmV?d00001 diff --git a/docs-android/app/src/main/res/layout/acl_list_item.xml b/docs-android/app/src/main/res/layout/acl_list_item.xml new file mode 100644 index 00000000..40384f83 --- /dev/null +++ b/docs-android/app/src/main/res/layout/acl_list_item.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs-android/app/src/main/res/layout/document_view_activity.xml b/docs-android/app/src/main/res/layout/document_view_activity.xml index b0e67d1d..19744f59 100644 --- a/docs-android/app/src/main/res/layout/document_view_activity.xml +++ b/docs-android/app/src/main/res/layout/document_view_activity.xml @@ -49,6 +49,94 @@ android:background="#fff" android:elevation="5dp"> + + + + + + + \ No newline at end of file diff --git a/docs-android/app/src/main/res/values/strings.xml b/docs-android/app/src/main/res/values/strings.xml index 9463ed2a..ac562371 100644 --- a/docs-android/app/src/main/res/values/strings.xml +++ b/docs-android/app/src/main/res/values/strings.xml @@ -29,7 +29,7 @@ Created date Download current file Downloading file number %1s - Download all files + Download Downloading document Search documents All documents @@ -54,7 +54,7 @@ Share link Error deleting the share Send share link to - Upload a file + Add file Upload a file from Settings Sign Out @@ -75,11 +75,11 @@ English Japanese Save - Edit document + Edit Network error, please try again Please wait Sending your data - Delete document + Delete Delete document Really delete this document and all associated files? Network error while deleting this document @@ -106,6 +106,7 @@ Search tags All languages Toggle informations + Who can access From 0228d4344286ddb99c5c4eb3ebcd36829d3dec9e Mon Sep 17 00:00:00 2001 From: jendib Date: Mon, 11 May 2015 11:34:49 +0200 Subject: [PATCH 03/10] Design fix --- .../main/webapp/src/partial/docs/document.html | 2 +- .../main/webapp/src/partial/docs/settings.html | 2 +- .../src/main/webapp/src/partial/docs/tag.html | 16 ++++++---------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 1d773b59..e4b91b11 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -68,6 +68,6 @@
-
+
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.html b/docs-web/src/main/webapp/src/partial/docs/settings.html index 6434e06b..d3246e12 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.html @@ -18,6 +18,6 @@
-
+
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.html b/docs-web/src/main/webapp/src/partial/docs/tag.html index 5f8d465c..c8f02165 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.html @@ -30,18 +30,14 @@
-
-

{{ tags.length }} tag{{ tags.length > 1 ? 's' : '' }}

-
-
{{ stat.name }} {{ stat.count }}
-
-
-
+

{{ tags.length }} tag{{ tags.length > 1 ? 's' : '' }}

+
+
{{ stat.name }} {{ stat.count }}
+
+
-
- -
+
\ No newline at end of file From b2a38cea628c403e87704c89902852d5ecbcd6ac Mon Sep 17 00:00:00 2001 From: jendib Date: Fri, 15 May 2015 17:30:21 +0200 Subject: [PATCH 04/10] Closes #21: Save IP and UA on login --- .../core/model/jpa/AuthenticationToken.java | 46 +++++++++++++++++++ .../src/main/resources/config.properties | 2 +- .../resources/db/update/dbupdate-010-0.sql | 4 ++ docs-web/src/dev/resources/config.properties | 2 +- .../docs/rest/resource/UserResource.java | 13 +++++- .../src/partial/docs/settings.session.html | 2 + docs-web/src/prod/resources/config.properties | 2 +- .../sismics/docs/rest/TestUserResource.java | 3 ++ 8 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 docs-core/src/main/resources/db/update/dbupdate-010-0.sql diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java index b14a7a83..7d87930c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java @@ -29,6 +29,18 @@ public class AuthenticationToken { @Column(name = "AUT_IDUSER_C", nullable = false, length = 36) private String userId; + /** + * Login IP. + */ + @Column(name = "AUT_IP_C", nullable = true, length = 45) + private String ip; + + /** + * Login user agent. + */ + @Column(name = "AUT_UA_C", nullable = true, length = 1000) + private String userAgent; + /** * Remember the user next time (long lasted session). */ @@ -100,6 +112,38 @@ public class AuthenticationToken { public void setLongLasted(boolean longLasted) { this.longLasted = longLasted; } + + /** + * Getter of ip. + * @return ip + */ + public String getIp() { + return ip; + } + + /** + * Setter of ip. + * @param ip ip + */ + public void setIp(String ip) { + this.ip = ip; + } + + /** + * Getter of userAgent. + * @return userAgent + */ + public String getUserAgent() { + return userAgent; + } + + /** + * Setter of userAgent. + * @param userAgent userAgent + */ + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } /** * Getter of creationDate. @@ -142,6 +186,8 @@ public class AuthenticationToken { return Objects.toStringHelper(this) .add("id", "**hidden**") .add("userId", userId) + .add("ip", ip) + .add("userAgent", userAgent) .add("longLasted", longLasted) .toString(); } diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index edf8e6a4..592e6288 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=9 \ No newline at end of file +db.version=10 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql new file mode 100644 index 00000000..78ee9cee --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql @@ -0,0 +1,4 @@ +alter table T_FILE alter column FIL_IDUSER_C set not null; +alter table T_AUTHENTICATION_TOKEN add column AUT_IP_C varchar(45); +alter table T_AUTHENTICATION_TOKEN add column AUT_UA_C varchar(1000); +update T_CONFIG set CFG_VALUE_C='10' where CFG_ID_C='DB_VERSION'; \ No newline at end of file diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 04b5153a..f935e8fa 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=9 \ No newline at end of file +db.version=10 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 44939ef4..78ebe922 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -1,5 +1,6 @@ package com.sismics.docs.rest.resource; +import com.google.common.base.Strings; import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao; import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao; @@ -288,12 +289,20 @@ public class UserResource extends BaseResource { if (userId == null) { throw new ForbiddenClientException(); } - + + // Get the remote IP + String ip = request.getHeader("x-forwarded-for"); + if (Strings.isNullOrEmpty(ip)) { + ip = request.getRemoteAddr(); + } + // Create a new session token AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); AuthenticationToken authenticationToken = new AuthenticationToken(); authenticationToken.setUserId(userId); authenticationToken.setLongLasted(longLasted); + authenticationToken.setIp(ip); + authenticationToken.setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000)); String token = authenticationTokenDao.create(authenticationToken); // Cleanup old session tokens @@ -566,6 +575,8 @@ public class UserResource extends BaseResource { for (AuthenticationToken authenticationToken : authenticationTokenDao.getByUserId(principal.getId())) { JSONObject session = new JSONObject(); session.put("create_date", authenticationToken.getCreationDate().getTime()); + session.put("ip", authenticationToken.getIp()); + session.put("user_agent", authenticationToken.getUserAgent()); if (authenticationToken.getLastConnectionDate() != null) { session.put("last_connection_date", authenticationToken.getLastConnectionDate().getTime()); } diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.session.html b/docs-web/src/main/webapp/src/partial/docs/settings.session.html index e2f1794d..8d32a86d 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.session.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.session.html @@ -4,6 +4,7 @@ Created date Last connection date + From Current @@ -11,6 +12,7 @@ {{ session.create_date | date: 'yyyy-MM-dd HH:mm' }} {{ session.last_connection_date | date: 'yyyy-MM-dd HH:mm' }} + {{ session.ip }} diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 04b5153a..f935e8fa 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=9 \ No newline at end of file +db.version=10 \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index b36f3105..022f4b3f 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -144,6 +144,9 @@ public class TestUserResource extends BaseJerseyTest { Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); json = response.getEntity(JSONObject.class); Assert.assertTrue(json.getJSONArray("sessions").length() > 0); + JSONObject session = json.getJSONArray("sessions").getJSONObject(0); + Assert.assertEquals("127.0.0.1", session.getString("ip")); + Assert.assertTrue(session.getString("user_agent").startsWith("Java")); // Delete all sessions userResource = resource().path("/user/session"); From ea4e3fd8f297ee41d2072cc6bf5b264741f8493c Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 17 May 2015 22:20:34 +0200 Subject: [PATCH 05/10] #20: Audit log displayed on main screen --- .../docs/core/constant/AuditLogType.java | 23 +++ .../com/sismics/docs/core/dao/jpa/AclDao.java | 20 +- .../docs/core/dao/jpa/AuditLogDao.java | 108 +++++++++++ .../docs/core/dao/jpa/DocumentDao.java | 35 ++++ .../sismics/docs/core/dao/jpa/FileDao.java | 11 ++ .../com/sismics/docs/core/dao/jpa/TagDao.java | 49 ++++- .../sismics/docs/core/dao/jpa/UserDao.java | 15 ++ .../dao/jpa/criteria/AuditLogCriteria.java | 36 ++++ .../docs/core/dao/jpa/dto/AuditLogDto.java | 91 +++++++++ .../com/sismics/docs/core/model/jpa/Acl.java | 11 +- .../sismics/docs/core/model/jpa/AuditLog.java | 178 ++++++++++++++++++ .../sismics/docs/core/model/jpa/Document.java | 12 +- .../com/sismics/docs/core/model/jpa/File.java | 12 +- .../sismics/docs/core/model/jpa/Loggable.java | 25 +++ .../com/sismics/docs/core/model/jpa/Tag.java | 12 +- .../com/sismics/docs/core/model/jpa/User.java | 16 +- .../sismics/docs/core/util/AuditLogUtil.java | 37 ++++ .../main/resources/META-INF/persistence.xml | 1 + .../resources/db/update/dbupdate-010-0.sql | 2 + .../util/filter/RequestContextFilter.java | 4 +- .../docs/rest/resource/AppResource.java | 18 -- .../docs/rest/resource/AuditLogResource.java | 90 +++++++++ .../docs/rest/resource/DocumentResource.java | 2 + .../docs/rest/resource/TagResource.java | 2 + .../app/docs/controller/DocumentDefault.js | 5 + .../src/partial/docs/document.default.html | 15 +- .../sismics/docs/rest/TestAppResource.java | 1 - .../docs/rest/TestAuditLogResource.java | 98 ++++++++++ 28 files changed, 890 insertions(+), 39 deletions(-) create mode 100644 docs-core/src/main/java/com/sismics/docs/core/constant/AuditLogType.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/AuditLogCriteria.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AuditLogDto.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/model/jpa/Loggable.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java create mode 100644 docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java create mode 100644 docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/AuditLogType.java b/docs-core/src/main/java/com/sismics/docs/core/constant/AuditLogType.java new file mode 100644 index 00000000..97dab44e --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/AuditLogType.java @@ -0,0 +1,23 @@ +package com.sismics.docs.core.constant; + +/** + * Audit log types. + * + * @author bgamard + */ +public enum AuditLogType { + /** + * Create. + */ + CREATE, + + /** + * Update. + */ + UPDATE, + + /** + * Delete. + */ + DELETE +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java index 97570e47..5609fd98 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java @@ -9,9 +9,11 @@ import javax.persistence.EntityManager; import javax.persistence.Query; import com.sismics.docs.core.constant.AclTargetType; +import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.jpa.dto.AclDto; import com.sismics.docs.core.model.jpa.Acl; +import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.util.context.ThreadLocalContext; /** @@ -35,6 +37,9 @@ public class AclDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); em.persist(acl); + // Create audit log + AuditLogUtil.create(acl, AuditLogType.CREATE); + return acl.getId(); } @@ -121,9 +126,22 @@ public class AclDao { * @param perm Permission * @param targetId Target ID */ + @SuppressWarnings("unchecked") public void delete(String sourceId, PermType perm, String targetId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId"); + + // Create audit log + Query q = em.createQuery("from Acl a where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId"); + q.setParameter("sourceId", sourceId); + q.setParameter("perm", perm); + q.setParameter("targetId", targetId); + List aclList = q.getResultList(); + for (Acl acl : aclList) { + AuditLogUtil.create(acl, AuditLogType.DELETE); + } + + // 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.setParameter("sourceId", sourceId); q.setParameter("perm", perm); q.setParameter("targetId", targetId); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java new file mode 100644 index 00000000..bb63923d --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java @@ -0,0 +1,108 @@ +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 com.google.common.base.Joiner; +import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.dao.jpa.criteria.AuditLogCriteria; +import com.sismics.docs.core.dao.jpa.dto.AuditLogDto; +import com.sismics.docs.core.model.jpa.AuditLog; +import com.sismics.docs.core.util.jpa.PaginatedList; +import com.sismics.docs.core.util.jpa.PaginatedLists; +import com.sismics.docs.core.util.jpa.QueryParam; +import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.util.context.ThreadLocalContext; + +/** + * Audit log DAO. + * + * @author bgamard + */ +public class AuditLogDao { + /** + * Creates a new audit log. + * + * @param auditLog Audit log + * @return New ID + * @throws Exception + */ + public String create(AuditLog auditLog) { + // Create the UUID + auditLog.setId(UUID.randomUUID().toString()); + + // Create the audit log + EntityManager em = ThreadLocalContext.get().getEntityManager(); + auditLog.setCreateDate(new Date()); + em.persist(auditLog); + + return auditLog.getId(); + } + + /** + * Searches audit logs by criteria. + * + * @param paginatedList List of audit logs (updated by side effects) + * @param criteria Search criteria + * @param sortCriteria Sort criteria + * @return List of audit logs + * @throws Exception + */ + public void findByCriteria(PaginatedList paginatedList, AuditLogCriteria criteria, SortCriteria sortCriteria) throws Exception { + Map parameterMap = new HashMap(); + List criteriaList = new ArrayList(); + + StringBuilder sb = new StringBuilder("select l.LOG_ID_C c0, l.LOG_CREATEDATE_D c1, l.LOG_IDENTITY_C c2, l.LOG_CLASSENTITY_C c3, l.LOG_TYPE_C c4, l.LOG_MESSAGE_C c5 "); + sb.append(" from T_AUDIT_LOG l "); + + // Adds search criteria + if (criteria.getDocumentId() != null) { + // ACL on document is not checked here, it's assumed + StringBuilder sb0 = new StringBuilder(" (l.LOG_IDENTITY_C = :documentId and l.LOG_CLASSENTITY_C = 'Document' "); + sb0.append(" or l.LOG_IDENTITY_C in (select f.FIL_ID_C from T_FILE f where f.FIL_IDDOC_C = :documentId) and l.LOG_CLASSENTITY_C = 'File' "); + sb0.append(" or l.LOG_IDENTITY_C in (select a.ACL_ID_C from T_ACL a where a.ACL_SOURCEID_C = :documentId) and l.LOG_CLASSENTITY_C = 'Acl') "); + criteriaList.add(sb0.toString()); + parameterMap.put("documentId", criteria.getDocumentId()); + } + + if (criteria.getUserId() != null) { + StringBuilder sb0 = new StringBuilder(" (l.LOG_IDENTITY_C = :userId and l.LOG_CLASSENTITY_C = 'User' "); + sb0.append(" or l.LOG_IDENTITY_C in (select t.TAG_ID_C from T_TAG t where t.TAG_IDUSER_C = :userId) and l.LOG_CLASSENTITY_C = 'Tag' "); + sb0.append(" or l.LOG_IDENTITY_C in (select d.DOC_ID_C from T_DOCUMENT d where d.DOC_IDUSER_C = :userId) and l.LOG_CLASSENTITY_C = 'Document') "); + criteriaList.add(sb0.toString()); + parameterMap.put("userId", criteria.getUserId()); + } + + if (!criteriaList.isEmpty()) { + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); + } + + // Perform the search + QueryParam queryParam = new QueryParam(sb.toString(), parameterMap); + List l = PaginatedLists.executePaginatedQuery(paginatedList, queryParam, sortCriteria); + + // Assemble results + List auditLogDtoList = new ArrayList(); + for (Object[] o : l) { + int i = 0; + AuditLogDto auditLogDto = new AuditLogDto(); + auditLogDto.setId((String) o[i++]); + auditLogDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); + auditLogDto.setEntityId((String) o[i++]); + auditLogDto.setEntityClass((String) o[i++]); + auditLogDto.setType(AuditLogType.valueOf((String) o[i++])); + auditLogDto.setMessage((String) o[i++]); + auditLogDtoList.add(auditLogDto); + } + + paginatedList.setResultList(auditLogDtoList); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java index 28e509b3..f6ae41f1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java @@ -15,11 +15,13 @@ import javax.persistence.Query; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria; import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.dao.lucene.LuceneDao; import com.sismics.docs.core.model.jpa.Document; +import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.QueryParam; @@ -47,6 +49,9 @@ public class DocumentDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); em.persist(document); + // Create audit log + AuditLogUtil.create(document, AuditLogType.CREATE); + return document.getId(); } @@ -145,6 +150,9 @@ public class DocumentDao { q.setParameter("documentId", id); q.setParameter("dateNow", dateNow); q.executeUpdate(); + + // Create audit log + AuditLogUtil.create(documentDb, AuditLogType.DELETE); } /** @@ -167,6 +175,7 @@ public class DocumentDao { * * @param paginatedList List of documents (updated by side effects) * @param criteria Search criteria + * @param sortCriteria Sort criteria * @return List of documents * @throws Exception */ @@ -248,4 +257,30 @@ public class DocumentDao { paginatedList.setResultList(documentDtoList); } + + /** + * Update a document. + * + * @param document Document to update + * @return Updated document + */ + public Document update(Document document) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // 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(); + + // Update the document + documentFromDb.setTitle(document.getTitle()); + documentFromDb.setDescription(document.getDescription()); + documentFromDb.setCreateDate(document.getCreateDate()); + documentFromDb.setLanguage(document.getLanguage()); + + // Create audit log + AuditLogUtil.create(documentFromDb, AuditLogType.UPDATE); + + return documentFromDb; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java index 6156a126..54da76f9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java @@ -8,7 +8,9 @@ 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; /** @@ -33,6 +35,9 @@ public class FileDao { file.setCreateDate(new Date()); em.persist(file); + // Create audit log + AuditLogUtil.create(file, AuditLogType.CREATE); + return file.getId(); } @@ -92,6 +97,9 @@ public class FileDao { // Delete the file Date dateNow = new Date(); fileDb.setDeleteDate(dateNow); + + // Create audit log + AuditLogUtil.create(fileDb, AuditLogType.DELETE); } /** @@ -113,6 +121,9 @@ public class FileDao { fileFromDb.setContent(file.getContent()); fileFromDb.setOrder(file.getOrder()); + // Create audit log + AuditLogUtil.create(fileFromDb, AuditLogType.UPDATE); + return file; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java index c2a66103..8345de17 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java @@ -1,15 +1,22 @@ package com.sismics.docs.core.dao.jpa; -import com.sismics.docs.core.dao.jpa.dto.TagDto; -import com.sismics.docs.core.dao.jpa.dto.TagStatDto; -import com.sismics.docs.core.model.jpa.DocumentTag; -import com.sismics.docs.core.model.jpa.Tag; -import com.sismics.util.context.ThreadLocalContext; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.UUID; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.Query; -import java.util.*; + +import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.dao.jpa.dto.TagDto; +import com.sismics.docs.core.dao.jpa.dto.TagStatDto; +import com.sismics.docs.core.model.jpa.DocumentTag; +import com.sismics.docs.core.model.jpa.Tag; +import com.sismics.docs.core.util.AuditLogUtil; +import com.sismics.util.context.ThreadLocalContext; /** * Tag DAO. @@ -153,6 +160,9 @@ public class TagDao { tag.setCreateDate(new Date()); em.persist(tag); + // Create audit log + AuditLogUtil.create(tag, AuditLogType.CREATE); + return tag.getId(); } @@ -213,6 +223,9 @@ public class TagDao { q = em.createQuery("delete DocumentTag dt where dt.tagId = :tagId"); q.setParameter("tagId", tagId); q.executeUpdate(); + + // Create audit log + AuditLogUtil.create(tagDb, AuditLogType.DELETE); } /** @@ -229,4 +242,28 @@ public class TagDao { q.setParameter("name", "%" + name + "%"); return q.getResultList(); } + + /** + * Update a tag. + * + * @param tag Tag to update + * @return Updated tag + */ + public Tag update(Tag tag) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // 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(); + + // Update the tag + tagFromDb.setName(tag.getName()); + tagFromDb.setColor(tag.getColor()); + + // Create audit log + AuditLogUtil.create(tagFromDb, AuditLogType.UPDATE); + + return tagFromDb; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java index c332cf66..d9bc4c7e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java @@ -15,10 +15,12 @@ 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.constant.Constants; import com.sismics.docs.core.dao.jpa.criteria.UserCriteria; import com.sismics.docs.core.dao.jpa.dto.UserDto; import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.QueryParam; @@ -73,11 +75,15 @@ public class UserDao { throw new Exception("AlreadyExistingUsername"); } + // Create the user user.setCreateDate(new Date()); user.setPassword(hashPassword(user.getPassword())); user.setTheme(Constants.DEFAULT_THEME_ID); em.persist(user); + // Create audit log + AuditLogUtil.create(user, AuditLogType.CREATE); + return user.getId(); } @@ -101,6 +107,9 @@ public class UserDao { userFromDb.setTheme(user.getTheme()); userFromDb.setFirstConnection(user.isFirstConnection()); + // Create audit log + AuditLogUtil.create(userFromDb, AuditLogType.UPDATE); + return user; } @@ -121,6 +130,9 @@ public class UserDao { // Update the user userFromDb.setPassword(hashPassword(user.getPassword())); + // Create audit log + AuditLogUtil.create(userFromDb, AuditLogType.UPDATE); + return user; } @@ -194,6 +206,9 @@ public class UserDao { q = em.createQuery("delete from AuthenticationToken at where at.userId = :userId"); q.setParameter("userId", userFromDb.getId()); q.executeUpdate(); + + // Create audit log + AuditLogUtil.create(userFromDb, AuditLogType.DELETE); } /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/AuditLogCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/AuditLogCriteria.java new file mode 100644 index 00000000..6c209d22 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/AuditLogCriteria.java @@ -0,0 +1,36 @@ +package com.sismics.docs.core.dao.jpa.criteria; + + + +/** + * Audit log criteria. + * + * @author bgamard + */ +public class AuditLogCriteria { + /** + * Document ID. + */ + private String documentId; + + /** + * User ID. + */ + private String userId; + + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AuditLogDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AuditLogDto.java new file mode 100644 index 00000000..44e4e920 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AuditLogDto.java @@ -0,0 +1,91 @@ +package com.sismics.docs.core.dao.jpa.dto; + +import javax.persistence.Id; + +import com.sismics.docs.core.constant.AuditLogType; + +/** + * Audit log DTO. + * + * @author bgamard + */ +public class AuditLogDto { + /** + * Audit log ID. + */ + @Id + private String id; + + /** + * Entity ID. + */ + private String entityId; + + /** + * Entity class. + */ + private String entityClass; + + /** + * Audit log type. + */ + private AuditLogType type; + + /** + * Audit log message. + */ + private String message; + + /** + * Creation date. + */ + private Long createTimestamp; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public String getEntityClass() { + return entityClass; + } + + public void setEntityClass(String entityClass) { + this.entityClass = entityClass; + } + + public AuditLogType getType() { + return type; + } + + public void setType(AuditLogType type) { + this.type = type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Long getCreateTimestamp() { + return createTimestamp; + } + + public void setCreateTimestamp(Long createTimestamp) { + this.createTimestamp = createTimestamp; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java index dc846c92..ab31e2b1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java @@ -4,6 +4,7 @@ import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Id; @@ -11,6 +12,7 @@ import javax.persistence.Table; import com.google.common.base.Objects; import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.util.AuditLogUtil; /** * ACL entity. @@ -18,8 +20,9 @@ import com.sismics.docs.core.constant.PermType; * @author bgamard */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_ACL") -public class Acl { +public class Acl implements Loggable { /** * ACL ID. */ @@ -84,6 +87,7 @@ public class Acl { this.targetId = targetId; } + @Override public Date getDeleteDate() { return deleteDate; } @@ -101,4 +105,9 @@ public class Acl { .add("targetId", targetId) .toString(); } + + @Override + public String toMessage() { + return perm.name(); + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java new file mode 100644 index 00000000..3f68ceb7 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java @@ -0,0 +1,178 @@ +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.Objects; +import com.sismics.docs.core.constant.AuditLogType; + +/** + * Audit log. + * + * @author bgamard + */ +@Entity +@Table(name = "T_AUDIT_LOG") +public class AuditLog { + /** + * Audit log ID. + */ + @Id + @Column(name = "LOG_ID_C", length = 36) + private String id; + + /** + * Entity ID. + */ + @Column(name = "LOG_IDENTITY_C", nullable = false, length = 36) + private String entityId; + + /** + * Entity class. + */ + @Column(name = "LOG_CLASSENTITY_C", nullable = false, length = 50) + private String entityClass; + + /** + * Audit log type. + */ + @Column(name = "LOG_TYPE_C", nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private AuditLogType type; + + /** + * Audit log message. + */ + @Column(name = "LOG_MESSAGE_C", length = 1000) + private String message; + + /** + * Creation date. + */ + @Column(name = "LOG_CREATEDATE_D", nullable = false) + private Date createDate; + + /** + * Getter of id. + * + * @return id + */ + public String getId() { + return id; + } + + /** + * Setter of id. + * + * @param id id + */ + public void setId(String id) { + this.id = id; + } + + /** + * Getter of entityId. + * + * @return entityId + */ + public String getEntityId() { + return entityId; + } + + /** + * Setter of entityId. + * + * @param entityId entityId + */ + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + /** + * Getter of entityClass. + * + * @return entityClass + */ + public String getEntityClass() { + return entityClass; + } + + /** + * Setter of entityClass. + * + * @param entityClass entityClass + */ + public void setEntityClass(String entityClass) { + this.entityClass = entityClass; + } + + /** + * Getter of message. + * + * @return message + */ + public String getMessage() { + return message; + } + + /** + * Setter of message. + * + * @param message message + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Getter of type. + * + * @return type + */ + public AuditLogType getType() { + return type; + } + + /** + * Setter of type. + * + * @param type type + */ + public void setType(AuditLogType type) { + this.type = type; + } + + /** + * Getter of createDate. + * + * @return createDate + */ + public Date getCreateDate() { + return createDate; + } + + /** + * Setter of createDate. + * + * @param createDate createDate + */ + public void setCreateDate(Date createDate) { + this.createDate = createDate; + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("id", id) + .add("entityId", entityId) + .add("entityClass", entityClass) + .add("type", type) + .toString(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java index 4622f4fe..c376767c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java @@ -1,11 +1,14 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.Objects; +import com.sismics.docs.core.util.AuditLogUtil; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; + import java.util.Date; /** @@ -14,8 +17,9 @@ import java.util.Date; * @author bgamard */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_DOCUMENT") -public class Document { +public class Document implements Loggable { /** * Document ID. */ @@ -172,6 +176,7 @@ public class Document { * * @return the deleteDate */ + @Override public Date getDeleteDate() { return deleteDate; } @@ -191,4 +196,9 @@ public class Document { .add("id", id) .toString(); } + + @Override + public String toMessage() { + return title; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java index 5ddd6641..673d4995 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java @@ -1,12 +1,15 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.Objects; +import com.sismics.docs.core.util.AuditLogUtil; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Lob; import javax.persistence.Table; + import java.util.Date; /** @@ -15,8 +18,9 @@ import java.util.Date; * @author bgamard */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_FILE") -public class File { +public class File implements Loggable { /** * File ID. */ @@ -144,6 +148,7 @@ public class File { * * @return the deleteDate */ + @Override public Date getDeleteDate() { return deleteDate; } @@ -217,4 +222,9 @@ public class File { .add("id", id) .toString(); } + + @Override + public String toMessage() { + return documentId; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Loggable.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Loggable.java new file mode 100644 index 00000000..3b7e1837 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Loggable.java @@ -0,0 +1,25 @@ +package com.sismics.docs.core.model.jpa; + +import java.util.Date; + +/** + * An entity which can be logged. + * + * @author bgamard + */ +public interface Loggable { + /** + * Get a string representation of this entity for logging purpose. + * Avoid returning sensitive data like passwords. + * + * @return Entity message + */ + public String toMessage(); + + /** + * Loggable are soft deletable. + * + * @return deleteDate + */ + public Date getDeleteDate(); +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java index b9e48674..c49cded3 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java @@ -1,11 +1,14 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.Objects; +import com.sismics.docs.core.util.AuditLogUtil; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; + import java.util.Date; /** @@ -14,8 +17,9 @@ import java.util.Date; * @author bgamard */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_TAG") -public class Tag { +public class Tag implements Loggable { /** * Tag ID. */ @@ -148,6 +152,7 @@ public class Tag { * * @return deleteDate */ + @Override public Date getDeleteDate() { return deleteDate; } @@ -168,4 +173,9 @@ public class Tag { .add("name", name) .toString(); } + + @Override + public String toMessage() { + return name; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java index 3a525ed5..0e6227d7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java @@ -1,12 +1,15 @@ package com.sismics.docs.core.model.jpa; -import com.google.common.base.Objects; +import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; -import java.util.Date; + +import com.google.common.base.Objects; +import com.sismics.docs.core.util.AuditLogUtil; /** * User entity. @@ -14,8 +17,9 @@ import java.util.Date; * @author jtremeaux */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_USER") -public class User { +public class User implements Loggable { /** * User ID. */ @@ -250,6 +254,7 @@ public class User { * * @return deleteDate */ + @Override public Date getDeleteDate() { return deleteDate; } @@ -286,4 +291,9 @@ public class User { .add("username", username) .toString(); } + + @Override + public String toMessage() { + return username; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java new file mode 100644 index 00000000..ffc3d089 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java @@ -0,0 +1,37 @@ +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; + +/** + * Audit log utilities. + * + * @author bgamard + */ +public class AuditLogUtil { + /** + * Create an audit log. + * + * @param entity Entity + * @param type Audit log type + */ + public static void create(Loggable loggable, AuditLogType type) { + // Get the entity ID + EntityManager em = ThreadLocalContext.get().getEntityManager(); + String entityId = (String) em.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(loggable); + + // Create the audit log + AuditLogDao auditLogDao = new AuditLogDao(); + AuditLog auditLog = new AuditLog(); + auditLog.setEntityId(entityId); + auditLog.setEntityClass(loggable.getClass().getSimpleName()); + auditLog.setType(type); + auditLog.setMessage(loggable.toMessage()); + auditLogDao.create(auditLog); + } +} diff --git a/docs-core/src/main/resources/META-INF/persistence.xml b/docs-core/src/main/resources/META-INF/persistence.xml index 51ff2cc5..16f8eff0 100644 --- a/docs-core/src/main/resources/META-INF/persistence.xml +++ b/docs-core/src/main/resources/META-INF/persistence.xml @@ -17,5 +17,6 @@ com.sismics.docs.core.model.jpa.DocumentTag com.sismics.docs.core.model.jpa.Share com.sismics.docs.core.model.jpa.Acl + com.sismics.docs.core.model.jpa.AuditLog \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql index 78ee9cee..a3ecc9c4 100644 --- a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql +++ b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql @@ -1,4 +1,6 @@ alter table T_FILE alter column FIL_IDUSER_C set not null; alter table T_AUTHENTICATION_TOKEN add column AUT_IP_C varchar(45); alter table T_AUTHENTICATION_TOKEN add column AUT_UA_C varchar(1000); +create cached table T_AUDIT_LOG ( LOG_ID_C varchar(36) not null, LOG_IDENTITY_C varchar(36) not null, LOG_CLASSENTITY_C varchar(50) not null, LOG_TYPE_C varchar(50) not null, LOG_MESSAGE_C varchar(1000), LOG_CREATEDATE_D datetime, primary key (LOG_ID_C) ); +create index IDX_LOG_COMPOSITE on T_AUDIT_LOG (LOG_IDENTITY_C, LOG_CLASSENTITY_C); update T_CONFIG set CFG_VALUE_C='10' where CFG_ID_C='DB_VERSION'; \ No newline at end of file diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java index 6582c488..c10ab7fb 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java @@ -117,8 +117,6 @@ public class RequestContextFilter implements Filter { throw new ServletException(e); } } - - ThreadLocalContext.cleanup(); // No error processing the request : commit / rollback the current transaction depending on the HTTP code if (em.isOpen()) { @@ -143,5 +141,7 @@ public class RequestContextFilter implements Filter { } } } + + ThreadLocalContext.cleanup(); } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java index 4d94ac10..9243e718 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java @@ -20,17 +20,13 @@ import org.apache.log4j.Logger; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; -import com.sismics.docs.core.dao.jpa.DocumentDao; import com.sismics.docs.core.dao.jpa.FileDao; -import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria; -import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.ConfigUtil; import com.sismics.docs.core.util.DirectoryUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; -import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; @@ -64,20 +60,6 @@ public class AppResource extends BaseResource { JSONObject response = new JSONObject(); - // Specific data - DocumentDao documentDao = new DocumentDao(); - PaginatedList paginatedList = PaginatedLists.create(1, 0); - SortCriteria sortCriteria = new SortCriteria(0, true); - DocumentCriteria documentCriteria = new DocumentCriteria(); - documentCriteria.setUserId(principal.getId()); - try { - documentDao.findByCriteria(paginatedList, documentCriteria, sortCriteria); - } catch (Exception e) { - throw new ServerException("SearchError", "Error searching in documents", e); - } - response.put("document_count", paginatedList.getResultCount()); - - // General data response.put("current_version", currentVersion.replace("-SNAPSHOT", "")); response.put("min_version", minVersion); response.put("total_memory", Runtime.getRuntime().totalMemory()); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java new file mode 100644 index 00000000..8a40472d --- /dev/null +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java @@ -0,0 +1,90 @@ +package com.sismics.docs.rest.resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; + +import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.dao.jpa.AclDao; +import com.sismics.docs.core.dao.jpa.AuditLogDao; +import com.sismics.docs.core.dao.jpa.criteria.AuditLogCriteria; +import com.sismics.docs.core.dao.jpa.dto.AuditLogDto; +import com.sismics.docs.core.util.jpa.PaginatedList; +import com.sismics.docs.core.util.jpa.PaginatedLists; +import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.rest.exception.ForbiddenClientException; +import com.sismics.rest.exception.ServerException; + +/** + * Audit log REST resources. + * + * @author bgamard + */ +@Path("/auditlog") +public class AuditLogResource extends BaseResource { + /** + * Returns the list of all logs for a document or user. + * + * @return Response + * @throws JSONException + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response list(@QueryParam("document") String documentId) throws JSONException { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // On a document or a user? + PaginatedList paginatedList = PaginatedLists.create(100, 0); + SortCriteria sortCriteria = new SortCriteria(1, true); + AuditLogCriteria criteria = new AuditLogCriteria(); + if (documentId == null) { + // Search logs for a user + criteria.setUserId(principal.getId()); + } else { + // Check ACL on the document + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(documentId, PermType.READ, principal.getId())) { + throw new ForbiddenClientException(); + } + criteria.setDocumentId(documentId); + } + + // Search the logs + try { + AuditLogDao auditLogDao = new AuditLogDao(); + auditLogDao.findByCriteria(paginatedList, criteria, sortCriteria); + } catch (Exception e) { + throw new ServerException("SearchError", "Error searching in logs", e); + } + + // Assemble the results + List logs = new ArrayList<>(); + JSONObject response = new JSONObject(); + for (AuditLogDto auditLogDto : paginatedList.getResultList()) { + JSONObject log = new JSONObject(); + log.put("id", auditLogDto.getId()); + log.put("target", auditLogDto.getEntityId()); + log.put("class", auditLogDto.getEntityClass()); + log.put("type", auditLogDto.getType().name()); + log.put("message", auditLogDto.getMessage()); + log.put("create_date", auditLogDto.getCreateTimestamp()); + logs.add(log); + } + + // Send the response + response.put("logs", logs); + response.put("total", paginatedList.getResultCount()); + return Response.ok().entity(response).build(); + } +} diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index 8e39e17c..c276e44a 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -447,6 +447,8 @@ public class DocumentResource extends BaseResource { document.setLanguage(language); } + document = documentDao.update(document); + // Update tags updateTagList(id, tagList); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java index 87fad66e..8e09de5d 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java @@ -175,6 +175,8 @@ public class TagResource extends BaseResource { tag.setColor(color); } + tagDao.update(tag); + JSONObject response = new JSONObject(); response.put("id", id); return Response.ok().entity(response).build(); diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js b/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js index 2bdba230..e5433e09 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js @@ -9,6 +9,11 @@ angular.module('docs').controller('DocumentDefault', function($scope, $state, Re $scope.app = data; }); + // Load user audit log + Restangular.one('auditlog').get().then(function(data) { + $scope.logs = data.logs; + }); + /** * Load unlinked files. */ diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index e68adb58..ba3b8790 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -1,10 +1,6 @@
-

- {{ app.document_count }} document{{ app.document_count > 1 ? 's' : '' }} in the database -

-
@@ -45,6 +41,17 @@
+ + + + + + + + + +
DateMessage
{{ log.create_date | date: 'yyyy-MM-dd HH:mm' }}{{ log.class }} {{ log.type }} {{ log.message }}
+
  • Version: {{ app.current_version }}
  • diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java index 3c79eb07..64e2e7fb 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java @@ -41,7 +41,6 @@ public class TestAppResource extends BaseJerseyTest { Assert.assertTrue(freeMemory > 0); Long totalMemory = json.getLong("total_memory"); Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory); - Assert.assertEquals(0, json.getInt("document_count")); // Rebuild Lucene index appResource = resource().path("/app/batch/reindex"); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java new file mode 100644 index 00000000..c94ad714 --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java @@ -0,0 +1,98 @@ +package com.sismics.docs.rest; + +import java.util.Date; + +import com.sismics.docs.rest.filter.CookieAuthenticationFilter; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.ClientResponse.Status; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.core.util.MultivaluedMapImpl; + +import junit.framework.Assert; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.junit.Test; + +/** + * Test the audit log resource. + * + * @author bgamard + */ +public class TestAuditLogResource extends BaseJerseyTest { + /** + * Test the audit log resource. + * + * @throws JSONException + */ + @Test + public void testAuditLogResource() throws JSONException { + // Login auditlog1 + clientUtil.createUser("auditlog1"); + String auditlog1Token = clientUtil.login("auditlog1"); + + // Create a tag + WebResource tagResource = resource().path("/tag"); + tagResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + MultivaluedMapImpl postParams = new MultivaluedMapImpl(); + postParams.add("name", "SuperTag"); + postParams.add("color", "#ffff00"); + ClientResponse response = tagResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + JSONObject json = response.getEntity(JSONObject.class); + String tag1Id = json.optString("id"); + Assert.assertNotNull(tag1Id); + + // Create a document + WebResource documentResource = resource().path("/document"); + documentResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("title", "My super title document 1"); + postParams.add("description", "My super description for document 1"); + postParams.add("tags", tag1Id); + postParams.add("language", "eng"); + long create1Date = new Date().getTime(); + postParams.add("create_date", create1Date); + response = documentResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + String document1Id = json.optString("id"); + Assert.assertNotNull(document1Id); + + // Get all logs for the document + WebResource auditLogResource = resource().path("/auditlog"); + auditLogResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + response = auditLogResource.queryParam("document", document1Id).get(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + JSONArray logs = json.getJSONArray("logs"); + Assert.assertTrue(logs.length() == 3); + + // Get all logs for the current user + auditLogResource = resource().path("/auditlog"); + auditLogResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + response = auditLogResource.get(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + logs = json.getJSONArray("logs"); + Assert.assertTrue(logs.length() == 3); + + // Deletes a tag + tagResource = resource().path("/tag/" + tag1Id); + tagResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + response = tagResource.delete(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + Assert.assertEquals("ok", json.getString("status")); + + // Get all logs for the current user + auditLogResource = resource().path("/auditlog"); + auditLogResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + response = auditLogResource.get(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + logs = json.getJSONArray("logs"); + Assert.assertTrue(logs.length() == 4); + } +} \ No newline at end of file From 6add34bb33d588add2f4253fae3cc97f320093f4 Mon Sep 17 00:00:00 2001 From: jendib Date: Sat, 23 May 2015 19:16:38 +0200 Subject: [PATCH 06/10] #20: Display logs on documents --- .../docs/core/dao/jpa/AuditLogDao.java | 1 + .../docs/rest/resource/AuditLogResource.java | 2 +- .../docs/rest/resource/FileResource.java | 5 +- .../src/app/docs/controller/DocumentView.js | 9 +- .../webapp/src/app/docs/directive/AuditLog.js | 15 ++++ docs-web/src/main/webapp/src/index.html | 1 + .../src/partial/docs/directive.auditlog.html | 32 +++++++ .../src/partial/docs/document.default.html | 87 +++++++++---------- .../src/partial/docs/document.view.html | 8 ++ .../sismics/docs/rest/TestFileResource.java | 6 ++ 10 files changed, 117 insertions(+), 49 deletions(-) create mode 100644 docs-web/src/main/webapp/src/app/docs/directive/AuditLog.js create mode 100644 docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java index bb63923d..786173c8 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java @@ -75,6 +75,7 @@ public class AuditLogDao { if (criteria.getUserId() != null) { StringBuilder sb0 = new StringBuilder(" (l.LOG_IDENTITY_C = :userId and l.LOG_CLASSENTITY_C = 'User' "); sb0.append(" or l.LOG_IDENTITY_C in (select t.TAG_ID_C from T_TAG t where t.TAG_IDUSER_C = :userId) and l.LOG_CLASSENTITY_C = 'Tag' "); + // Show only logs from owned documents, ACL are lost on delete sb0.append(" or l.LOG_IDENTITY_C in (select d.DOC_ID_C from T_DOCUMENT d where d.DOC_IDUSER_C = :userId) and l.LOG_CLASSENTITY_C = 'Document') "); criteriaList.add(sb0.toString()); parameterMap.put("userId", criteria.getUserId()); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java index 8a40472d..3b431a95 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java @@ -45,7 +45,7 @@ public class AuditLogResource extends BaseResource { } // On a document or a user? - PaginatedList paginatedList = PaginatedLists.create(100, 0); + PaginatedList paginatedList = PaginatedLists.create(20, 0); SortCriteria sortCriteria = new SortCriteria(1, true); AuditLogCriteria criteria = new AuditLogCriteria(); if (documentId == null) { diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 4f909015..f86383f4 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -57,6 +57,7 @@ import com.sismics.rest.exception.ServerException; import com.sismics.rest.util.ValidationUtil; import com.sismics.util.mime.MimeType; import com.sismics.util.mime.MimeTypeUtil; +import com.sun.jersey.api.client.ClientResponse.Status; import com.sun.jersey.multipart.FormDataBodyPart; import com.sun.jersey.multipart.FormDataParam; @@ -417,7 +418,7 @@ public class FileResource extends BaseResource { } } } catch (NoResultException e) { - throw new ClientException("FileNotFound", MessageFormat.format("File not found: {0}", fileId)); + return Response.status(Status.NOT_FOUND).build(); } @@ -461,7 +462,7 @@ public class FileResource extends BaseResource { } }; } catch (Exception e) { - throw new ServerException("FileError", "Error while reading the file", e); + return Response.status(Status.SERVICE_UNAVAILABLE).build(); } return Response.ok(stream) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js b/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js index 3bd93077..fe80252a 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js @@ -4,11 +4,18 @@ * Document view controller. */ angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $modal, Restangular, $upload, $q) { - // Load data from server + // Load document data from server Restangular.one('document', $stateParams.id).get().then(function(data) { $scope.document = data; }); + // Load audit log data from server + Restangular.one('auditlog').get({ + document: $stateParams.id + }).then(function(data) { + $scope.logs = data.logs; + }); + // Watch for ACLs change and group them for easy displaying $scope.$watch('document.acls', function(acls) { $scope.acls = _.groupBy(acls, function(acl) { diff --git a/docs-web/src/main/webapp/src/app/docs/directive/AuditLog.js b/docs-web/src/main/webapp/src/app/docs/directive/AuditLog.js new file mode 100644 index 00000000..42d8b391 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/directive/AuditLog.js @@ -0,0 +1,15 @@ +'use strict'; + +/** + * Audit log directive. + */ +angular.module('docs').directive('auditLog', function() { + return { + restrict: 'E', + templateUrl: 'partial/docs/directive.auditlog.html', + replace: true, + scope: { + logs: '=' + } + } +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index 8fee9ee4..ce5a6e97 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -62,6 +62,7 @@ + diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html new file mode 100644 index 00000000..e15252b2 --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html @@ -0,0 +1,32 @@ + + + + + +
    {{ log.create_date | date: 'yyyy-MM-dd HH:mm' }} + {{ log.class }} + + created + updated + deleted + + + + : + + {{ log.message }} + + + Open + + + {{ log.message }} + + + {{ log.message }} + + + {{ log.message }} + + +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index ba3b8790..23598581 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -1,61 +1,58 @@
    -
    -
    -
    - - - -
    -
    - +
    +

    Quick upload

    +
    +
    +
    + + + +
    +
    + +
    +
    + +
    +
    -
    - +
    + +
    +

    + {{ file.status }} +

    +
    +
    -
    -
    -

    - {{ file.status }} -

    -
    - -
    -
    +

    + + Drag & drop files here to upload +

    -

    - - Drag & drop files here to upload -

    -
    - -
    - +
    + +
    - - - - - - - - - -
    DateMessage
    {{ log.create_date | date: 'yyyy-MM-dd HH:mm' }}{{ log.class }} {{ log.type }} {{ log.message }}
    +
    +

    Latest activity

    + +
    -
    -
      -
    • Version: {{ app.current_version }}
    • -
    • Memory: {{ app.free_memory / 1000000 | number: 0 }}/{{ app.total_memory / 1000000 | number: 0 }} MB
    • -
    -
    +
    +
      +
    • Version: {{ app.current_version }}
    • +
    • Memory: {{ app.free_memory / 1000000 | number: 0 }}/{{ app.total_memory / 1000000 | number: 0 }} MB
    • +
    +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 1560225c..43988090 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -135,6 +135,14 @@
    + + + + Activity + + + +
    diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index c29dfec5..dc66674d 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -178,6 +178,12 @@ public class TestFileResource extends BaseJerseyTest { json = response.getEntity(JSONObject.class); Assert.assertEquals("ok", json.getString("status")); + // Get the file data (not found) + fileResource = resource().path("/file/" + file1Id + "/data"); + fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken)); + response = fileResource.get(ClientResponse.class); + Assert.assertEquals(Status.NOT_FOUND, Status.fromStatusCode(response.getStatus())); + // Check that files are deleted from FS storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile(); java.io.File webFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_web").toFile(); From 4625f9e42adbb89cd5d27e36b384946cb02a9928 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 21 Aug 2015 00:16:33 +0200 Subject: [PATCH 07/10] Tesseract package for japanese language --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9ea8d2ad..f485f1de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM sismics/debian-java7-jetty9 MAINTAINER benjamin.gam@gmail.com -RUN apt-get -y -q install tesseract-ocr tesseract-ocr-fra +RUN apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-jpn ENV TESSDATA_PREFIX /usr/share/tesseract-ocr ENV LC_NUMERIC C From f8d889bb1ffcb7543899416077caa616e8118ca5 Mon Sep 17 00:00:00 2001 From: jendib Date: Mon, 24 Aug 2015 22:18:47 +0200 Subject: [PATCH 08/10] Closes #22: incorrect composite ID for DocumentTag --- .../main/java/com/sismics/docs/core/model/jpa/DocumentTag.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java index 0dad273b..53d0b770 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java @@ -31,14 +31,12 @@ public class DocumentTag implements Serializable { /** * Document ID. */ - @Id @Column(name = "DOT_IDDOCUMENT_C", length = 36) private String documentId; /** * Tag ID. */ - @Id @Column(name = "DOT_IDTAG_C", length = 36) private String tagId; From 86cae53789140f688e2ae7cb5c869c09677083de Mon Sep 17 00:00:00 2001 From: jendib Date: Wed, 26 Aug 2015 22:11:39 +0200 Subject: [PATCH 09/10] Closes #20: Clean error message if document or file does not exist --- .../docs/rest/resource/AuditLogResource.java | 3 ++- .../docs/rest/resource/DocumentResource.java | 7 ++++--- .../docs/rest/resource/FileResource.java | 20 ++++++++----------- .../src/app/docs/controller/DocumentView.js | 2 ++ .../webapp/src/app/docs/directive/ImgError.js | 16 +++++++++++++++ docs-web/src/main/webapp/src/index.html | 1 + .../src/partial/docs/document.view.html | 9 ++++++++- .../webapp/src/partial/docs/file.view.html | 9 ++++++++- .../docs/rest/TestDocumentResource.java | 3 +-- 9 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 docs-web/src/main/webapp/src/app/docs/directive/ImgError.js diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java index 3b431a95..b87c3b7e 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java @@ -9,6 +9,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; @@ -55,7 +56,7 @@ public class AuditLogResource extends BaseResource { // Check ACL on the document AclDao aclDao = new AclDao(); if (!aclDao.checkPermission(documentId, PermType.READ, principal.getId())) { - throw new ForbiddenClientException(); + return Response.status(Status.NOT_FOUND).build(); } criteria.setDocumentId(documentId); } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index c276e44a..a0c8ba44 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -20,6 +20,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import org.apache.commons.lang.StringUtils; import org.codehaus.jettison.json.JSONException; @@ -92,7 +93,7 @@ public class DocumentResource extends BaseResource { throw new ForbiddenClientException(); } } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + return Response.status(Status.NOT_FOUND).build(); } JSONObject document = new JSONObject(); @@ -430,7 +431,7 @@ public class DocumentResource extends BaseResource { try { document = documentDao.getDocument(id, PermType.WRITE, principal.getId()); } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", id)); + return Response.status(Status.NOT_FOUND).build(); } // Update the document @@ -514,7 +515,7 @@ public class DocumentResource extends BaseResource { document = documentDao.getDocument(id, PermType.WRITE, principal.getId()); fileList = fileDao.getByDocumentId(principal.getId(), id); } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", id)); + return Response.status(Status.NOT_FOUND).build(); } // Delete the document diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index f86383f4..5e1a65f8 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -102,7 +102,7 @@ public class FileResource extends BaseResource { try { document = documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + return Response.status(Status.NOT_FOUND).build(); } } @@ -199,7 +199,7 @@ public class FileResource extends BaseResource { file = fileDao.getFile(id, principal.getId()); document = documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + return Response.status(Status.NOT_FOUND).build(); } // Check that the file is orphan @@ -259,7 +259,7 @@ public class FileResource extends BaseResource { try { documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + return Response.status(Status.NOT_FOUND).build(); } // Reorder files @@ -295,13 +295,9 @@ public class FileResource extends BaseResource { // Check document visibility if (documentId != null) { - try { - AclDao aclDao = new AclDao(); - if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { - throw new ForbiddenClientException(); - } - } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { + return Response.status(Status.NOT_FOUND).build(); } } else if (!authenticated) { throw new ForbiddenClientException(); @@ -358,7 +354,7 @@ public class FileResource extends BaseResource { documentDao.getDocument(file.getDocumentId(), PermType.WRITE, principal.getId()); } } catch (NoResultException e) { - throw new ClientException("FileNotFound", MessageFormat.format("File not found: {0}", id)); + return Response.status(Status.NOT_FOUND).build(); } // Delete the file @@ -498,7 +494,7 @@ public class FileResource extends BaseResource { throw new ForbiddenClientException(); } } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + return Response.status(Status.NOT_FOUND).build(); } // Get files and user associated with this document diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js b/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js index fe80252a..8962c884 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js @@ -7,6 +7,8 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta // Load document data from server Restangular.one('document', $stateParams.id).get().then(function(data) { $scope.document = data; + }, function(response) { + $scope.error = response; }); // Load audit log data from server diff --git a/docs-web/src/main/webapp/src/app/docs/directive/ImgError.js b/docs-web/src/main/webapp/src/app/docs/directive/ImgError.js new file mode 100644 index 00000000..78772edd --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/directive/ImgError.js @@ -0,0 +1,16 @@ +'use strict'; + +/** + * Image error event directive. + */ +angular.module('docs').directive('imgError', function() { + return { + restrict: 'A', + link: function(scope, element, attrs) { + element.bind('error', function() { + //call the function that was passed + scope.$apply(attrs.imgError); + }); + } + }; +}) \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index ce5a6e97..6247f46b 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -64,6 +64,7 @@ + diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 43988090..2643b426 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -1,4 +1,11 @@ - + + +
    +

    + + Document not found +

    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/file.view.html b/docs-web/src/main/webapp/src/partial/docs/file.view.html index b0ecd077..8bf1cdb1 100644 --- a/docs-web/src/main/webapp/src/partial/docs/file.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/file.view.html @@ -22,5 +22,12 @@
    - + +

    + + File not found +

    diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index 88ebe3b1..3e818114 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -282,8 +282,7 @@ public class TestDocumentResource extends BaseJerseyTest { documentResource = resource().path("/document/" + document1Id); documentResource.addFilter(new CookieAuthenticationFilter(document1Token)); response = documentResource.get(ClientResponse.class); - json = response.getEntity(JSONObject.class); - Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus())); + Assert.assertEquals(Status.NOT_FOUND, Status.fromStatusCode(response.getStatus())); } /** From 08e4f6ddae477faea9338acd886a7989b9fee013 Mon Sep 17 00:00:00 2001 From: jendib Date: Fri, 28 Aug 2015 01:02:33 +0200 Subject: [PATCH 10/10] Closes #14: Soft delete on DocumentTag + audit log ordering --- .../com/sismics/docs/core/dao/jpa/TagDao.java | 65 ++++++++++++------- .../docs/core/model/jpa/DocumentTag.java | 58 +++++++---------- .../resources/db/update/dbupdate-010-0.sql | 1 + .../docs/rest/resource/AuditLogResource.java | 2 +- .../sismics/docs/rest/TestTagResource.java | 53 ++++++++++++++- 5 files changed, 119 insertions(+), 60 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java index 8345de17..da108dd9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java @@ -55,31 +55,50 @@ public class TagDao { /** * Update tags on a document. * - * @param documentId - * @param tagIdSet + * @param documentId Document ID + * @param tagIdSet Set of tag ID */ public void updateTagList(String documentId, Set tagIdSet) { - // Delete old tag links EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("delete DocumentTag dt where dt.documentId = :documentId"); - q.setParameter("documentId", documentId); - q.executeUpdate(); - // Create new tag links - for (String tagId : tagIdSet) { - DocumentTag documentTag = new DocumentTag(); - documentTag.setId(UUID.randomUUID().toString()); - documentTag.setDocumentId(documentId); - documentTag.setTagId(tagId); - em.persist(documentTag); + // Get current tag links + Query q = em.createQuery("select dt from DocumentTag dt where dt.documentId = :documentId and dt.deleteDate is null"); + q.setParameter("documentId", documentId); + @SuppressWarnings("unchecked") + List documentTagList = q.getResultList(); + + // Deleting tags no longer linked + for (DocumentTag documentTag : documentTagList) { + if (!tagIdSet.contains(documentTag.getTagId())) { + documentTag.setDeleteDate(new Date()); + } + } + + // Adding new tag links + for (String tagId : tagIdSet) { + boolean found = false; + for (DocumentTag documentTag : documentTagList) { + if (documentTag.getTagId().equals(tagId)) { + found = true; + break; + } + } + + if (!found) { + DocumentTag documentTag = new DocumentTag(); + documentTag.setId(UUID.randomUUID().toString()); + documentTag.setDocumentId(documentId); + documentTag.setTagId(tagId); + em.persist(documentTag); + } } - } /** * Returns tag list on a document. - * @param documentId - * @return + * + * @param documentId Document ID + * @return List of tags */ @SuppressWarnings("unchecked") public List getByDocumentId(String documentId, String userId) { @@ -87,7 +106,7 @@ public class TagDao { StringBuilder sb = new StringBuilder("select t.TAG_ID_C, t.TAG_NAME_C, t.TAG_COLOR_C from T_DOCUMENT_TAG dt "); sb.append(" join T_TAG t on t.TAG_ID_C = dt.DOT_IDTAG_C "); sb.append(" where dt.DOT_IDDOCUMENT_C = :documentId and t.TAG_DELETEDATE_D is null "); - sb.append(" and t.TAG_IDUSER_C = :userId "); + sb.append(" and t.TAG_IDUSER_C = :userId and dt.DOT_DELETEDATE_D is null "); sb.append(" order by t.TAG_NAME_C "); // Perform the query @@ -111,15 +130,16 @@ public class TagDao { /** * Returns stats on tags. - * @param documentId - * @return + * + * @param documentId Document ID + * @return Stats by tag */ @SuppressWarnings("unchecked") public List getStats(String userId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); StringBuilder sb = new StringBuilder("select t.TAG_ID_C, t.TAG_NAME_C, t.TAG_COLOR_C, count(d.DOC_ID_C) "); sb.append(" from T_TAG t "); - sb.append(" left join T_DOCUMENT_TAG dt on t.TAG_ID_C = dt.DOT_IDTAG_C "); + sb.append(" left join T_DOCUMENT_TAG dt on t.TAG_ID_C = dt.DOT_IDTAG_C and dt.DOT_DELETEDATE_D is null "); sb.append(" left join T_DOCUMENT d on d.DOC_ID_C = dt.DOT_IDDOCUMENT_C and d.DOC_DELETEDATE_D is null and d.DOC_IDUSER_C = :userId "); sb.append(" where t.TAG_IDUSER_C = :userId and t.TAG_DELETEDATE_D is null "); sb.append(" group by t.TAG_ID_C "); @@ -168,6 +188,7 @@ public class TagDao { /** * Returns a tag by name. + * * @param userId User ID * @param name Name * @return Tag @@ -186,6 +207,7 @@ public class TagDao { /** * Returns a tag by ID. + * * @param userId User ID * @param tagId Tag ID * @return Tag @@ -216,8 +238,7 @@ public class TagDao { Tag tagDb = (Tag) q.getSingleResult(); // Delete the tag - Date dateNow = new Date(); - tagDb.setDeleteDate(dateNow); + tagDb.setDeleteDate(new Date()); // Delete linked data q = em.createQuery("delete DocumentTag dt where dt.tagId = :tagId"); diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java index 53d0b770..e38963bb 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java @@ -6,7 +6,9 @@ import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; + import java.io.Serializable; +import java.util.Date; /** * Link between a document and a tag. @@ -40,6 +42,12 @@ public class DocumentTag implements Serializable { @Column(name = "DOT_IDTAG_C", length = 36) private String tagId; + /** + * Deletion date. + */ + @Column(name = "DOT_DELETEDATE_D") + private Date deleteDate; + /** * Getter of id. * @@ -94,44 +102,24 @@ public class DocumentTag implements Serializable { this.tagId = tagId; } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((documentId == null) ? 0 : documentId.hashCode()); - result = prime * result + ((tagId == null) ? 0 : tagId.hashCode()); - return result; + /** + * Getter of deleteDate. + * + * @return the deleteDate + */ + public Date getDeleteDate() { + return deleteDate; } - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - DocumentTag other = (DocumentTag) obj; - if (documentId == null) { - if (other.documentId != null) { - return false; - } - } else if (!documentId.equals(other.documentId)) { - return false; - } - if (tagId == null) { - if (other.tagId != null) { - return false; - } - } else if (!tagId.equals(other.tagId)) { - return false; - } - return true; + /** + * Setter of deleteDate. + * + * @param deleteDate deleteDate + */ + public void setDeleteDate(Date deleteDate) { + this.deleteDate = deleteDate; } - + @Override public String toString() { return Objects.toStringHelper(this) diff --git a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql index a3ecc9c4..3f0127d2 100644 --- a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql +++ b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql @@ -3,4 +3,5 @@ alter table T_AUTHENTICATION_TOKEN add column AUT_IP_C varchar(45); alter table T_AUTHENTICATION_TOKEN add column AUT_UA_C varchar(1000); create cached table T_AUDIT_LOG ( LOG_ID_C varchar(36) not null, LOG_IDENTITY_C varchar(36) not null, LOG_CLASSENTITY_C varchar(50) not null, LOG_TYPE_C varchar(50) not null, LOG_MESSAGE_C varchar(1000), LOG_CREATEDATE_D datetime, primary key (LOG_ID_C) ); create index IDX_LOG_COMPOSITE on T_AUDIT_LOG (LOG_IDENTITY_C, LOG_CLASSENTITY_C); +alter table T_DOCUMENT_TAG add column DOT_DELETEDATE_D datetime; update T_CONFIG set CFG_VALUE_C='10' where CFG_ID_C='DB_VERSION'; \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java index b87c3b7e..2081eb2a 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java @@ -47,7 +47,7 @@ public class AuditLogResource extends BaseResource { // On a document or a user? PaginatedList paginatedList = PaginatedLists.create(20, 0); - SortCriteria sortCriteria = new SortCriteria(1, true); + SortCriteria sortCriteria = new SortCriteria(1, false); AuditLogCriteria criteria = new AuditLogCriteria(); if (documentId == null) { // Search logs for a user diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java index cd8d1ecf..bacedd6e 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java @@ -75,12 +75,61 @@ public class TestTagResource extends BaseJerseyTest { documentResource = resource().path("/document"); documentResource.addFilter(new CookieAuthenticationFilter(tag1Token)); postParams = new MultivaluedMapImpl(); - postParams.add("title", "My super document 1"); + postParams.add("title", "My super document 2"); postParams.add("tags", tag4Id); postParams.add("language", "eng"); response = documentResource.put(ClientResponse.class, postParams); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); json = response.getEntity(JSONObject.class); + String document2Id = json.getString("id"); + + // Check tags on a document + documentResource = resource().path("/document/" + document2Id); + documentResource.addFilter(new CookieAuthenticationFilter(tag1Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + JSONArray tags = json.getJSONArray("tags"); + Assert.assertEquals(1, tags.length()); + Assert.assertEquals(tag4Id, tags.getJSONObject(0).getString("id")); + + // Update tags on a document + documentResource = resource().path("/document/" + document2Id); + documentResource.addFilter(new CookieAuthenticationFilter(tag1Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("tags", tag3Id); + postParams.add("tags", tag4Id); + response = documentResource.post(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + + // Check tags on a document + documentResource = resource().path("/document/" + document2Id); + documentResource.addFilter(new CookieAuthenticationFilter(tag1Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + tags = json.getJSONArray("tags"); + Assert.assertEquals(2, tags.length()); + Assert.assertEquals(tag3Id, tags.getJSONObject(0).getString("id")); + Assert.assertEquals(tag4Id, tags.getJSONObject(1).getString("id")); + + // Update tags on a document + documentResource = resource().path("/document/" + document2Id); + documentResource.addFilter(new CookieAuthenticationFilter(tag1Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("tags", tag4Id); + response = documentResource.post(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + + // Check tags on a document + documentResource = resource().path("/document/" + document2Id); + documentResource.addFilter(new CookieAuthenticationFilter(tag1Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + tags = json.getJSONArray("tags"); + Assert.assertEquals(1, tags.length()); + Assert.assertEquals(tag4Id, tags.getJSONObject(0).getString("id")); // Get tag stats tagResource = resource().path("/tag/stats"); @@ -99,7 +148,7 @@ public class TestTagResource extends BaseJerseyTest { response = tagResource.get(ClientResponse.class); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); json = response.getEntity(JSONObject.class); - JSONArray tags = json.getJSONArray("tags"); + tags = json.getJSONArray("tags"); Assert.assertTrue(tags.length() > 0); Assert.assertEquals("Tag4", tags.getJSONObject(1).getString("name")); Assert.assertEquals("#00ff00", tags.getJSONObject(1).getString("color"));