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

39 Commits
v1.0 ... v1.1

Author SHA1 Message Date
Ben
60beb0e2f5 Init Android project 2013-12-27 13:27:58 +01:00
jendib
77f0368ba5 Hardwire TIFFImageReaderSpi to avoid registering bug (again) 2013-11-07 01:12:11 +01:00
jendib
596dd4db13 Merge branch 'master' of https://github.com/sismics/docs.git 2013-10-01 22:24:23 +02:00
jendib
d2ba291287 Change package for tess4j 2013-10-01 22:24:12 +02:00
Benjamin Gamard
01cb3e611c Update README.md 2013-09-23 13:36:58 +02:00
jendib
bd9e918a62 Merge branch 'master' of https://github.com/sismics/docs.git 2013-09-07 00:27:22 +02:00
jendib
56f6038c09 Vertical scroll on file preview for small screens 2013-09-07 00:27:13 +02:00
Benjamin Gamard
f29ed3e671 Update README.md 2013-09-06 11:18:30 +02:00
jendib
726121d8c8 Fix Hibernate entity 2013-09-05 21:43:45 +02:00
jendib
c40e7e1cc9 File upload feedback in title and form state, new favicon 2013-09-05 19:14:21 +02:00
jendib
b1f9b072f3 TODO 2013-09-05 17:41:33 +02:00
jendib
22cea20a90 Hard coupling between tess4j and imageIO to avoid service registering 2013-09-05 16:10:26 +02:00
jendib
8eb5b8066e Merge branch 'master' of https://github.com/sismics/docs.git 2013-09-05 11:35:00 +02:00
jendib
e37eab3b7a All users can access logs 2013-09-05 11:34:53 +02:00
jendib
1db54174d4 Remove TODO 2013-09-05 00:50:23 +02:00
jendib
d5fa3a4e3a TODO 2013-09-04 18:04:31 +02:00
jendib
fc53758eb7 Typo, TODO 2013-09-03 18:03:44 +02:00
jendib
f5079e83cb Typo 2013-09-03 09:19:47 +02:00
jendib
b399e4081f Typo 2013-09-02 23:28:38 +02:00
jendib
a80bc27582 TODO 2013-09-02 22:25:58 +02:00
jendib
eb57af4029 Fix multi-platform jUnit 2013-08-26 22:35:30 +02:00
jendib
ac3580fb4a Fix leak 2013-08-26 22:03:14 +02:00
jendib
6e3d2ea972 Show file count on documents list (client) 2013-08-22 22:24:14 +02:00
jendib
870a44da0d Return file count on GET /document/list 2013-08-22 17:59:24 +02:00
jendib
62a5840777 Alert in navigation bar if there is a new error in logs 2013-08-22 17:41:47 +02:00
jendib
a858699391 Git ignore 2013-08-22 14:44:58 +02:00
jendib
fb0aec5fee Fix document scope reset after add, fix wrong web.xml configuration
(it broke TIFF image encoding on OCR)
2013-08-22 01:44:51 +02:00
jendib
1a495ac948 Fix jUnit on PDF rendering 2013-08-21 01:30:37 +02:00
jendib
ba083fb57a Disable verbose logging 2013-08-21 01:15:52 +02:00
jendib
4e22111f38 Fix default thumbnail 2013-08-21 01:05:59 +02:00
jendib
db7a9f0e4a Encrypt stored files in SHA 256 2013-08-20 21:51:07 +02:00
jendib
906de329ae DB update script 6 2013-08-20 18:55:49 +02:00
jendib
00b00f0d0c File encryption utilities 2013-08-20 18:06:08 +02:00
jendib
0bc658a396 More loading feedback (client) 2013-08-20 00:57:22 +02:00
jendib
464d43194b File encryption (in progress) 2013-08-19 23:57:50 +02:00
jendib
1c606ebf25 Delete files when a document is deleted, fix search by date 2013-08-19 15:27:34 +02:00
jendib
504c4dd815 Merge branch 'master' of https://github.com/sismics/docs.git 2013-08-19 01:30:27 +02:00
jendib
ecb5a7abf6 TODO 2013-08-19 01:29:59 +02:00
Benjamin Gamard
cc6d599293 Update README.md 2013-08-18 16:31:41 +02:00
93 changed files with 3008 additions and 623 deletions

View File

@@ -14,10 +14,11 @@ Features
--------
- Responsive user interface
- Optical characted recognition
- Optical character recognition
- Support image and PDF files
- Flexible search engine
- Full text search in image and PDF
- SHA-256 encryption
- Tag system
- Multi-users
- Document sharing
@@ -26,13 +27,13 @@ Features
License
-------
Reader is released under the terms of the GPL license. See `COPYING` for more
Docs is released under the terms of the GPL license. See `COPYING` for more
information or see <http://opensource.org/licenses/GPL-2.0>.
How to build Docs from the sources
------------------------------------
----------------------------------
Prerequisites: JDK 7, Maven 3
Prerequisites: JDK 7, Maven 3, Tesseract 3.02
Docs is organized in several Maven modules:
@@ -48,6 +49,7 @@ or download the sources from GitHub.
From the `docs-parent` directory:
mvn -Pinit validate -N
mvn clean -DskipTests install
#### Run a stand-alone version

4
docs-android/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.gradle
/local.properties
/.idea
.DS_Store

1
docs-android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

76
docs-android/app/app.iml Normal file
View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugJava" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/source/r/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/aidl/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/rs/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/source/r/test/debug" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/aidl/test/debug" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/test/debug" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/rs/test/debug" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/res/rs/test/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/assets" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/resources" type="java-test-resource" />
<excludeFolder url="file://$MODULE_DIR$/build/apk" />
<excludeFolder url="file://$MODULE_DIR$/build/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/res" />
<excludeFolder url="file://$MODULE_DIR$/build/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 19 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="support-v4-18.0.0" level="project" />
</component>
</module>

View File

@@ -0,0 +1,40 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.7.+'
}
}
apply plugin: 'android'
repositories {
mavenCentral()
}
android {
compileSdkVersion 19
buildToolsVersion "19.0.0"
defaultConfig {
minSdkVersion 14
targetSdkVersion 19
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}
dependencies {
compile 'com.android.support:support-v4:18.0.0'
}

View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /opt/android-studio/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sismics.docs" >
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.sismics.docs.DocListActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.sismics.docs.DocDetailActivity"
android:label="@string/title_doc_detail"
android:parentActivityName="com.sismics.docs.DocListActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.sismics.docs.DocListActivity" />
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,67 @@
package com.sismics.docs;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
/**
* An activity representing a single Doc detail screen. This
* activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items
* in a {@link DocListActivity}.
* <p>
* This activity is mostly just a 'shell' activity containing nothing
* more than a {@link DocDetailFragment}.
*/
public class DocDetailActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_doc_detail);
// Show the Up button in the action bar.
getActionBar().setDisplayHomeAsUpEnabled(true);
// savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity
// (e.g. when rotating the screen from portrait to landscape).
// In this case, the fragment will automatically be re-added
// to its container so we don't need to manually add it.
// For more information, see the Fragments API guide at:
//
// http://developer.android.com/guide/components/fragments.html
//
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
Bundle arguments = new Bundle();
arguments.putString(DocDetailFragment.ARG_ITEM_ID,
getIntent().getStringExtra(DocDetailFragment.ARG_ITEM_ID));
DocDetailFragment fragment = new DocDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.add(R.id.doc_detail_container, fragment)
.commit();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
// This ID represents the Home or Up button. In the case of this
// activity, the Up button is shown. Use NavUtils to allow users
// to navigate up one level in the application structure. For
// more details, see the Navigation pattern on Android Design:
//
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
//
NavUtils.navigateUpTo(this, new Intent(this, DocListActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,61 @@
package com.sismics.docs;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.sismics.docs.dummy.DummyContent;
/**
* A fragment representing a single Doc detail screen.
* This fragment is either contained in a {@link DocListActivity}
* in two-pane mode (on tablets) or a {@link DocDetailActivity}
* on handsets.
*/
public class DocDetailFragment extends Fragment {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
public static final String ARG_ITEM_ID = "item_id";
/**
* The dummy content this fragment is presenting.
*/
private DummyContent.DummyItem mItem;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public DocDetailFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments().containsKey(ARG_ITEM_ID)) {
// Load the dummy content specified by the fragment
// arguments. In a real-world scenario, use a Loader
// to load content from a content provider.
mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID));
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_doc_detail, container, false);
// Show the dummy content as text in a TextView.
if (mItem != null) {
((TextView) rootView.findViewById(R.id.doc_detail)).setText(mItem.content);
}
return rootView;
}
}

View File

@@ -0,0 +1,81 @@
package com.sismics.docs;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
/**
* An activity representing a list of Docs. This activity
* has different presentations for handset and tablet-size devices. On
* handsets, the activity presents a list of items, which when touched,
* lead to a {@link DocDetailActivity} representing
* item details. On tablets, the activity presents the list of items and
* item details side-by-side using two vertical panes.
* <p>
* The activity makes heavy use of fragments. The list of items is a
* {@link DocListFragment} and the item details
* (if present) is a {@link DocDetailFragment}.
* <p>
* This activity also implements the required
* {@link DocListFragment.Callbacks} interface
* to listen for item selections.
*/
public class DocListActivity extends FragmentActivity
implements DocListFragment.Callbacks {
/**
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
* device.
*/
private boolean mTwoPane;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_doc_list);
if (findViewById(R.id.doc_detail_container) != null) {
// The detail container view will be present only in the
// large-screen layouts (res/values-large and
// res/values-sw600dp). If this view is present, then the
// activity should be in two-pane mode.
mTwoPane = true;
// In two-pane mode, list items should be given the
// 'activated' state when touched.
((DocListFragment) getSupportFragmentManager()
.findFragmentById(R.id.doc_list))
.setActivateOnItemClick(true);
}
// TODO: If exposing deep links into your app, handle intents here.
}
/**
* Callback method from {@link DocListFragment.Callbacks}
* indicating that the item with the given ID was selected.
*/
@Override
public void onItemSelected(String id) {
if (mTwoPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
Bundle arguments = new Bundle();
arguments.putString(DocDetailFragment.ARG_ITEM_ID, id);
DocDetailFragment fragment = new DocDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.doc_detail_container, fragment)
.commit();
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent = new Intent(this, DocDetailActivity.class);
detailIntent.putExtra(DocDetailFragment.ARG_ITEM_ID, id);
startActivity(detailIntent);
}
}
}

View File

@@ -0,0 +1,151 @@
package com.sismics.docs;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.sismics.docs.dummy.DummyContent;
/**
* A list fragment representing a list of Docs. This fragment
* also supports tablet devices by allowing list items to be given an
* 'activated' state upon selection. This helps indicate which item is
* currently being viewed in a {@link DocDetailFragment}.
* <p>
* Activities containing this fragment MUST implement the {@link Callbacks}
* interface.
*/
public class DocListFragment extends ListFragment {
/**
* The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets.
*/
private static final String STATE_ACTIVATED_POSITION = "activated_position";
/**
* The fragment's current callback object, which is notified of list item
* clicks.
*/
private Callbacks mCallbacks = sDummyCallbacks;
/**
* The current activated item position. Only used on tablets.
*/
private int mActivatedPosition = ListView.INVALID_POSITION;
/**
* A callback interface that all activities containing this fragment must
* implement. This mechanism allows activities to be notified of item
* selections.
*/
public interface Callbacks {
/**
* Callback for when an item has been selected.
*/
public void onItemSelected(String id);
}
/**
* A dummy implementation of the {@link Callbacks} interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
private static Callbacks sDummyCallbacks = new Callbacks() {
@Override
public void onItemSelected(String id) {
}
};
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public DocListFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TODO: replace with a real list adapter.
setListAdapter(new ArrayAdapter<DummyContent.DummyItem>(
getActivity(),
android.R.layout.simple_list_item_activated_1,
android.R.id.text1,
DummyContent.ITEMS));
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Restore the previously serialized activated item position.
if (savedInstanceState != null
&& savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Activities containing this fragment must implement its callbacks.
if (!(activity instanceof Callbacks)) {
throw new IllegalStateException("Activity must implement fragment's callbacks.");
}
mCallbacks = (Callbacks) activity;
}
@Override
public void onDetach() {
super.onDetach();
// Reset the active callbacks interface to the dummy implementation.
mCallbacks = sDummyCallbacks;
}
@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
super.onListItemClick(listView, view, position, id);
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mActivatedPosition != ListView.INVALID_POSITION) {
// Serialize and persist the activated item position.
outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
}
}
/**
* Turns on activate-on-click mode. When this mode is on, list items will be
* given the 'activated' state when touched.
*/
public void setActivateOnItemClick(boolean activateOnItemClick) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
getListView().setChoiceMode(activateOnItemClick
? ListView.CHOICE_MODE_SINGLE
: ListView.CHOICE_MODE_NONE);
}
private void setActivatedPosition(int position) {
if (position == ListView.INVALID_POSITION) {
getListView().setItemChecked(mActivatedPosition, false);
} else {
getListView().setItemChecked(position, true);
}
mActivatedPosition = position;
}
}

View File

@@ -0,0 +1,55 @@
package com.sismics.docs.dummy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helper class for providing sample content for user interfaces created by
* Android template wizards.
* <p>
* TODO: Replace all uses of this class before publishing your app.
*/
public class DummyContent {
/**
* An array of sample (dummy) items.
*/
public static List<DummyItem> ITEMS = new ArrayList<DummyItem>();
/**
* A map of sample (dummy) items, by ID.
*/
public static Map<String, DummyItem> ITEM_MAP = new HashMap<String, DummyItem>();
static {
// Add 3 sample items.
addItem(new DummyItem("1", "Item 1"));
addItem(new DummyItem("2", "Item 2"));
addItem(new DummyItem("3", "Item 3"));
}
private static void addItem(DummyItem item) {
ITEMS.add(item);
ITEM_MAP.put(item.id, item);
}
/**
* A dummy item representing a piece of content.
*/
public static class DummyItem {
public String id;
public String content;
public DummyItem(String id, String content) {
this.id = id;
this.content = content;
}
@Override
public String toString() {
return content;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/doc_detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.sismics.docs.DocDetailActivity"
tools:ignore="MergeRootFrame" />

View File

@@ -0,0 +1,10 @@
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/doc_list"
android:name="com.sismics.docs.DocListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
tools:context="com.sismics.docs.DocListActivity"
tools:layout="@android:layout/list_content" />

View File

@@ -0,0 +1,38 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:baselineAligned="false"
android:divider="?android:attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
tools:context="com.sismics.docs.DocListActivity">
<!--
This layout is a two-pane layout for the Docs
master/detail flow. See res/values-large/refs.xml and
res/values-sw600dp/refs.xml for an example of layout aliases
that replace the single-pane version of the layout with
this two-pane version.
For more on layout aliases, see:
http://developer.android.com/training/multiscreen/screensizes.html#TaskUseAliasFilters
-->
<fragment
android:id="@+id/doc_list"
android:name="com.sismics.docs.DocListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
tools:layout="@android:layout/list_content" />
<FrameLayout
android:id="@+id/doc_detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/doc_detail"
style="?android:attr/textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:textIsSelectable="true"
tools:context="com.sismics.docs.DocDetailFragment" />

View File

@@ -0,0 +1,10 @@
<resources>
<!--
Layout alias to replace the single-pane version of the layout with a
two-pane version on Large screens.
For more on layout aliases, see:
http://developer.android.com/training/multiscreen/screensizes.html#TaskUseAliasFilters
-->
<item type="layout" name="activity_doc_list">@layout/activity_doc_twopane</item>
</resources>

View File

@@ -0,0 +1,10 @@
<resources>
<!--
Layout alias to replace the single-pane version of the layout with a
two-pane version on Large screens.
For more on layout aliases, see:
http://developer.android.com/training/multiscreen/screensizes.html#TaskUseAliasFilters
-->
<item type="layout" name="activity_doc_list">@layout/activity_doc_twopane</item>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Sismics Docs</string>
<string name="title_doc_detail">Doc Detail</string>
</resources>

View File

@@ -0,0 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Holo.Light">
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.

View File

@@ -0,0 +1,18 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Settings specified in this file will override any Gradle settings
# configured through the IDE.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.9-all.zip

164
docs-android/gradlew vendored Normal file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
docs-android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
include ':app'

View File

@@ -122,6 +122,11 @@
<artifactId>pdfbox</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<!-- OCR dependencies -->
<dependency>
<groupId>jna</groupId>
@@ -133,11 +138,6 @@
<artifactId>imageio</artifactId>
</dependency>
<dependency>
<groupId>tess4j</groupId>
<artifactId>tess4j</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>

View File

@@ -138,14 +138,15 @@ public class DocumentDao {
*
* @param paginatedList List of documents (updated by side effects)
* @param criteria Search criteria
* @return List of document
* @return List of documents
* @throws Exception
*/
public void findByCriteria(PaginatedList<DocumentDto> paginatedList, DocumentCriteria criteria, SortCriteria sortCriteria) throws Exception {
Map<String, Object> parameterMap = new HashMap<String, Object>();
List<String> criteriaList = new ArrayList<String>();
StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C c0, d.DOC_TITLE_C c1, d.DOC_DESCRIPTION_C c2, d.DOC_CREATEDATE_D c3, d.DOC_LANGUAGE_C c4, s.SHA_ID_C is not null c5 ");
StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C c0, d.DOC_TITLE_C c1, d.DOC_DESCRIPTION_C c2, d.DOC_CREATEDATE_D c3, d.DOC_LANGUAGE_C c4, s.SHA_ID_C is not null c5, ");
sb.append(" (select count(f.FIL_ID_C) from T_FILE f where f.FIL_DELETEDATE_D is null and f.FIL_IDDOC_C = d.DOC_ID_C) c6 ");
sb.append(" from T_DOCUMENT d ");
sb.append(" left join T_SHARE s on s.SHA_IDDOCUMENT_C = d.DOC_ID_C and s.SHA_DELETEDATE_D is null ");
@@ -211,6 +212,7 @@ public class DocumentDao {
documentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime());
documentDto.setLanguage((String) o[i++]);
documentDto.setShared((Boolean) o[i++]);
documentDto.setFileCount(((Number) o[i++]).intValue());
documentDtoList.add(documentDto);
}

View File

@@ -39,6 +39,11 @@ public class DocumentDto {
*/
private Boolean shared;
/**
* File count.
*/
private Integer fileCount;
/**
* Getter de id.
*
@@ -146,4 +151,20 @@ public class DocumentDto {
public void setLanguage(String language) {
this.language = language;
}
/**
* Getter of fileCount.
* @return fileCount
*/
public Integer getFileCount() {
return fileCount;
}
/**
* Setter of fileCount.
* @param fileCount fileCount
*/
public void setFileCount(Integer fileCount) {
this.fileCount = fileCount;
}
}

View File

@@ -10,6 +10,7 @@ import java.util.Set;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.TermsFilter;
@@ -171,12 +172,17 @@ public class LuceneDao {
TermsFilter userFilter = new TermsFilter(terms);
// Search
IndexSearcher searcher = new IndexSearcher(AppContext.getInstance().getIndexingService().getDirectoryReader());
DirectoryReader directoryReader = AppContext.getInstance().getIndexingService().getDirectoryReader();
Set<String> documentIdList = new HashSet<String>();
if (directoryReader == null) {
// The directory reader is not yet initialized (probably because there is nothing indexed)
return documentIdList;
}
IndexSearcher searcher = new IndexSearcher(directoryReader);
TopDocs topDocs = searcher.search(query, userFilter, Integer.MAX_VALUE);
ScoreDoc[] docs = topDocs.scoreDocs;
// Extract document IDs
Set<String> documentIdList = new HashSet<String>();
for (int i = 0; i < docs.length; i++) {
org.apache.lucene.document.Document document = searcher.doc(docs[i].doc);
String type = document.get("type");

View File

@@ -1,16 +0,0 @@
package com.sismics.docs.core.event;
import com.google.common.base.Objects;
/**
* Extract file content event.
*
* @author bgamard
*/
public class ExtractFileAsyncEvent {
@Override
public String toString() {
return Objects.toStringHelper(this)
.toString();
}
}

View File

@@ -1,5 +1,7 @@
package com.sismics.docs.core.event;
import java.io.InputStream;
import com.google.common.base.Objects;
import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.File;
@@ -20,6 +22,11 @@ public class FileCreatedAsyncEvent {
*/
private Document document;
/**
* Unencrypted input stream containing the file.
*/
private InputStream inputStream;
/**
* Getter of file.
*
@@ -56,6 +63,24 @@ public class FileCreatedAsyncEvent {
this.document = document;
}
/**
* Getter of inputStream.
*
* @return the inputStream
*/
public InputStream getInputStream() {
return inputStream;
}
/**
* Setter de inputStream.
*
* @param inputStream inputStream
*/
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public String toString() {
return Objects.toStringHelper(this)

View File

@@ -1,57 +0,0 @@
package com.sismics.docs.core.listener.async;
import java.text.MessageFormat;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.sismics.docs.core.dao.jpa.DocumentDao;
import com.sismics.docs.core.dao.jpa.FileDao;
import com.sismics.docs.core.event.ExtractFileAsyncEvent;
import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.FileUtil;
import com.sismics.docs.core.util.TransactionUtil;
/**
* Listener on extract content from all files.
*
* @author bgamard
*/
public class ExtractFileAsyncListener {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(ExtractFileAsyncListener.class);
/**
* Extract content from all files.
*
* @param extractFileAsyncEvent Extract file content event
* @throws Exception
*/
@Subscribe
public void on(final ExtractFileAsyncEvent extractFileAsyncEvent) throws Exception {
if (log.isInfoEnabled()) {
log.info("Extract file content event: " + extractFileAsyncEvent.toString());
}
TransactionUtil.handle(new Runnable() {
@Override
public void run() {
FileDao fileDao = new FileDao();
DocumentDao documentDao = new DocumentDao();
List<File> fileList = fileDao.findAll();
for (File file : fileList) {
long startTime = System.currentTimeMillis();
Document document = documentDao.getById(file.getDocumentId());
file.setContent(FileUtil.extractContent(document, file));
TransactionUtil.commit();
log.info(MessageFormat.format("File content extracted in {0}ms", System.currentTimeMillis() - startTime));
}
}
});
}
}

View File

@@ -39,7 +39,7 @@ public class FileCreatedAsyncListener {
// OCR the file
final File file = fileCreatedAsyncEvent.getFile();
long startTime = System.currentTimeMillis();
final String content = FileUtil.extractContent(fileCreatedAsyncEvent.getDocument(), file);
final String content = FileUtil.extractContent(fileCreatedAsyncEvent.getDocument(), file, fileCreatedAsyncEvent.getInputStream());
log.info(MessageFormat.format("File content extracted in {0}ms", System.currentTimeMillis() - startTime));
// Store the OCR-ization result in the database

View File

@@ -16,7 +16,6 @@ import com.sismics.docs.core.listener.async.DocumentDeletedAsyncListener;
import com.sismics.docs.core.listener.async.DocumentUpdatedAsyncListener;
import com.sismics.docs.core.listener.async.FileCreatedAsyncListener;
import com.sismics.docs.core.listener.async.FileDeletedAsyncListener;
import com.sismics.docs.core.listener.async.ExtractFileAsyncListener;
import com.sismics.docs.core.listener.async.RebuildIndexAsyncListener;
import com.sismics.docs.core.listener.sync.DeadEventListener;
import com.sismics.docs.core.model.jpa.Config;
@@ -82,7 +81,6 @@ public class AppContext {
asyncEventBus.register(new DocumentUpdatedAsyncListener());
asyncEventBus.register(new DocumentDeletedAsyncListener());
asyncEventBus.register(new RebuildIndexAsyncListener());
asyncEventBus.register(new ExtractFileAsyncListener());
}
/**

View File

@@ -50,7 +50,7 @@ public class Tag {
/**
* Tag name.
*/
@Column(name = "TAG_COLOR_C", nullable = false, length = 6)
@Column(name = "TAG_COLOR_C", nullable = false, length = 7)
private String color;
/**

View File

@@ -47,6 +47,12 @@ public class User {
@Column(name = "USE_PASSWORD_C", nullable = false, length = 100)
private String password;
/**
* User's private key.
*/
@Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100)
private String privateKey;
/**
* Email address.
*/
@@ -257,6 +263,22 @@ public class User {
this.deleteDate = deleteDate;
}
/**
* Getter de privateKey.
* @return privateKey
*/
public String getPrivateKey() {
return privateKey;
}
/**
* Setter de privateKey.
* @param privateKey privateKey
*/
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
@Override
public String toString() {
return Objects.toStringHelper(this)

View File

@@ -128,7 +128,6 @@ public class IndexingService extends AbstractScheduledService {
public DirectoryReader getDirectoryReader() {
if (directoryReader == null) {
if (!DirectoryReader.indexExists(directory)) {
log.info("Lucene directory not yet created");
return null;
}
try {

View File

@@ -0,0 +1,89 @@
package com.sismics.docs.core.util;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import com.google.common.base.Strings;
/**
* Encryption utilities.
*
* @author bgamard
*/
public class EncryptionUtil {
/**
* Salt.
*/
private static final String SALT = "LEpxZmm2SMu2PeKzPNrar2rhVAS6LrrgvXKeL9uyXC4vgKHg";
static {
// Initialize Bouncy Castle provider
Security.insertProviderAt(new BouncyCastleProvider(), 1);
}
/**
* Generate a private key.
*
* @return New random private key
* @throws NoSuchAlgorithmException
*/
public static String generatePrivateKey() throws NoSuchAlgorithmException {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
return new BigInteger(176, random).toString(32);
}
/**
* Decrypt an InputStream using the specified private key.
*
* @param is InputStream to encrypt
* @param privateKey Private key
* @return Encrypted stream
* @throws Exception
*/
public static InputStream decryptInputStream(InputStream is, String privateKey) throws Exception {
return new CipherInputStream(is, getCipher(privateKey, Cipher.DECRYPT_MODE));
}
/**
* Return an encryption cipher.
*
* @param privateKey Private key
* @return Encryption cipher
* @throws Exception
*/
public static Cipher getEncryptionCipher(String privateKey) throws Exception {
if (Strings.isNullOrEmpty(privateKey)) {
throw new IllegalArgumentException("The private key is null or empty");
}
return getCipher(privateKey, Cipher.ENCRYPT_MODE);
}
/**
* Initialize a Cipher.
*
* @param privateKey Private key
* @param mode Mode (encrypt or decrypt)
* @return Cipher
* @throws Exception
*/
private static Cipher getCipher(String privateKey, int mode) throws Exception {
PBEKeySpec keySpec = new PBEKeySpec(privateKey.toCharArray(), SALT.getBytes(), 2000, 256);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA256AND256BITAES-CBC-BC");
SecretKey desKey = skf.generateSecret(keySpec);
Cipher cipher = Cipher.getInstance("AES/CTR/NOPADDING");
cipher.init(mode, desKey);
return cipher;
}
}

View File

@@ -1,17 +1,20 @@
package com.sismics.docs.core.util;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.imageio.ImageIO;
import net.sourceforge.tess4j.Tesseract;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.util.PDFTextStripper;
@@ -23,6 +26,7 @@ import org.slf4j.LoggerFactory;
import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.tess4j.Tesseract;
import com.sismics.util.ImageUtil;
import com.sismics.util.mime.MimeType;
@@ -42,36 +46,36 @@ public class FileUtil {
*
* @param document Document linked to the file
* @param file File to extract
* @param inputStream Unencrypted input stream
* @return Content extract
*/
public static String extractContent(Document document, File file) {
public static String extractContent(Document document, File file, InputStream inputStream) {
String content = null;
if (ImageUtil.isImage(file.getMimeType())) {
content = ocrFile(document, file);
content = ocrFile(inputStream, document);
} else if (file.getMimeType().equals(MimeType.APPLICATION_PDF)) {
content = extractPdf(file);
content = extractPdf(inputStream);
}
return content;
}
/**
* Optical character recognition on a file.
* Optical character recognition on a stream.
*
* @param inputStream Unencrypted input stream
* @param document Document linked to the file
* @param file File to OCR
* @return Content extracted
*/
private static String ocrFile(Document document, File file) {
private static String ocrFile(InputStream inputStream, Document document) {
Tesseract instance = Tesseract.getInstance();
java.io.File storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId()).toFile();
String content = null;
BufferedImage image = null;
try {
image = ImageIO.read(storedfile);
image = ImageIO.read(inputStream);
} catch (IOException e) {
log.error("Error reading the image " + storedfile, e);
log.error("Error reading the image", e);
}
// Upscale and grayscale the image
@@ -85,7 +89,7 @@ public class FileUtil {
instance.setLanguage(document.getLanguage());
content = instance.doOCR(image);
} catch (Exception e) {
log.error("Error while OCR-izing the file " + storedfile, e);
log.error("Error while OCR-izing the image", e);
}
return content;
@@ -94,19 +98,18 @@ public class FileUtil {
/**
* Extract text from a PDF.
*
* @param file File to extract
* @param inputStream Unencrypted input stream
* @return Content extracted
*/
private static String extractPdf(File file) {
private static String extractPdf(InputStream inputStream) {
String content = null;
PDDocument pdfDocument = null;
java.io.File storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId()).toFile();
try {
PDFTextStripper stripper = new PDFTextStripper();
pdfDocument = PDDocument.load(storedfile.getAbsolutePath(), true);
pdfDocument = PDDocument.load(inputStream, true);
content = stripper.getText(pdfDocument);
} catch (IOException e) {
log.error("Error while extracting text from the PDF " + storedfile, e);
log.error("Error while extracting text from the PDF", e);
} finally {
if (pdfDocument != null) {
try {
@@ -123,39 +126,39 @@ public class FileUtil {
/**
* Save a file on the storage filesystem.
*
* @param is InputStream
* @param inputStream Unencrypted input stream
* @param file File to save
* @throws IOException
* @param privateKey Private key used for encryption
* @throws Exception
*/
public static void save(InputStream is, File file) throws IOException {
public static void save(InputStream inputStream, File file, String privateKey) throws Exception {
Cipher cipher = EncryptionUtil.getEncryptionCipher(privateKey);
Path path = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId());
Files.copy(is, path);
Files.copy(new CipherInputStream(inputStream, cipher), path);
// Generate file variations
try {
saveVariations(file, path.toFile());
} catch (IOException e) {
// Don't rethrow Exception from file variations generation
log.error("Error creating file variations", e);
}
inputStream.reset();
saveVariations(file, inputStream, cipher);
inputStream.reset();
}
/**
* Generate file variations.
*
* @param file File from database
* @param originalFile Original file
* @throws IOException
* @param inputStream Unencrypted input stream
* @param cipher Cipher to use for encryption
* @throws Exception
*/
public static void saveVariations(File file, java.io.File originalFile) throws IOException {
public static void saveVariations(File file, InputStream inputStream, Cipher cipher) throws Exception {
BufferedImage image = null;
if (ImageUtil.isImage(file.getMimeType())) {
image = ImageIO.read(originalFile);
image = ImageIO.read(inputStream);
} else if(file.getMimeType().equals(MimeType.APPLICATION_PDF)) {
// Generate preview from the first page of the PDF
PDDocument pdfDocument = null;
try {
pdfDocument = PDDocument.load(originalFile.getAbsolutePath(), true);
pdfDocument = PDDocument.load(inputStream, true);
@SuppressWarnings("unchecked")
List<PDPage> pageList = pdfDocument.getDocumentCatalog().getAllPages();
if (pageList.size() > 0) {
@@ -172,8 +175,24 @@ public class FileUtil {
BufferedImage web = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 1280, Scalr.OP_ANTIALIAS);
BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 256, Scalr.OP_ANTIALIAS);
image.flush();
ImageUtil.writeJpeg(web, Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId() + "_web").toFile());
ImageUtil.writeJpeg(thumbnail, Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId() + "_thumb").toFile());
// Write "web" encrypted image
java.io.File outputFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId() + "_web").toFile();
OutputStream outputStream = new CipherOutputStream(new FileOutputStream(outputFile), cipher);
try {
ImageUtil.writeJpeg(web, outputStream);
} finally {
outputStream.close();
}
// Write "thumb" encrypted image
outputFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId() + "_thumb").toFile();
outputStream = new CipherOutputStream(new FileOutputStream(outputFile), cipher);
try {
ImageUtil.writeJpeg(thumbnail, outputStream);
} finally {
outputStream.close();
}
}
}

View File

@@ -0,0 +1,173 @@
/**
* Copyright @ 2008 Quan Nguyen
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.sismics.tess4j;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.image.*;
public class ImageHelper {
/**
* Convenience method that returns a scaled instance of the provided
* {@code BufferedImage}.
*
* @param image the original image to be scaled
* @param targetWidth the desired width of the scaled instance, in pixels
* @param targetHeight the desired height of the scaled instance, in pixels
* @return a scaled version of the original {@code BufferedImage}
*/
public static BufferedImage getScaledInstance(BufferedImage image, int targetWidth, int targetHeight) {
int type = (image.getTransparency() == Transparency.OPAQUE)
? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
BufferedImage tmp = new BufferedImage(targetWidth, targetHeight, type);
Graphics2D g2 = tmp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2.drawImage(image, 0, 0, targetWidth, targetHeight, null);
g2.dispose();
return tmp;
}
/**
* A replacement for the standard
* <code>BufferedImage.getSubimage</code> method.
*
* @param image
* @param x the X coordinate of the upper-left corner of the specified
* rectangular region
* @param y the Y coordinate of the upper-left corner of the specified
* rectangular region
* @param width the width of the specified rectangular region
* @param height the height of the specified rectangular region
* @return a BufferedImage that is the subimage of <code>image</code>.
*/
public static BufferedImage getSubImage(BufferedImage image, int x, int y, int width, int height) {
int type = (image.getTransparency() == Transparency.OPAQUE)
? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
BufferedImage tmp = new BufferedImage(width, height, type);
Graphics2D g2 = tmp.createGraphics();
g2.drawImage(image.getSubimage(x, y, width, height), 0, 0, null);
g2.dispose();
return tmp;
}
/**
* A simple method to convert an image to binary or B/W image.
*
* @param image input image
* @return a monochrome image
*/
public static BufferedImage convertImageToBinary(BufferedImage image) {
BufferedImage tmp = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_BINARY);
Graphics2D g2 = tmp.createGraphics();
g2.drawImage(image, 0, 0, null);
g2.dispose();
return tmp;
}
/**
* A simple method to convert an image to binary or B/W image.
*
* @param image input image
* @return a monochrome image
* @deprecated As of release 1.1, renamed to {@link #convertImageToBinary(BufferedImage image)}
*/
@Deprecated
public static BufferedImage convertImage2Binary(BufferedImage image) {
return convertImageToBinary(image);
}
/**
* A simple method to convert an image to gray scale.
*
* @param image input image
* @return a monochrome image
*/
public static BufferedImage convertImageToGrayscale(BufferedImage image) {
BufferedImage tmp = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g2 = tmp.createGraphics();
g2.drawImage(image, 0, 0, null);
g2.dispose();
return tmp;
}
private static final short[] invertTable;
static {
invertTable = new short[256];
for (int i = 0; i < 256; i++) {
invertTable[i] = (short) (255 - i);
}
}
/**
* Inverts image color.
*
* @param image input image
* @return an inverted-color image
*/
public static BufferedImage invertImageColor(BufferedImage image) {
BufferedImage tmp = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
BufferedImageOp invertOp = new LookupOp(new ShortLookupTable(0, invertTable), null);
return invertOp.filter(image, tmp);
}
/**
* Rotates an image.
*
* @param image the original image
* @param angle the degree of rotation
* @return a rotated image
*/
public static BufferedImage rotateImage(BufferedImage image, double angle) {
double theta = Math.toRadians(angle);
double sin = Math.abs(Math.sin(theta));
double cos = Math.abs(Math.cos(theta));
int w = image.getWidth();
int h = image.getHeight();
int newW = (int) Math.floor(w * cos + h * sin);
int newH = (int) Math.floor(h * cos + w * sin);
BufferedImage tmp = new BufferedImage(newW, newH, image.getType());
Graphics2D g2d = tmp.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.translate((newW - w) / 2, (newH - h) / 2);
g2d.rotate(theta, w / 2, h / 2);
g2d.drawImage(image, 0, 0, null);
g2d.dispose();
return tmp;
}
/**
* Gets an image from Clipboard.
*
* @return image
*/
public static Image getClipboardImage() {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
try {
return (Image) clipboard.getData(DataFlavor.imageFlavor);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,143 @@
/**
* Copyright @ 2008 Quan Nguyen
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.sismics.tess4j;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;
import org.w3c.dom.NodeList;
import com.sun.media.imageio.plugins.tiff.TIFFImageWriteParam;
import com.sun.media.imageioimpl.plugins.tiff.TIFFImageReaderSpi;
import com.sun.media.imageioimpl.plugins.tiff.TIFFImageWriterSpi;
public class ImageIOHelper {
final static String TIFF_FORMAT = "tiff";
/**
* Gets pixel data of an
* <code>IIOImage</code> object.
*
* @param image an
* <code>IIOImage</code> object
* @return a byte buffer of pixel data
* @throws Exception
*/
public static ByteBuffer getImageByteBuffer(IIOImage image) throws IOException {
//Set up the writeParam
TIFFImageWriteParam tiffWriteParam = new TIFFImageWriteParam(Locale.US);
tiffWriteParam.setCompressionMode(ImageWriteParam.MODE_DISABLED);
//Get tif writer and set output to file
ImageWriter writer = new TIFFImageWriterSpi().createWriterInstance();
//Get the stream metadata
IIOMetadata streamMetadata = writer.getDefaultStreamMetadata(tiffWriteParam);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageOutputStream ios = ImageIO.createImageOutputStream(outputStream);
writer.setOutput(ios);
writer.write(streamMetadata, new IIOImage(image.getRenderedImage(), null, null), tiffWriteParam);
writer.dispose();
// Read the writed image
ios.seek(0);
ImageReader reader = new TIFFImageReaderSpi().createReaderInstance();
ImageReadParam param = reader.getDefaultReadParam();
reader.setInput(ios, true, true);
BufferedImage bi;
try {
bi = reader.read(0, param);
} finally {
reader.dispose();
ios.close();
}
return convertImageData(bi);
}
/**
* Converts <code>BufferedImage</code> to <code>ByteBuffer</code>.
*
* @param bi Input image
* @return pixel data
*/
public static ByteBuffer convertImageData(BufferedImage bi) {
byte[] pixelData = ((DataBufferByte) bi.getRaster().getDataBuffer()).getData();
// return ByteBuffer.wrap(pixelData);
ByteBuffer buf = ByteBuffer.allocateDirect(pixelData.length);
buf.order(ByteOrder.nativeOrder());
buf.put(pixelData);
buf.flip();
return buf;
}
/**
* Reads image meta data.
*
* @param oimage
* @return a map of meta data
*/
public static Map<String, String> readImageData(IIOImage oimage) {
Map<String, String> dict = new HashMap<String, String>();
IIOMetadata imageMetadata = oimage.getMetadata();
if (imageMetadata != null) {
IIOMetadataNode dimNode = (IIOMetadataNode) imageMetadata.getAsTree("javax_imageio_1.0");
NodeList nodes = dimNode.getElementsByTagName("HorizontalPixelSize");
int dpiX;
if (nodes.getLength() > 0) {
float dpcWidth = Float.parseFloat(nodes.item(0).getAttributes().item(0).getNodeValue());
dpiX = (int) Math.round(25.4f / dpcWidth);
} else {
dpiX = Toolkit.getDefaultToolkit().getScreenResolution();
}
dict.put("dpiX", String.valueOf(dpiX));
nodes = dimNode.getElementsByTagName("VerticalPixelSize");
int dpiY;
if (nodes.getLength() > 0) {
float dpcHeight = Float.parseFloat(nodes.item(0).getAttributes().item(0).getNodeValue());
dpiY = (int) Math.round(25.4f / dpcHeight);
} else {
dpiY = Toolkit.getDefaultToolkit().getScreenResolution();
}
dict.put("dpiY", String.valueOf(dpiY));
}
return dict;
}
}

View File

@@ -0,0 +1,686 @@
/**
* Copyright @ 2012 Quan Nguyen
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.sismics.tess4j;
import com.sun.jna.*;
import com.sun.jna.ptr.*;
import java.nio.*;
/**
* A Java wrapper for
* <code>Tesseract OCR 3.02 API</code> using
* <code>JNA Interface Mapping</code>.
*/
public interface TessAPI extends Library {
static final boolean WINDOWS = System.getProperty("os.name").toLowerCase().startsWith("windows");
/**
* Native library name.
*/
public static final String LIB_NAME = "libtesseract302";
public static final String LIB_NAME_NON_WIN = "tesseract";
/**
* An instance of the class library.
*/
public static final TessAPI INSTANCE = (TessAPI) Native.loadLibrary(WINDOWS ? LIB_NAME : LIB_NAME_NON_WIN, TessAPI.class);
/**
* When Tesseract/Cube is initialized we can choose to instantiate/load/run
* only the Tesseract part, only the Cube part or both along with the
* combiner. The preference of which engine to use is stored in
* <code>tessedit_ocr_engine_mode</code>.<br /> <br /> ATTENTION: When
* modifying this enum, please make sure to make the appropriate changes to
* all the enums mirroring it (e.g. OCREngine in
* cityblock/workflow/detection/detection_storage.proto). Such enums will
* mention the connection to OcrEngineMode in the comments.
*/
public static interface TessOcrEngineMode {
public static final int OEM_TESSERACT_ONLY = (int) 0;
public static final int OEM_CUBE_ONLY = (int) 1;
public static final int OEM_TESSERACT_CUBE_COMBINED = (int) 2;
public static final int OEM_DEFAULT = (int) 3;
};
/**
* Possible modes for page layout analysis. These *must* be kept in order of
* decreasing amount of layout analysis to be done, except for
* <code>OSD_ONLY</code>, so that the inequality test macros below work.
*/
public static interface TessPageSegMode {
public static final int PSM_OSD_ONLY = (int) 0;
public static final int PSM_AUTO_OSD = (int) 1;
public static final int PSM_AUTO_ONLY = (int) 2;
public static final int PSM_AUTO = (int) 3;
public static final int PSM_SINGLE_COLUMN = (int) 4;
public static final int PSM_SINGLE_BLOCK_VERT_TEXT = (int) 5;
public static final int PSM_SINGLE_BLOCK = (int) 6;
public static final int PSM_SINGLE_LINE = (int) 7;
public static final int PSM_SINGLE_WORD = (int) 8;
public static final int PSM_CIRCLE_WORD = (int) 9;
public static final int PSM_SINGLE_CHAR = (int) 10;
public static final int PSM_COUNT = (int) 11;
};
/**
* Enum of the elements of the page hierarchy, used in
* <code>ResultIterator</code> to provide functions that operate on each
* level without having to have 5x as many functions.
*/
public static interface TessPageIteratorLevel {
public static final int RIL_BLOCK = (int) 0;
public static final int RIL_PARA = (int) 1;
public static final int RIL_TEXTLINE = (int) 2;
public static final int RIL_WORD = (int) 3;
public static final int RIL_SYMBOL = (int) 4;
};
public static interface TessPolyBlockType {
public static final int PT_UNKNOWN = (int) 0;
public static final int PT_FLOWING_TEXT = (int) 1;
public static final int PT_HEADING_TEXT = (int) 2;
public static final int PT_PULLOUT_TEXT = (int) 3;
public static final int PT_TABLE = (int) 4;
public static final int PT_VERTICAL_TEXT = (int) 5;
public static final int PT_CAPTION_TEXT = (int) 6;
public static final int PT_FLOWING_IMAGE = (int) 7;
public static final int PT_HEADING_IMAGE = (int) 8;
public static final int PT_PULLOUT_IMAGE = (int) 9;
public static final int PT_HORZ_LINE = (int) 10;
public static final int PT_VERT_LINE = (int) 11;
public static final int PT_NOISE = (int) 12;
public static final int PT_COUNT = (int) 13;
};
/**
* <pre>
* +------------------+
* | 1 Aaaa Aaaa Aaaa |
* | Aaa aa aaa aa |
* | aaaaaa A aa aaa. |
* | 2 |
* | ####### c c C |
* | ####### c c c |
* | < ####### c c c |
* | < ####### c c |
* | < ####### . c |
* | 3 ####### c |
* +------------------+
* </pre>
* Orientation Example:<br />
* ====================<br />
* Above is a
* diagram of some (1) English and (2) Chinese text and a (3) photo
* credit.<br />
* <br />
* Upright Latin characters are represented as A and a. '<' represents
* a latin character rotated anti-clockwise 90 degrees. Upright
* Chinese characters are represented C and c.<br />
* <br />
* NOTA BENE: enum values here should match goodoc.proto<br />
* <br />
* If you orient your head so that "up" aligns with Orientation, then
* the characters will appear "right side up" and readable.<br />
* <br />
* In the example above, both the
* English and Chinese paragraphs are oriented so their "up" is the top of
* the page (page up). The photo credit is read with one's head turned
* leftward ("up" is to page left).<br />
* <br /> The values of this enum
* match the convention of Tesseract's osdetect.h
*/
public static interface TessOrientation {
public static final int ORIENTATION_PAGE_UP = (int) 0;
public static final int ORIENTATION_PAGE_RIGHT = (int) 1;
public static final int ORIENTATION_PAGE_DOWN = (int) 2;
public static final int ORIENTATION_PAGE_LEFT = (int) 3;
};
/**
* The grapheme clusters within a line of text are laid out logically in
* this direction, judged when looking at the text line rotated so that its
* Orientation is "page up".<br /> <br /> For English text, the writing
* direction is left-to-right. For the Chinese text in the above example,
* the writing direction is top-to-bottom.
*/
public static interface TessWritingDirection {
public static final int WRITING_DIRECTION_LEFT_TO_RIGHT = (int) 0;
public static final int WRITING_DIRECTION_RIGHT_TO_LEFT = (int) 1;
public static final int WRITING_DIRECTION_TOP_TO_BOTTOM = (int) 2;
};
/**
* The text lines are read in the given sequence.<br /> <br /> In English,
* the order is top-to-bottom. In Chinese, vertical text lines are read
* right-to-left. Mongolian is written in vertical columns top to bottom
* like Chinese, but the lines order left-to right.<br /> <br /> Note that
* only some combinations make sense. For example,
* <code>WRITING_DIRECTION_LEFT_TO_RIGHT</code> implies
* <code>TEXTLINE_ORDER_TOP_TO_BOTTOM</code>.
*/
public static interface TessTextlineOrder {
public static final int TEXTLINE_ORDER_LEFT_TO_RIGHT = (int) 0;
public static final int TEXTLINE_ORDER_RIGHT_TO_LEFT = (int) 1;
public static final int TEXTLINE_ORDER_TOP_TO_BOTTOM = (int) 2;
};
public static final int TRUE = (int) 1;
public static final int FALSE = (int) 0;
/**
* Returns the version identifier.
*/
String TessVersion();
void TessDeleteText(Pointer text);
void TessDeleteTextArray(PointerByReference arr);
void TessDeleteIntArray(IntBuffer arr);
/**
* Creates an instance of the base class for all Tesseract APIs.
*/
TessAPI.TessBaseAPI TessBaseAPICreate();
/**
* Disposes the TesseractAPI instance.
*/
void TessBaseAPIDelete(TessAPI.TessBaseAPI handle);
/**
* Set the name of the input file. Needed only for training and reading a
* UNLV zone file.
*/
void TessBaseAPISetInputName(TessAPI.TessBaseAPI handle, String name);
/**
* Set the name of the bonus output files. Needed only for debugging.
*/
void TessBaseAPISetOutputName(TessAPI.TessBaseAPI handle, String name);
/**
* Set the value of an internal "parameter." Supply the name of the
* parameter and the value as a string, just as you would in a config file.
* Returns false if the name lookup failed. E.g.,
* <code>SetVariable("tessedit_char_blacklist", "xyz");</code> to ignore x,
* y and z. Or
* <code>SetVariable("classify_bln_numeric_mode", "1");</code> to set
* numeric-only mode.
* <code>SetVariable</code> may be used before
* <code>Init</code>, but settings will revert to defaults on
* <code>End()</code>.<br /> <br /> Note: Must be called after
* <code>Init()</code>. Only works for non-init variables (init variables
* should be passed to
* <code>Init()</code>).
*/
int TessBaseAPISetVariable(TessAPI.TessBaseAPI handle, String name, String value);
/**
* Returns true (1) if the parameter was found among Tesseract parameters.
* Fills in value with the value of the parameter.
*/
int TessBaseAPIGetIntVariable(TessAPI.TessBaseAPI handle, String name, IntBuffer value);
int TessBaseAPIGetBoolVariable(TessAPI.TessBaseAPI handle, String name, IntBuffer value);
int TessBaseAPIGetDoubleVariable(TessAPI.TessBaseAPI handle, String name, DoubleBuffer value);
String TessBaseAPIGetStringVariable(TessAPI.TessBaseAPI handle, String name);
/**
* Print Tesseract parameters to the given file.<br /> <br /> Note: Must not
* be the first method called after instance create.
*/
void TessBaseAPIPrintVariablesToFile(TessAPI.TessBaseAPI handle, String filename);
/**
* Instances are now mostly thread-safe and totally independent, but some
* global parameters remain. Basically it is safe to use multiple
* TessBaseAPIs in different threads in parallel, UNLESS: you use
* <code>SetVariable</code> on some of the Params in classify and textord.
* If you do, then the effect will be to change it for all your
* instances.<br /> <br /> Start tesseract. Returns zero on success and -1
* on failure. NOTE that the only members that may be called before Init are
* those listed above here in the class definition.<br /> <br /> The
* <code>datapath</code> must be the name of the parent directory of
* tessdata and must end in / . Any name after the last / will be stripped.
* The language is (usually) an
* <code>ISO 639-3</code> string or
* <code>NULL</code> will default to eng. It is entirely safe (and
* eventually will be efficient too) to call Init multiple times on the same
* instance to change language, or just to reset the classifier. The
* language may be a string of the form [~]<lang>[+[~]<lang>]* indicating
* that multiple languages are to be loaded. E.g., hin+eng will load Hindi
* and English. Languages may specify internally that they want to be loaded
* with one or more other languages, so the ~ sign is available to override
* that. E.g., if hin were set to load eng by default, then hin+~eng would
* force loading only hin. The number of loaded languages is limited only by
* memory, with the caveat that loading additional languages will impact
* both speed and accuracy, as there is more work to do to decide on the
* applicable language, and there is more chance of hallucinating incorrect
* words. WARNING: On changing languages, all Tesseract parameters are reset
* back to their default values. (Which may vary between languages.) If you
* have a rare need to set a Variable that controls initialization for a
* second call to
* <code>Init</code> you should explicitly call
* <code>End()</code> and then use
* <code>SetVariable</code> before
* <code>Init</code>. This is only a very rare use case, since there are
* very few uses that require any parameters to be set before
* <code>Init</code>.<br /> <br /> If
* <code>set_only_non_debug_params</code> is true, only params that do not
* contain "debug" in the name will be set.
*/
int TessBaseAPIInit1(TessAPI.TessBaseAPI handle, String datapath, String language, int oem, PointerByReference configs, int configs_size);
int TessBaseAPIInit2(TessAPI.TessBaseAPI handle, String datapath, String language, int oem);
int TessBaseAPIInit3(TessAPI.TessBaseAPI handle, String datapath, String language);
/**
* Returns the languages string used in the last valid initialization. If
* the last initialization specified "deu+hin" then that will be returned.
* If hin loaded eng automatically as well, then that will not be included
* in this list. To find the languages actually loaded, use
* <code>GetLoadedLanguagesAsVector</code>. The returned string should NOT
* be deleted.
*/
String TessBaseAPIGetInitLanguagesAsString(TessAPI.TessBaseAPI handle);
/**
* Returns the loaded languages in the vector of STRINGs. Includes all
* languages loaded by the last
* <code>Init</code>, including those loaded as dependencies of other loaded
* languages.
*/
PointerByReference TessBaseAPIGetLoadedLanguagesAsVector(TessAPI.TessBaseAPI handle);
/**
* Returns the available languages in the vector of STRINGs.
*/
PointerByReference TessBaseAPIGetAvailableLanguagesAsVector(TessAPI.TessBaseAPI handle);
/**
* Init only the lang model component of Tesseract. The only functions that
* work after this init are
* <code>SetVariable</code> and
* <code>IsValidWord</code>. WARNING: temporary! This function will be
* removed from here and placed in a separate API at some future time.
*/
int TessBaseAPIInitLangMod(TessAPI.TessBaseAPI handle, String datapath, String language);
/**
* Init only for page layout analysis. Use only for calls to
* <code>SetImage</code> and
* <code>AnalysePage</code>. Calls that attempt recognition will generate an
* error.
*/
void TessBaseAPIInitForAnalysePage(TessAPI.TessBaseAPI handle);
/**
* Read a "config" file containing a set of param, value pairs. Searches the
* standard places:
* <code>tessdata/configs</code>,
* <code>tessdata/tessconfigs</code> and also accepts a relative or absolute
* path name. Note: only non-init params will be set (init params are set by
* <code>Init()</code>).
*/
void TessBaseAPIReadConfigFile(TessAPI.TessBaseAPI handle, String filename, int init_only);
/**
* Set the current page segmentation mode. Defaults to PSM_SINGLE_BLOCK. The
* mode is stored as an IntParam so it can also be modified by
* <code>ReadConfigFile</code> or
* <code>SetVariable("tessedit_pageseg_mode", mode as string)</code>.
*/
void TessBaseAPISetPageSegMode(TessAPI.TessBaseAPI handle, int mode);
/**
* Return the current page segmentation mode.
*/
int TessBaseAPIGetPageSegMode(TessAPI.TessBaseAPI handle);
/**
* Recognize a rectangle from an image and return the result as a string.
* May be called many times for a single
* <code>Init</code>. Currently has no error checking. Greyscale of 8 and
* color of 24 or 32 bits per pixel may be given. Palette color images will
* not work properly and must be converted to 24 bit. Binary images of 1 bit
* per pixel may also be given but they must be byte packed with the MSB of
* the first byte being the first pixel, and a 1 represents WHITE. For
* binary images set bytes_per_pixel=0. The recognized text is returned as a
* char* which is coded as UTF8 and must be freed with the delete []
* operator.<br /> <br /> Note that
* <code>TesseractRect</code> is the simplified convenience interface. For
* advanced uses, use
* <code>SetImage</code>, (optionally)
* <code>SetRectangle</code>,
* <code>Recognize</code>, and one or more of the
* <code>Get*Text</code> functions below.
*/
Pointer TessBaseAPIRect(TessAPI.TessBaseAPI handle, ByteBuffer imagedata, int bytes_per_pixel, int bytes_per_line, int left, int top, int width, int height);
/**
* Call between pages or documents etc to free up memory and forget adaptive
* data.
*/
void TessBaseAPIClearAdaptiveClassifier(TessAPI.TessBaseAPI handle);
/**
* Provide an image for Tesseract to recognize. Format is as TesseractRect
* above. Does not copy the image buffer, or take ownership. The source
* image may be destroyed after Recognize is called, either explicitly or
* implicitly via one of the
* <code>Get*Text</code> functions.
* <code>SetImage</code> clears all recognition results, and sets the
* rectangle to the full image, so it may be followed immediately by a
* <code>GetUTF8Text</code>, and it will automatically perform recognition.
*/
void TessBaseAPISetImage(TessAPI.TessBaseAPI handle, ByteBuffer imagedata, int width, int height, int bytes_per_pixel, int bytes_per_line);
/**
* Set the resolution of the source image in pixels per inch so font size
* information can be calculated in results. Call this after SetImage().
*/
void TessBaseAPISetSourceResolution(TessAPI.TessBaseAPI handle, int ppi);
/**
* Restrict recognition to a sub-rectangle of the image. Call after
* <code>SetImage</code>. Each
* <code>SetRectangle</code> clears the recognition results so multiple
* rectangles can be recognized with the same image.
*/
void TessBaseAPISetRectangle(TessAPI.TessBaseAPI handle, int left, int top, int width, int height);
/** Scale factor from original image. */
int TessBaseAPIGetThresholdedImageScaleFactor(TessAPI.TessBaseAPI handle);
/** Dump the internal binary image to a PGM file. */
void TessBaseAPIDumpPGM(TessAPI.TessBaseAPI handle, String filename);
/**
* Runs page layout analysis in the mode set by SetPageSegMode. May
* optionally be called prior to Recognize to get access to just the page
* layout results. Returns an iterator to the results. Returns NULL on
* error. The returned iterator must be deleted after use. WARNING! This
* class points to data held within the TessBaseAPI class, and therefore can
* only be used while the TessBaseAPI class still exists and has not been
* subjected to a call of
* <code>Init</code>,
* <code>SetImage</code>,
* <code>Recognize</code>,
* <code>Clear</code>,
* <code>End</code>, DetectOS, or anything else that changes the internal
* PAGE_RES.
*/
TessAPI.TessPageIterator TessBaseAPIAnalyseLayout(TessAPI.TessBaseAPI handle);
/**
* Recognize the image from SetAndThresholdImage, generating Tesseract
* internal structures. Returns 0 on success. Optional. The
* <code>Get*Text</code> functions below will call
* <code>Recognize</code> if needed. After Recognize, the output is kept
* internally until the next
* <code>SetImage</code>.
*/
int TessBaseAPIRecognize(TessAPI.TessBaseAPI handle, TessAPI.ETEXT_DESC monitor);
/**
* Variant on Recognize used for testing chopper.
*/
int TessBaseAPIRecognizeForChopTest(TessAPI.TessBaseAPI handle, TessAPI.ETEXT_DESC monitor);
/**
* Get a reading-order iterator to the results of LayoutAnalysis and/or
* Recognize. The returned iterator must be deleted after use. WARNING! This
* class points to data held within the TessBaseAPI class, and therefore can
* only be used while the TessBaseAPI class still exists and has not been
* subjected to a call of
* <code>Init</code>,
* <code>SetImage</code>,
* <code>Recognize</code>,
* <code>Clear</code>,
* <code>End</code>, DetectOS, or anything else that changes the internal
* PAGE_RES.
*/
TessAPI.TessResultIterator TessBaseAPIGetIterator(TessAPI.TessBaseAPI handle);
/**
* Get a mutable iterator to the results of LayoutAnalysis and/or Recognize.
* The returned iterator must be deleted after use.
* WARNING! This class points to data held within the TessBaseAPI class, and
* therefore can only be used while the TessBaseAPI class still exists and
* has not been subjected to a call of Init, SetImage, Recognize, Clear, End
* DetectOS, or anything else that changes the internal PAGE_RES.
*/
TessAPI.TessMutableIterator TessBaseAPIGetMutableIterator(TessAPI.TessBaseAPI handle);
/**
* Recognizes all the pages in the named file, as a multi-page tiff or list
* of filenames, or single image, and gets the appropriate kind of text
* according to parameters:
* <code>tessedit_create_boxfile</code>,
* <code>tessedit_make_boxes_from_boxes</code>,
* <code>tessedit_write_unlv</code>,
* <code>tessedit_create_hocr</code>. Calls ProcessPage on each page in the
* input file, which may be a multi-page tiff, single-page other file
* format, or a plain text list of images to read. If tessedit_page_number
* is non-negative, processing begins at that page of a multi-page tiff
* file, or filelist. The text is returned in text_out. Returns false on
* error. If non-zero timeout_millisec terminates processing after the
* timeout on a single page. If non-NULL and non-empty, and some page fails
* for some reason, the page is reprocessed with the retry_config config
* file. Useful for interactively debugging a bad page.
*/
Pointer TessBaseAPIProcessPages(TessAPI.TessBaseAPI handle, String filename, String retry_config, int timeout_millisec);
/**
* The recognized text is returned as a char* which is coded as UTF-8 and
* must be freed with the delete [] operator.
*/
Pointer TessBaseAPIGetUTF8Text(TessAPI.TessBaseAPI handle);
/**
* Make a HTML-formatted string with hOCR markup from the internal data
* structures. page_number is 0-based but will appear in the output as
* 1-based.
*/
Pointer TessBaseAPIGetHOCRText(TessAPI.TessBaseAPI handle, int page_number);
/**
* The recognized text is returned as a char* which is coded in the same
* format as a box file used in training. Returned string must be freed with
* the delete [] operator. Constructs coordinates in the original image -
* not just the rectangle. page_number is a 0-based page index that will
* appear in the box file.
*/
Pointer TessBaseAPIGetBoxText(TessAPI.TessBaseAPI handle, int page_number);
/**
* The recognized text is returned as a char* which is coded as UNLV format
* Latin-1 with specific reject and suspect codes and must be freed with the
* delete [] operator.
*/
Pointer TessBaseAPIGetUNLVText(TessAPI.TessBaseAPI handle);
/**
* Returns the (average) confidence value between 0 and 100.
*/
int TessBaseAPIMeanTextConf(TessAPI.TessBaseAPI handle);
/**
* Returns all word confidences (between 0 and 100) in an array, terminated
* by -1. The calling function must delete [] after use. The number of
* confidences should correspond to the number of space-delimited words in
* GetUTF8Text.
*/
IntByReference TessBaseAPIAllWordConfidences(TessAPI.TessBaseAPI handle);
/**
* Applies the given word to the adaptive classifier if possible. The word
* must be SPACE-DELIMITED UTF-8 - l i k e t h i s , so it can tell the
* boundaries of the graphemes. Assumes that SetImage/SetRectangle have been
* used to set the image to the given word. The mode arg should be
* PSM_SINGLE_WORD or PSM_CIRCLE_WORD, as that will be used to control
* layout analysis. The currently set PageSegMode is preserved. Returns
* false if adaption was not possible for some reason.
*/
int TessBaseAPIAdaptToWordStr(TessAPI.TessBaseAPI handle, int mode, String wordstr);
/**
* Free up recognition results and any stored image data, without actually
* freeing any recognition data that would be time-consuming to reload.
* Afterwards, you must call
* <code>SetImage</code> or
* <code>TesseractRect</code> before doing any
* <code>Recognize</code> or
* <code>Get*</code> operation.
*/
void TessBaseAPIClear(TessAPI.TessBaseAPI handle);
/**
* Close down tesseract and free up all memory.
* <code>End()</code> is equivalent to destructing and reconstructing your
* TessBaseAPI. Once
* <code>End()</code> has been used, none of the other API functions may be
* used other than
* <code>Init</code> and anything declared above it in the class definition.
*/
void TessBaseAPIEnd(TessAPI.TessBaseAPI handle);
/**
* Check whether a word is valid according to Tesseract's language model.
*
* @return 0 if the word is invalid, non-zero if valid. @warning temporary!
* This function will be removed from here and placed in a separate API at
* some future time.
*/
int TessBaseAPIIsValidWord(TessAPI.TessBaseAPI handle, String word);
int TessBaseAPIGetTextDirection(TessAPI.TessBaseAPI handle, IntBuffer out_offset, FloatBuffer out_slope);
/**
* This method returns the string form of the specified unichar.
*/
String TessBaseAPIGetUnichar(TessAPI.TessBaseAPI handle, int unichar_id);
/* Page iterator */
void TessPageIteratorDelete(TessAPI.TessPageIterator handle);
TessAPI.TessPageIterator TessPageIteratorCopy(TessAPI.TessPageIterator handle);
void TessPageIteratorBegin(TessAPI.TessPageIterator handle);
int TessPageIteratorNext(TessAPI.TessPageIterator handle, int level);
int TessPageIteratorIsAtBeginningOf(TessAPI.TessPageIterator handle, int level);
int TessPageIteratorIsAtFinalElement(TessAPI.TessPageIterator handle, int level, int element);
int TessPageIteratorBoundingBox(TessAPI.TessPageIterator handle, int level, IntBuffer left, IntBuffer top, IntBuffer right, IntBuffer bottom);
int TessPageIteratorBlockType(TessAPI.TessPageIterator handle);
int TessPageIteratorBaseline(TessAPI.TessPageIterator handle, int level, IntBuffer x1, IntBuffer y1, IntBuffer x2, IntBuffer y2);
void TessPageIteratorOrientation(TessAPI.TessPageIterator handle, IntBuffer orientation, IntBuffer writing_direction, IntBuffer textline_order, FloatBuffer deskew_angle);
/* Result iterator */
void TessResultIteratorDelete(TessAPI.TessResultIterator handle);
TessAPI.TessResultIterator TessResultIteratorCopy(TessAPI.TessResultIterator handle);
TessAPI.TessPageIterator TessResultIteratorGetPageIterator(TessAPI.TessResultIterator handle);
TessAPI.TessPageIterator TessResultIteratorGetPageIteratorConst(TessAPI.TessResultIterator handle);
Pointer TessResultIteratorGetUTF8Text(TessAPI.TessResultIterator handle, int level);
float TessResultIteratorConfidence(TessAPI.TessResultIterator handle, int level);
String TessResultIteratorWordFontAttributes(TessAPI.TessResultIterator handle, IntBuffer is_bold, IntBuffer is_italic, IntBuffer is_underlined, IntBuffer is_monospace, IntBuffer is_serif, IntBuffer is_smallcaps, IntBuffer pointsize, IntBuffer font_id);
int TessResultIteratorWordIsFromDictionary(TessAPI.TessResultIterator handle);
int TessResultIteratorWordIsNumeric(TessAPI.TessResultIterator handle);
int TessResultIteratorSymbolIsSuperscript(TessAPI.TessResultIterator handle);
int TessResultIteratorSymbolIsSubscript(TessAPI.TessResultIterator handle);
int TessResultIteratorSymbolIsDropcap(TessAPI.TessResultIterator handle);
public static class TessBaseAPI extends PointerType {
public TessBaseAPI(Pointer address) {
super(address);
}
public TessBaseAPI() {
super();
}
};
public static class ETEXT_DESC extends PointerType {
public ETEXT_DESC(Pointer address) {
super(address);
}
public ETEXT_DESC() {
super();
}
};
public static class TessPageIterator extends PointerType {
public TessPageIterator(Pointer address) {
super(address);
}
public TessPageIterator() {
super();
}
};
public static class TessMutableIterator extends PointerType {
public TessMutableIterator(Pointer address) {
super(address);
}
public TessMutableIterator() {
super();
}
};
public static class TessResultIterator extends PointerType {
public TessResultIterator(Pointer address) {
super(address);
}
public TessResultIterator() {
super();
}
};
}

View File

@@ -0,0 +1,256 @@
/**
* Copyright @ 2012 Quan Nguyen
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.sismics.tess4j;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import javax.imageio.IIOImage;
import com.sun.jna.Pointer;
/**
* An object layer on top of
* <code>TessAPI</code>, provides character recognition support for common image
* formats, and multi-page TIFF images beyond the uncompressed, binary TIFF
* format supported by Tesseract OCR engine. The extended capabilities are
* provided by the
* <code>Java Advanced Imaging Image I/O Tools</code>. <br /><br /> Support for
* PDF documents is available through
* <code>Ghost4J</code>, a
* <code>JNA</code> wrapper for
* <code>GPL Ghostscript</code>, which should be installed and included in
* system path. <br /><br /> Any program that uses the library will need to
* ensure that the required libraries (the
* <code>.jar</code> files for
* <code>jna</code>,
* <code>jai-imageio</code>, and
* <code>ghost4j</code>) are in its compile and run-time
* <code>classpath</code>.
*/
public class Tesseract {
private static Tesseract instance;
private final static Rectangle EMPTY_RECTANGLE = new Rectangle();
private String language = "eng";
private String datapath = "tessdata";
private int psm = TessAPI.TessPageSegMode.PSM_AUTO;
private boolean hocr;
private int pageNum;
private int ocrEngineMode = TessAPI.TessOcrEngineMode.OEM_DEFAULT;
private Properties prop = new Properties();
public final static String htmlBeginTag =
"<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\""
+ " \"http://www.w3.org/TR/html4/loose.dtd\">\n"
+ "<html>\n<head>\n<title></title>\n"
+ "<meta http-equiv=\"Content-Type\" content=\"text/html;"
+ "charset=utf-8\" />\n<meta name='ocr-system' content='tesseract'/>\n"
+ "</head>\n<body>\n";
public final static String htmlEndTag = "</body>\n</html>\n";
/**
* Private constructor.
*/
private Tesseract() {
System.setProperty("jna.encoding", "UTF8");
}
/**
* Gets an instance of the class library.
*
* @return instance
*/
public static synchronized Tesseract getInstance() {
if (instance == null) {
instance = new Tesseract();
}
return instance;
}
/**
* Sets tessdata path.
*
* @param datapath the tessdata path to set
*/
public void setDatapath(String datapath) {
this.datapath = datapath;
}
/**
* Sets language for OCR.
*
* @param language the language code, which follows ISO 639-3 standard.
*/
public void setLanguage(String language) {
this.language = language;
}
/**
* Sets OCR engine mode.
*
* @param ocrEngineMode the OcrEngineMode to set
*/
public void setOcrEngineMode(int ocrEngineMode) {
this.ocrEngineMode = ocrEngineMode;
}
/**
* Sets page segmentation mode.
*
* @param mode the page segmentation mode to set
*/
public void setPageSegMode(int mode) {
this.psm = mode;
}
/**
* Enables hocr output.
*
* @param hocr to enable or disable hocr output
*/
public void setHocr(boolean hocr) {
this.hocr = hocr;
prop.setProperty("tessedit_create_hocr", hocr ? "1" : "0");
}
/**
* Set the value of Tesseract's internal parameter.
*
* @param key variable name, e.g.,
* <code>tessedit_create_hocr</code>,
* <code>tessedit_char_whitelist</code>, etc.
* @param value value for corresponding variable, e.g., "1", "0",
* "0123456789", etc.
*/
public void setTessVariable(String key, String value) {
prop.setProperty(key, value);
}
/**
* Performs OCR operation.
*
* @param bi a buffered image
* @return the recognized text
* @throws TesseractException
*/
public String doOCR(BufferedImage bi) throws TesseractException {
return doOCR(bi, null);
}
/**
* Performs OCR operation.
*
* @param bi a buffered image
* @param rect the bounding rectangle defines the region of the image to be
* recognized. A rectangle of zero dimension or
* <code>null</code> indicates the whole image.
* @return the recognized text
* @throws TesseractException
*/
public String doOCR(BufferedImage bi, Rectangle rect) throws TesseractException {
IIOImage oimage = new IIOImage(bi, null, null);
List<IIOImage> imageList = new ArrayList<IIOImage>();
imageList.add(oimage);
return doOCR(imageList, rect);
}
/**
* Performs OCR operation.
*
* @param imageList a list of
* <code>IIOImage</code> objects
* @param rect the bounding rectangle defines the region of the image to be
* recognized. A rectangle of zero dimension or
* <code>null</code> indicates the whole image.
* @return the recognized text
* @throws TesseractException
*/
public String doOCR(List<IIOImage> imageList, Rectangle rect) throws TesseractException {
StringBuilder sb = new StringBuilder();
pageNum = 0;
for (IIOImage oimage : imageList) {
pageNum++;
try {
ByteBuffer buf = ImageIOHelper.getImageByteBuffer(oimage);
RenderedImage ri = oimage.getRenderedImage();
String pageText = doOCR(ri.getWidth(), ri.getHeight(), buf, rect, ri.getColorModel().getPixelSize());
sb.append(pageText);
} catch (IOException ioe) {
//skip the problematic image
System.err.println(ioe.getMessage());
}
}
if (hocr) {
sb.insert(0, htmlBeginTag).append(htmlEndTag);
}
return sb.toString();
}
/**
* Performs OCR operation. Use
* <code>SetImage</code>, (optionally)
* <code>SetRectangle</code>, and one or more of the
* <code>Get*Text</code> functions.
*
* @param xsize width of image
* @param ysize height of image
* @param buf pixel data
* @param rect the bounding rectangle defines the region of the image to be
* recognized. A rectangle of zero dimension or
* <code>null</code> indicates the whole image.
* @param bpp bits per pixel, represents the bit depth of the image, with 1
* for binary bitmap, 8 for gray, and 24 for color RGB.
* @return the recognized text
* @throws TesseractException
*/
public String doOCR(int xsize, int ysize, ByteBuffer buf, Rectangle rect, int bpp) throws TesseractException {
TessAPI api = TessAPI.INSTANCE;
TessAPI.TessBaseAPI handle = api.TessBaseAPICreate();
api.TessBaseAPIInit2(handle, datapath, language, ocrEngineMode);
api.TessBaseAPISetPageSegMode(handle, psm);
Enumeration<?> em = prop.propertyNames();
while (em.hasMoreElements()) {
String key = (String) em.nextElement();
api.TessBaseAPISetVariable(handle, key, prop.getProperty(key));
}
int bytespp = bpp / 8;
int bytespl = (int) Math.ceil(xsize * bpp / 8.0);
api.TessBaseAPISetImage(handle, buf, xsize, ysize, bytespp, bytespl);
if (rect != null && !rect.equals(EMPTY_RECTANGLE)) {
api.TessBaseAPISetRectangle(handle, rect.x, rect.y, rect.width, rect.height);
}
Pointer utf8Text = hocr ? api.TessBaseAPIGetHOCRText(handle, pageNum - 1) : api.TessBaseAPIGetUTF8Text(handle);
String str = utf8Text.getString(0);
api.TessDeleteText(utf8Text);
api.TessBaseAPIDelete(handle);
return str;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright @ 2010 Quan Nguyen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sismics.tess4j;
public class TesseractException extends Exception {
private static final long serialVersionUID = 1L;
public TesseractException() {
super();
}
public TesseractException(String message) {
super(message);
}
public TesseractException(Throwable cause) {
super(cause);
}
public TesseractException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,16 +1,17 @@
package com.sismics.util;
import com.sismics.util.mime.MimeType;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import javax.imageio.stream.ImageOutputStream;
import com.sismics.util.mime.MimeType;
/**
* Image processing utilities.
@@ -23,26 +24,26 @@ public class ImageUtil {
* Write a high quality JPEG.
*
* @param image
* @param file
* @param outputStream Output stream
* @throws IOException
*/
public static void writeJpeg(BufferedImage image, File file) throws IOException {
public static void writeJpeg(BufferedImage image, OutputStream outputStream) throws IOException {
Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName("jpeg");
ImageWriter writer = null;
FileImageOutputStream output = null;
ImageOutputStream imageOutputStream = null;
try {
writer = (ImageWriter) iter.next();
ImageWriteParam iwp = writer.getDefaultWriteParam();
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionQuality(1.f);
output = new FileImageOutputStream(file);
writer.setOutput(output);
imageOutputStream = ImageIO.createImageOutputStream(outputStream);
writer.setOutput(imageOutputStream);
IIOImage iioImage = new IIOImage(image, null, null);
writer.write(null, iioImage, iwp);
} finally {
if (output != null) {
if (imageOutputStream != null) {
try {
output.close();
imageOutputStream.close();
} catch (Exception inner) {
// NOP
}

View File

@@ -54,28 +54,33 @@ public class ResourceUtil {
// Extract the JAR path
String jarPath = dirUrl.getPath().substring(5, dirUrl.getPath().indexOf("!"));
JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
Enumeration<JarEntry> entries = jar.entries();
Set<String> fileSet = new HashSet<String>();
while (entries.hasMoreElements()) {
// Filter according to the path
String entryName = entries.nextElement().getName();
if (!entryName.startsWith(path)) {
continue;
}
String name = entryName.substring(path.length());
if (!"".equals(name)) {
// If it is a subdirectory, just return the directory name
int checkSubdir = name.indexOf("/");
if (checkSubdir >= 0) {
name = name.substring(0, checkSubdir);
}
if (filter == null || filter.accept(null, name)) {
fileSet.add(name);
try {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
// Filter according to the path
String entryName = entries.nextElement().getName();
if (!entryName.startsWith(path)) {
continue;
}
String name = entryName.substring(path.length());
if (!"".equals(name)) {
// If it is a subdirectory, just return the directory name
int checkSubdir = name.indexOf("/");
if (checkSubdir >= 0) {
name = name.substring(0, checkSubdir);
}
if (filter == null || filter.accept(null, name)) {
fileSet.add(name);
}
}
}
} finally {
jar.close();
}
return Lists.newArrayList(fileSet);
}

View File

@@ -44,7 +44,6 @@ public class MemoryAppender extends AppenderSkeleton {
@Override
public synchronized void append(LoggingEvent event) {
// TODO Don't use size()
while (logQueue.size() > size) {
logQueue.remove();
}

View File

@@ -1,6 +1,7 @@
package com.sismics.util.mime;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
/**
* Utility to check MIME types.
@@ -24,6 +25,18 @@ public class MimeTypeUtil {
if (readCount <= 0) {
throw new Exception("Cannot read input file");
}
return guessMimeType(headerBytes);
}
/**
* Try to guess the MIME type of a file by its magic number (header).
*
* @param headerBytes File header (first bytes)
* @return MIME type
* @throws UnsupportedEncodingException
*/
public static String guessMimeType(byte[] headerBytes) throws UnsupportedEncodingException {
String header = new String(headerBytes, "US-ASCII");
if (header.startsWith("PK")) {

View File

@@ -1 +1 @@
db.version=5
db.version=6

View File

@@ -0,0 +1,3 @@
alter table T_USER add column USE_PRIVATEKEY_C varchar(100) default '' not null;
update T_USER set USE_PRIVATEKEY_C = 'AdminPk' where USE_ID_C = 'admin';
update T_CONFIG set CFG_VALUE_C='6' where CFG_ID_C='DB_VERSION';

View File

@@ -21,6 +21,7 @@ public class TestJpa extends BaseTransactionalTest {
user.setEmail("toto@docs.com");
user.setLocaleId("fr");
user.setRoleId("admin");
user.setPrivateKey("AwesomePrivateKey");
String id = userDao.create(user);
TransactionUtil.commit();

View File

@@ -0,0 +1,60 @@
package com.sismics.docs.core.util;
import java.io.InputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import junit.framework.Assert;
import org.junit.Test;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
/**
* Test of the encryption utilities.
*
* @author bgamard
*/
public class TestEncryptUtil {
/**
* Test private key.
*/
String pk = "OnceUponATime";
@Test
public void generatePrivateKeyTest() throws Exception {
String key = EncryptionUtil.generatePrivateKey();
System.out.println(key);
Assert.assertFalse(Strings.isNullOrEmpty(key));
}
@Test
public void encryptStreamTest() throws Exception {
try {
EncryptionUtil.getEncryptionCipher("");
Assert.fail();
} catch (IllegalArgumentException e) {
// NOP
}
Cipher cipher = EncryptionUtil.getEncryptionCipher(pk);
InputStream inputStream = new CipherInputStream(this.getClass().getResourceAsStream("/file/udhr.pdf"), cipher);
byte[] encryptedData = ByteStreams.toByteArray(inputStream);
byte[] assertData = ByteStreams.toByteArray(this.getClass().getResourceAsStream("/file/udhr_encrypted.pdf"));
Assert.assertTrue(ByteStreams.equal(
ByteStreams.newInputStreamSupplier(encryptedData),
ByteStreams.newInputStreamSupplier(assertData)));
}
@Test
public void decryptStreamTest() throws Exception {
InputStream inputStream = EncryptionUtil.decryptInputStream(this.getClass().getResourceAsStream("/file/udhr_encrypted.pdf"), pk);
byte[] encryptedData = ByteStreams.toByteArray(inputStream);
byte[] assertData = ByteStreams.toByteArray(this.getClass().getResourceAsStream("/file/udhr.pdf"));
Assert.assertTrue(ByteStreams.equal(
ByteStreams.newInputStreamSupplier(encryptedData),
ByteStreams.newInputStreamSupplier(assertData)));
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
- Automatic backup system using Quartz (server)

Binary file not shown.

View File

@@ -63,6 +63,7 @@
<org.vafer.jdeb.version>1.0.1</org.vafer.jdeb.version>
<com.samaxes.maven.minify-maven-plugin.version>1.7</com.samaxes.maven.minify-maven-plugin.version>
<org.apache.pdfbox.pdfbox.version>1.8.2</org.apache.pdfbox.pdfbox.version>
<org.bouncycastle.bcprov-jdk15on.version>1.49</org.bouncycastle.bcprov-jdk15on.version>
</properties>
<scm>
@@ -443,6 +444,12 @@
<version>${org.apache.pdfbox.pdfbox.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${org.bouncycastle.bcprov-jdk15on.version}</version>
</dependency>
<!-- OCR dependencies -->
<dependency>
<groupId>jna</groupId>
@@ -456,11 +463,6 @@
<version>1.0</version>
</dependency>
<dependency>
<groupId>tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -524,23 +526,6 @@
</goals>
</execution>
<execution>
<id>install-tess4j</id>
<phase>validate</phase>
<configuration>
<file>${project.basedir}/lib/tess4j.jar</file>
<repositoryLayout>default</repositoryLayout>
<groupId>tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<generatePom>true</generatePom>
</configuration>
<goals>
<goal>install-file</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>

View File

@@ -102,7 +102,6 @@ public class ValidationUtil {
* @throws JSONException
*/
public static void validateHexColor(String s, String name, boolean nullable) throws JSONException {
// TODO Do a real check
ValidationUtil.validateLength(s, "name", 7, 7, nullable);
}

View File

@@ -30,6 +30,7 @@ public class JerseyTestWebAppDescriptorFactory {
.addFilter(TokenBasedSecurityFilter.class, "tokenBasedSecurityFilter")
.initParam("com.sun.jersey.spi.container.ContainerRequestFilters", "com.sun.jersey.api.container.filter.LoggingFilter")
.initParam("com.sun.jersey.spi.container.ContainerResponseFilters", "com.sun.jersey.api.container.filter.LoggingFilter")
.initParam("com.sun.jersey.config.feature.logging.DisableEntitylogging", "true")
.build();
}
}

View File

@@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=5
db.version=6

View File

@@ -1,7 +1,5 @@
package com.sismics.docs.rest.resource;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -26,12 +24,10 @@ 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.event.ExtractFileAsyncEvent;
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.FileUtil;
import com.sismics.docs.core.util.jpa.PaginatedList;
import com.sismics.docs.core.util.jpa.PaginatedLists;
import com.sismics.docs.core.util.jpa.SortCriteria;
@@ -113,7 +109,7 @@ public class AppResource extends BaseResource {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// TODO Change level by minLevel (returns all logs above)
// Get the memory appender
Logger logger = Logger.getRootLogger();
@@ -147,29 +143,6 @@ public class AppResource extends BaseResource {
return Response.ok().entity(response).build();
}
/**
* Extract content from all files again.
*
* @return Response
* @throws JSONException
*/
@POST
@Path("batch/extract")
@Produces(MediaType.APPLICATION_JSON)
public Response batchExtract() throws JSONException {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Raise an extract file content event
AppContext.getInstance().getAsyncEventBus().post(new ExtractFileAsyncEvent());
JSONObject response = new JSONObject();
response.put("status", "ok");
return Response.ok().entity(response).build();
}
/**
* Destroy and rebuild Lucene index.
*
@@ -197,7 +170,7 @@ public class AppResource extends BaseResource {
}
/**
* Destroy and rebuild Lucene index.
* Clean storage.
*
* @return Response
* @throws JSONException
@@ -233,38 +206,4 @@ public class AppResource extends BaseResource {
response.put("status", "ok");
return Response.ok().entity(response).build();
}
/**
* Regenerate file variations.
*
* @return Response
* @throws JSONException
*/
@POST
@Path("batch/file_variations")
@Produces(MediaType.APPLICATION_JSON)
public Response batchFileVariations() throws JSONException {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Get all files
FileDao fileDao = new FileDao();
List<File> fileList = fileDao.findAll();
// Generate variations for each file
for (File file : fileList) {
java.io.File originalFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId()).toFile();
try {
FileUtil.saveVariations(file, originalFile);
} catch (IOException e) {
throw new ServerException("FileError", "Error generating file variations", e);
}
}
JSONObject response = new JSONObject();
response.put("status", "ok");
return Response.ok().entity(response).build();
}
}

View File

@@ -34,6 +34,7 @@ import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.DocumentDao;
import com.sismics.docs.core.dao.jpa.FileDao;
import com.sismics.docs.core.dao.jpa.ShareDao;
import com.sismics.docs.core.dao.jpa.TagDao;
import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria;
@@ -42,8 +43,10 @@ import com.sismics.docs.core.dao.jpa.dto.TagDto;
import com.sismics.docs.core.event.DocumentCreatedAsyncEvent;
import com.sismics.docs.core.event.DocumentDeletedAsyncEvent;
import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent;
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.Share;
import com.sismics.docs.core.model.jpa.Tag;
import com.sismics.docs.core.util.jpa.PaginatedList;
@@ -168,6 +171,7 @@ public class DocumentResource extends BaseResource {
document.put("create_date", documentDto.getCreateTimestamp());
document.put("shared", documentDto.getShared());
document.put("language", documentDto.getLanguage());
document.put("file_count", documentDto.getFileCount());
// Get tags
List<TagDto> tagDtoList = tagDao.getByDocumentId(documentDto.getId());
@@ -243,7 +247,9 @@ public class DocumentResource extends BaseResource {
if (params[0].equals("before")) documentCriteria.setCreateDateMax(date.toDate());
else documentCriteria.setCreateDateMin(date.toDate());
} catch (IllegalArgumentException e) {
// NOP
// Invalid date, returns no documents
if (params[0].equals("before")) documentCriteria.setCreateDateMax(new Date(0));
else documentCriteria.setCreateDateMin(new Date(Long.MAX_VALUE / 2));
}
} else if (params[0].equals("at")) {
// New specific date criteria
@@ -262,7 +268,9 @@ public class DocumentResource extends BaseResource {
documentCriteria.setCreateDateMax(date.plusYears(1).minusSeconds(1).toDate());
}
} catch (IllegalArgumentException e) {
// NOP
// Invalid date, returns no documents
documentCriteria.setCreateDateMin(new Date(0));
documentCriteria.setCreateDateMax(new Date(0));
}
} else if (params[0].equals("shared")) {
// New shared state criteria
@@ -292,6 +300,9 @@ public class DocumentResource extends BaseResource {
*
* @param title Title
* @param description Description
* @param tags Tags
* @param language Language
* @param createDateStr Creation date
* @return Response
* @throws JSONException
*/
@@ -455,21 +466,31 @@ public class DocumentResource extends BaseResource {
// Get the document
DocumentDao documentDao = new DocumentDao();
FileDao fileDao = new FileDao();
Document document;
List<File> fileList;
try {
document = documentDao.getDocument(id, principal.getId());
fileList = fileDao.getByDocumentId(id);
} catch (NoResultException e) {
throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", id));
}
// Delete the document
documentDao.delete(document.getId());
// Raise file deleted events
for (File file : fileList) {
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
fileDeletedAsyncEvent.setFile(file);
AppContext.getInstance().getAsyncEventBus().post(fileDeletedAsyncEvent);
}
// Raise a document deleted event
DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent();
documentDeletedAsyncEvent.setDocument(document);
AppContext.getInstance().getAsyncEventBus().post(documentDeletedAsyncEvent);
// Delete the document
documentDao.delete(document.getId());
// Always return ok
JSONObject response = new JSONObject();
response.put("status", "ok");

View File

@@ -1,7 +1,10 @@
package com.sismics.docs.rest.resource;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
@@ -20,22 +23,28 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.sismics.docs.core.dao.jpa.DocumentDao;
import com.sismics.docs.core.dao.jpa.FileDao;
import com.sismics.docs.core.dao.jpa.ShareDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.event.FileCreatedAsyncEvent;
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.core.util.EncryptionUtil;
import com.sismics.docs.core.util.FileUtil;
import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException;
@@ -77,20 +86,30 @@ public class FileResource extends BaseResource {
// Get the document
DocumentDao documentDao = new DocumentDao();
FileDao fileDao = new FileDao();
UserDao userDao = new UserDao();
Document document;
User user;
try {
document = documentDao.getDocument(documentId, principal.getId());
user = userDao.getById(principal.getId());
} catch (NoResultException e) {
throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId));
}
FileDao fileDao = new FileDao();
// Keep unencrypted data in memory, because we will need it two times
byte[] fileData;
try {
fileData = ByteStreams.toByteArray(fileBodyPart.getValueAs(InputStream.class));
} catch (IOException e) {
throw new ServerException("StreamError", "Error reading the input file", e);
}
InputStream fileInputStream = new ByteArrayInputStream(fileData);
// Validate mime type
InputStream is = new BufferedInputStream(fileBodyPart.getValueAs(InputStream.class));
String mimeType;
try {
mimeType = MimeTypeUtil.guessMimeType(is);
mimeType = MimeTypeUtil.guessMimeType(fileInputStream);
} catch (Exception e) {
throw new ServerException("ErrorGuessMime", "Error guessing mime type", e);
}
@@ -113,12 +132,13 @@ public class FileResource extends BaseResource {
String fileId = fileDao.create(file);
// Save the file
FileUtil.save(is, file);
FileUtil.save(fileInputStream, file, user.getPrivateKey());
// Raise a new file created event
FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent();
fileCreatedAsyncEvent.setDocument(document);
fileCreatedAsyncEvent.setFile(file);
fileCreatedAsyncEvent.setInputStream(fileInputStream);
AppContext.getInstance().getAsyncEventBus().post(fileCreatedAsyncEvent);
// Always return ok
@@ -249,14 +269,14 @@ public class FileResource extends BaseResource {
throw new ClientException("FileNotFound", MessageFormat.format("File not found: {0}", id));
}
// Delete the file
fileDao.delete(file.getId());
// Raise a new file deleted event
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
fileDeletedAsyncEvent.setFile(file);
AppContext.getInstance().getAsyncEventBus().post(fileDeletedAsyncEvent);
// Delete the file
fileDao.delete(file.getId());
// Always return ok
JSONObject response = new JSONObject();
response.put("status", "ok");
@@ -288,10 +308,12 @@ public class FileResource extends BaseResource {
// Get the file
FileDao fileDao = new FileDao();
DocumentDao documentDao = new DocumentDao();
UserDao userDao = new UserDao();
File file;
Document document;
try {
file = fileDao.getFile(fileId);
Document document = documentDao.getDocument(file.getDocumentId());
document = documentDao.getDocument(file.getDocumentId());
// Check document visibility
ShareDao shareDao = new ShareDao();
@@ -306,21 +328,48 @@ public class FileResource extends BaseResource {
// Get the stored file
java.io.File storedfile;
String mimeType;
boolean decrypt = false;
if (size != null) {
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId + "_" + size).toFile();
mimeType = MimeType.IMAGE_JPEG; // Thumbnails are JPEG
decrypt = true; // Thumbnails are encrypted
if (!storedfile.exists()) {
storedfile = new java.io.File(getClass().getResource("/image/file.png").getFile());
mimeType = MimeType.IMAGE_PNG;
decrypt = false;
}
} else {
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId).toFile();
mimeType = file.getMimeType();
decrypt = true; // Original files are encrypted
}
return Response.ok(storedfile)
// Stream the output and decrypt it if necessary
StreamingOutput stream;
User user = userDao.getById(document.getUserId());
try {
InputStream fileInputStream = new FileInputStream(storedfile);
final InputStream responseInputStream = decrypt ?
EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey()) : fileInputStream;
stream = new StreamingOutput() {
@Override
public void write(OutputStream outputStream) throws IOException, WebApplicationException {
try {
ByteStreams.copy(responseInputStream, outputStream);
} finally {
responseInputStream.close();
outputStream.close();
}
}
};
} catch (Exception e) {
throw new ServerException("FileError", "Error while reading the file", e);
}
return Response.ok(stream)
.header("Content-Type", mimeType)
.header("Expires", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(new Date().getTime() + 3600000 * 24 * 7))
.header("Expires", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(new Date().getTime() + 3600000 * 24))
.build();
}
}

View File

@@ -7,6 +7,7 @@ import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.dao.jpa.dto.UserDto;
import com.sismics.docs.core.model.jpa.AuthenticationToken;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.util.EncryptionUtil;
import com.sismics.docs.core.util.jpa.PaginatedList;
import com.sismics.docs.core.util.jpa.PaginatedLists;
import com.sismics.docs.core.util.jpa.SortCriteria;
@@ -28,6 +29,8 @@ import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -76,6 +79,11 @@ public class UserResource extends BaseResource {
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
try {
user.setPrivateKey(EncryptionUtil.generatePrivateKey());
} catch (NoSuchAlgorithmException e) {
throw new ServerException("PrivateKeyError", "Error while generating a private key", e);
}
user.setCreateDate(new Date());
if (localeId == null) {

View File

@@ -1 +1,2 @@
/sismicsdocs
/.idea

View File

@@ -219,4 +219,5 @@ var App = angular.module('docs',
.run(function($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$rootScope.pageTitle = 'Sismics Docs';
});

View File

@@ -3,7 +3,7 @@
/**
* Document edition controller.
*/
App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams, Restangular, Tag) {
App.controller('DocumentEdit', function($rootScope, $scope, $q, $http, $state, $stateParams, Restangular, Tag) {
// Alerts
$scope.alerts = [];
@@ -39,18 +39,15 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
};
/**
* In edit mode, load the current document.
* Reset the form to add a new document.
*/
if ($scope.isEdit()) {
Restangular.one('document', $stateParams.id).get().then(function(data) {
$scope.document = data;
});
} else {
$scope.resetForm = function() {
$scope.document = {
tags: [],
language: 'fra'
};
}
$scope.newFiles = [];
};
/**
* Edit a document.
@@ -84,16 +81,17 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
// When all files upload are over, move on
var navigateNext = function() {
if ($scope.isEdit()) {
// Go back to the edited document
$scope.pageDocuments();
$state.transitionTo('document.view', { id: $stateParams.id });
} else {
// Reset the scope and stay here
var fileUploadCount = _.size($scope.newFiles);
$scope.alerts.unshift({
type: 'success',
msg: 'Document successfully added (with ' + fileUploadCount + ' file' + (fileUploadCount > 1 ? 's' : '') + ')'
});
$scope.document = { tags: [] };
$scope.newFiles = [];
$scope.resetForm();
$scope.loadDocuments();
}
}
@@ -102,6 +100,7 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
navigateNext();
} else {
$scope.fileIsUploading = true;
$rootScope.pageTitle = '0% - Sismics Docs';
// Send a file from the input file array and return a promise
var sendFile = function(key) {
@@ -122,6 +121,7 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
promiseFile.then(function() {
$scope.fileProgress += 100 / _.size($scope.newFiles);
$rootScope.pageTitle = Math.round($scope.fileProgress) + '% - Sismics Docs';
});
return promiseFile;
@@ -136,6 +136,7 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
} else {
$scope.fileIsUploading = false;
$scope.fileProgress = 0;
$rootScope.pageTitle = 'Sismics Docs';
navigateNext();
}
};
@@ -154,4 +155,15 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
$state.transitionTo('document.default');
}
};
/**
* In edit mode, load the current document.
*/
if ($scope.isEdit()) {
Restangular.one('document', $stateParams.id).get().then(function(data) {
$scope.document = data;
});
} else {
$scope.resetForm();
}
});

View File

@@ -6,6 +6,7 @@
App.controller('FileView', function($dialog, $state, $stateParams) {
var dialog = $dialog.dialog({
keyboard: true,
dialogClass: 'modal modal-fileview',
templateUrl: 'partial/docs/file.view.html',
controller: function($scope, $state, $stateParams, Restangular, dialog) {
// Load files

View File

@@ -6,6 +6,42 @@
App.controller('Navigation', function($scope, $http, $state, $rootScope, User, Restangular) {
$rootScope.userInfo = User.userInfo();
// Last time when the errors logs was checked
$scope.lastLogCheck = new Date().getTime();
// Number of errors logs
$scope.errorNumber = 0;
// Check repeatedly if there is a new error log
setInterval(function() {
$scope.$apply(function() {
Restangular.one('app/log').get({
// Error count will be wrong if there is more than 10 errors in 60 seconds
limit: 10,
level: 'ERROR'
}).then(function(data) {
// Add new errors
$scope.errorNumber += _.reduce(data.logs, function(number, log) {
if (log.date > $scope.lastLogCheck) {
return ++number; // It's a new error
}
return number; // Not a new error
}, 0);
// Update last check timestamp
$scope.lastLogCheck = new Date().getTime();
});
})
}, 60000);
/**
* Navigate to error logs.
*/
$scope.openLogs = function() {
$scope.errorNumber = 0;
$state.transitionTo('settings.log');
};
/**
* User logout.
*/
@@ -17,6 +53,9 @@ App.controller('Navigation', function($scope, $http, $state, $rootScope, User, R
$event.preventDefault();
};
/**
* Returns true if at least an asynchronous request is in progress.
*/
$scope.isLoading = function() {
return $http.pendingRequests.length > 0;
};

View File

@@ -38,7 +38,6 @@ App.controller('Tag', function($scope, $dialog, $state, Tag, Restangular) {
* Add a tag.
*/
$scope.addTag = function() {
// TODO Check if the tag don't already exists
Restangular.one('tag').put($scope.tag).then(function(data) {
$scope.tags.push({ id: data.id, name: $scope.tag.name, color: $scope.tag.color });
$scope.tag = { name: '', color: '#3a87ad' };

View File

@@ -10,7 +10,8 @@ App.directive('selectTag', function() {
replace: true,
scope: {
tags: '=',
ref: '@'
ref: '@',
ngDisabled: '='
},
controller: function($scope, Tag) {
// Retrieve tags

View File

@@ -6,6 +6,7 @@
App.controller('FileView', function($dialog, $state, $stateParams) {
var dialog = $dialog.dialog({
keyboard: true,
dialogClass: 'modal modal-fileview',
templateUrl: 'partial/share/file.view.html',
controller: function($scope, $state, $stateParams, Restangular, dialog) {
// Load files

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html ng-app="docs">
<head>
<title>Sismics Docs</title>
<title ng-bind-template="{{ pageTitle }}">Sismics Docs</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="favicon.png" />
@@ -60,7 +60,7 @@
<div class="navbar" ng-controller="Navigation">
<div class="navbar-inner">
<div class="container">
<div class="brand loader" ng-class="{hide: !isLoading() }">
<div class="brand loader hidden-phone" ng-class="{hide: !isLoading() }">
<img src="img/loader.gif" />
</div>
@@ -72,6 +72,11 @@
</ul>
<ul class="nav pull-right" ng-show="!userInfo.anonymous">
<li ng-show="errorNumber > 0">
<a href="#/settings/log" ng-click="openLogs()" class="nav-text-error">
<span class="icon-warning-sign"></span> {{ errorNumber }} new error{{ errorNumber > 1 ? 's' : '' }}
</a>
</li>
<li ng-class="{active: $uiRoute}" ui-route="/settings.*"><a href="#/settings/account"><span class="icon-cog"></span><span class="hidden-phone"> Settings</span></a></li>
<li><a href="#" ng-click="logout($event)"><span class="icon-off"></span><span class="hidden-phone"> Logout</span></a></li>
</ul>

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
<div>
<ul class="inline">
<li ng-repeat="tag in tags"><span class="label label-info" ng-style="{ 'background': tag.color }">{{ tag.name }} <span class="icon-remove icon-white" ng-click="deleteTag(tag)"></span></span></li>
<li ng-repeat="tag in tags"><span class="label label-info" ng-style="{ 'background': tag.color }">{{ tag.name }} <span class="icon-remove icon-white" ng-click="deleteTag(tag)" ng-show="!ngDisabled"></span></span></li>
</ul>
<input class="span12" type="text" id="{{ ref }}" placeholder="Type a tag" ng-model="input"
<input class="span12" type="text" id="{{ ref }}" placeholder="Type a tag" ng-model="input" ng-disabled="ngDisabled"
autocomplete="off" typeahead="tag.name for tag in allTags | filter: $viewValue" typeahead-on-select="addTag()" />
</div>

View File

@@ -1,56 +1,63 @@
<form class="form-horizontal" name="documentForm">
<div class="control-group" ng-class="{ error: !documentForm.title.$valid }">
<label class="control-label" for="inputTitle">Title</label>
<div class="controls">
<input required ng-maxlength="100" class="input-block-level" type="text" id="inputTitle"
placeholder="Title" name="title" ng-model="document.title" autocomplete="off"
typeahead="document for document in getTitleTypeahead($viewValue) | filter: $viewValue"
typeahead-wait-ms="200" />
</div>
</div>
<div class="control-group" ng-class="{ error: !documentForm.description.$valid }">
<label class="control-label" for="inputDescription">Description</label>
<div class="controls">
<textarea ng-maxlength="4000" class="input-block-level" rows="5" id="inputDescription" name="description" ng-model="document.description"></textarea>
</div>
</div>
<div class="control-group">
<label class="control-label" for="inputCreateDate">Creation date</label>
<div class="controls">
<input type="text" id="inputCreateDate" ng-readonly="true" datepicker-popup="yyyy-MM-dd" ng-model="document.create_date" starting-day="1" show-weeks="false" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="inputCreateDate">Language</label>
<div class="controls">
<select id="inputLanguage" ng-model="document.language">
<option value="fra">French</option>
<option value="eng">English</option>
<option value="jpn">Japanese</option>
</select>
</div>
</div>
<div class="control-group">
<label class="control-label" for="inputFiles">New files</label>
<div class="controls">
<file class="input-block-level" id="inputFiles" multiple="multiple" ng-model="newFiles" accept="image/png,image/jpg,image/jpeg,image/gif,application/pdf" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="inputTags">Tags</label>
<div class="controls">
<select-tag tags="document.tags" class="input-block-level" ref="inputTags" />
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" ng-disabled="!documentForm.$valid || fileIsUploading" ng-click="edit()">{{ isEdit() ? 'Edit' : 'Add' }}</button>
<button type="submit" class="btn" ng-click="cancel()">Cancel</button>
</div>
</form>
<img src="img/loader.gif" ng-show="!document && isEdit()" />
<div class="row-fluid" ng-show="fileIsUploading">
<h4>Uploading files...</h4>
<div class="span6"><progress percent="fileProgress" class="progress-info active"></progress></div>
<div ng-show="document || !isEdit()">
<form class="form-horizontal" name="documentForm">
<div class="control-group" ng-class="{ error: !documentForm.title.$valid }">
<label class="control-label" for="inputTitle">Title</label>
<div class="controls">
<input required ng-maxlength="100" class="input-block-level" type="text" id="inputTitle"
placeholder="Title" name="title" ng-model="document.title" autocomplete="off"
typeahead="document for document in getTitleTypeahead($viewValue) | filter: $viewValue"
typeahead-wait-ms="200" ng-disabled="fileIsUploading" />
</div>
</div>
<div class="control-group" ng-class="{ error: !documentForm.description.$valid }">
<label class="control-label" for="inputDescription">Description</label>
<div class="controls">
<textarea ng-maxlength="4000" class="input-block-level" rows="5" id="inputDescription"
name="description" ng-model="document.description" ng-disabled="fileIsUploading"></textarea>
</div>
</div>
<div class="control-group">
<label class="control-label" for="inputCreateDate">Creation date</label>
<div class="controls">
<input type="text" id="inputCreateDate" ng-readonly="true" datepicker-popup="yyyy-MM-dd"
ng-model="document.create_date" starting-day="1" show-weeks="false" ng-disabled="fileIsUploading" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="inputCreateDate">Language</label>
<div class="controls">
<select id="inputLanguage" ng-model="document.language" ng-disabled="fileIsUploading">
<option value="fra">French</option>
<option value="eng">English</option>
<option value="jpn">Japanese</option>
</select>
</div>
</div>
<div class="control-group">
<label class="control-label" for="inputFiles">New files</label>
<div class="controls">
<file class="input-block-level" id="inputFiles" multiple="multiple" ng-model="newFiles"
accept="image/png,image/jpg,image/jpeg,image/gif,application/pdf" ng-disabled="fileIsUploading" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="inputTags">Tags</label>
<div class="controls">
<select-tag tags="document.tags" class="input-block-level" ref="inputTags" ng-disabled="fileIsUploading" />
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" ng-disabled="!documentForm.$valid || fileIsUploading" ng-click="edit()">{{ isEdit() ? 'Edit' : 'Add' }}</button>
<button type="submit" class="btn" ng-click="cancel()" ng-disabled="fileIsUploading">Cancel</button>
</div>
</form>
<div class="row-fluid" ng-show="fileIsUploading">
<h4>Uploading files...</h4>
<div class="span6"><progress percent="fileProgress" class="progress-info active"></progress></div>
</div>
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
</div>
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>

View File

@@ -30,7 +30,7 @@
</thead>
<tbody>
<tr ng-click="viewDocument(document.id)" ng-repeat="document in documents">
<td>{{ document.title }} <span class="icon-share" ng-if="document.shared" tooltip="Shared"></span></td>
<td>{{ document.title }} ({{ document.file_count }})<span class="icon-share" ng-if="document.shared" tooltip="Shared"></span></td>
<td>{{ document.create_date | date: 'yyyy-MM-dd' }}</td>
<td class="hidden-phone cell-tags">
<div class="tags">

View File

@@ -1,6 +1,6 @@
<img src="img/loader.gif" ng-if="!document" />
<img src="img/loader.gif" ng-show="!document" />
<div ng-if="document">
<div ng-show="document">
<div class="text-right">
<div class="btn-group">
<button class="btn btn-danger" ng-click="deleteDocument(document)"><span class="icon-trash icon-white"></span> Delete</button>
@@ -9,7 +9,7 @@
</div>
<div class="page-header">
<h1>{{ document.title }} <small>{{ document.create_date | date: 'yyyy-MM-dd' }}</small> <img ng-src="img/flag/{{ document.language }}.png" title="{{ document.language }}" /></h1>
<h1>{{ document.title }} <small>{{ document.create_date | date: 'yyyy-MM-dd' }}</small> <img ng-if="document" ng-src="img/flag/{{ document.language }}.png" title="{{ document.language }}" /></h1>
<p>
<button class="btn btn-small btn-inverse" ng-click="share()"><span class="icon-share icon-white"></span> Share</button>
<button class="btn btn-small" ng-repeat="share in document.shares" ng-click="showShare(share)"><span class="icon-ok"></span> {{ share.name ? share.name : 'shared' }}</button>

View File

@@ -1,62 +1,66 @@
<h2 ng-show="isEdit()">Edit
<small>"{{ user.username }}"</small>
</h2>
<h2 ng-show="!isEdit()">Add
<small>user</small>
</h2>
<form class="form-horizontal" name="editUserForm" novalidate>
<div class="control-group" ng-class="{ error: !editUserForm.username.$valid, success: editUserForm.username.$valid }">
<label class="control-label" for="inputUsername">Username</label>
<img src="img/loader.gif" ng-show="!user && isEdit()" />
<div class="controls">
<input name="username" type="text" id="inputUsername" required ng-disabled="isEdit()"
ng-minlength="3" ng-maxlength="50" placeholder="Username" ng-model="user.username"/>
<span class="help-inline" ng-show="editUserForm.username.$error.required">Required</span>
<span class="help-inline" ng-show="editUserForm.username.$error.minlength">Too short</span>
<span class="help-inline" ng-show="editUserForm.username.$error.maxlength">Too long</span>
</div>
</div>
<div class="control-group" ng-class="{ error: !editUserForm.email.$valid, success: editUserForm.email.$valid }">
<label class="control-label" for="inputEmail">E-mail</label>
<div ng-show="user || !isEdit()">
<h2 ng-show="isEdit()">Edit
<small>"{{ user.username }}"</small>
</h2>
<h2 ng-show="!isEdit()">Add
<small>user</small>
</h2>
<form class="form-horizontal" name="editUserForm" novalidate>
<div class="control-group" ng-class="{ error: !editUserForm.username.$valid, success: editUserForm.username.$valid }">
<label class="control-label" for="inputUsername">Username</label>
<div class="controls">
<input name="email" type="email" id="inputEmail" required
ng-minlength="3" ng-maxlength="50" placeholder="E-mail" ng-model="user.email"/>
<span class="help-inline" ng-show="editUserForm.email.$error.required">Required</span>
<span class="help-inline" ng-show="editUserForm.email.$error.email">Must be a valid e-mail</span>
<span class="help-inline" ng-show="editUserForm.email.$error.minlength">Too short</span>
<span class="help-inline" ng-show="editUserForm.email.$error.maxlength">Too long</span>
<div class="controls">
<input name="username" type="text" id="inputUsername" required ng-disabled="isEdit()"
ng-minlength="3" ng-maxlength="50" placeholder="Username" ng-model="user.username"/>
<span class="help-inline" ng-show="editUserForm.username.$error.required">Required</span>
<span class="help-inline" ng-show="editUserForm.username.$error.minlength">Too short</span>
<span class="help-inline" ng-show="editUserForm.username.$error.maxlength">Too long</span>
</div>
</div>
</div>
<div class="control-group" ng-class="{ error: !editUserForm.password.$valid, success: editUserForm.password.$valid }">
<label class="control-label" for="inputPassword">Password</label>
<div class="control-group" ng-class="{ error: !editUserForm.email.$valid, success: editUserForm.email.$valid }">
<label class="control-label" for="inputEmail">E-mail</label>
<div class="controls">
<input name="password" type="password" id="inputPassword" ng-required="!isEdit()"
ng-minlength="8" ng-maxlength="50" placeholder="Password" ng-model="user.password"/>
<span class="help-inline" ng-show="editUserForm.password.$error.required">Required</span>
<span class="help-inline" ng-show="editUserForm.password.$error.minlength">Too short</span>
<span class="help-inline" ng-show="editUserForm.password.$error.maxlength">Too long</span>
<div class="controls">
<input name="email" type="email" id="inputEmail" required
ng-minlength="3" ng-maxlength="50" placeholder="E-mail" ng-model="user.email"/>
<span class="help-inline" ng-show="editUserForm.email.$error.required">Required</span>
<span class="help-inline" ng-show="editUserForm.email.$error.email">Must be a valid e-mail</span>
<span class="help-inline" ng-show="editUserForm.email.$error.minlength">Too short</span>
<span class="help-inline" ng-show="editUserForm.email.$error.maxlength">Too long</span>
</div>
</div>
</div>
<div class="control-group"
ng-class="{ error: !editUserForm.passwordconfirm.$valid, success: editUserForm.passwordconfirm.$valid }">
<label class="control-label" for="inputPasswordConfirm">Password (confirm)</label>
<div class="control-group" ng-class="{ error: !editUserForm.password.$valid, success: editUserForm.password.$valid }">
<label class="control-label" for="inputPassword">Password</label>
<div class="controls">
<input name="passwordconfirm" type="password" id="inputPasswordConfirm" ng-required="!isEdit()"
ui-validate="'$value == user.password'" ui-validate-watch="'user.password'"
placeholder="Password (confirm)" ng-model="user.passwordconfirm"/>
<span class="help-inline" ng-show="editUserForm.passwordconfirm.$error.validator">Password and password confirmation must match</span>
<div class="controls">
<input name="password" type="password" id="inputPassword" ng-required="!isEdit()"
ng-minlength="8" ng-maxlength="50" placeholder="Password" ng-model="user.password"/>
<span class="help-inline" ng-show="editUserForm.password.$error.required">Required</span>
<span class="help-inline" ng-show="editUserForm.password.$error.minlength">Too short</span>
<span class="help-inline" ng-show="editUserForm.password.$error.maxlength">Too long</span>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editUserForm.$valid">
<span class="icon-pencil icon-white"></span> {{ isEdit() ? 'Edit' : 'Add' }}
</button>
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit()">
<span class="icon-trash icon-white"></span> Delete
</button>
</div>
</form>
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
<div class="control-group"
ng-class="{ error: !editUserForm.passwordconfirm.$valid, success: editUserForm.passwordconfirm.$valid }">
<label class="control-label" for="inputPasswordConfirm">Password (confirm)</label>
<div class="controls">
<input name="passwordconfirm" type="password" id="inputPasswordConfirm" ng-required="!isEdit()"
ui-validate="'$value == user.password'" ui-validate-watch="'user.password'"
placeholder="Password (confirm)" ng-model="user.passwordconfirm"/>
<span class="help-inline" ng-show="editUserForm.passwordconfirm.$error.validator">Password and password confirmation must match</span>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editUserForm.$valid">
<span class="icon-pencil icon-white"></span> {{ isEdit() ? 'Edit' : 'Add' }}
</button>
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit()">
<span class="icon-trash icon-white"></span> Delete
</button>
</div>
</form>
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
</div>

View File

@@ -80,6 +80,13 @@
}
}
// File view
.modal-fileview {
top: 2%;
max-height: 96%;
overflow-y: scroll;
}
// File thumbnails
.thumbnail-file {
cursor: pointer;
@@ -134,3 +141,7 @@ input[readonly].share-link {
cursor: pointer;
}
}
.nav-text-error {
color: #b94a48 !important;
}

View File

@@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=5
db.version=6

View File

@@ -43,13 +43,6 @@ public class TestAppResource extends BaseJerseyTest {
Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory);
Assert.assertEquals(0, json.getInt("document_count"));
// OCR-ize all files
appResource = resource().path("/app/batch/extract");
appResource.addFilter(new CookieAuthenticationFilter(adminAuthenticationToken));
response = appResource.post(ClientResponse.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
json = response.getEntity(JSONObject.class);
// Rebuild Lucene index
appResource = resource().path("/app/batch/reindex");
appResource.addFilter(new CookieAuthenticationFilter(adminAuthenticationToken));
@@ -83,7 +76,7 @@ public class TestAppResource extends BaseJerseyTest {
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
JSONObject json = response.getEntity(JSONObject.class);
JSONArray logs = json.getJSONArray("logs");
Assert.assertTrue(logs.length() == 10);
Assert.assertTrue(logs.length() > 0);
Long date1 = logs.optJSONObject(0).optLong("date");
Long date2 = logs.optJSONObject(9).optLong("date");
Assert.assertTrue(date1 > date2);
@@ -99,7 +92,7 @@ public class TestAppResource extends BaseJerseyTest {
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
json = response.getEntity(JSONObject.class);
logs = json.getJSONArray("logs");
Assert.assertTrue(logs.length() == 10);
Assert.assertTrue(logs.length() > 0);
Long date3 = logs.optJSONObject(0).optLong("date");
Long date4 = logs.optJSONObject(9).optLong("date");
Assert.assertTrue(date3 > date4);

View File

@@ -3,6 +3,7 @@ package com.sismics.docs.rest;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.Date;
import javax.ws.rs.core.MediaType;
@@ -10,13 +11,15 @@ import javax.ws.rs.core.MediaType;
import junit.framework.Assert;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.joda.time.format.DateTimeFormat;
import org.junit.Test;
import com.google.common.io.ByteStreams;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.rest.filter.CookieAuthenticationFilter;
import com.sismics.util.mime.MimeType;
import com.sismics.util.mime.MimeTypeUtil;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.api.client.WebResource;
@@ -32,11 +35,10 @@ import com.sun.jersey.multipart.FormDataMultiPart;
public class TestDocumentResource extends BaseJerseyTest {
/**
* Test the document resource.
*
* @throws JSONException
* @throws Exception
*/
@Test
public void testDocumentResource() throws JSONException {
public void testDocumentResource() throws Exception {
// Login document1
clientUtil.createUser("document1");
String document1Token = clientUtil.login("document1");
@@ -82,6 +84,7 @@ public class TestDocumentResource extends BaseJerseyTest {
response = fileResource.type(MediaType.MULTIPART_FORM_DATA).put(ClientResponse.class, form);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
json = response.getEntity(JSONObject.class);
String file1Id = json.getString("id");
// Share this document
WebResource fileShareResource = resource().path("/share");
@@ -106,183 +109,36 @@ public class TestDocumentResource extends BaseJerseyTest {
Assert.assertTrue(documents.length() == 1);
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
Assert.assertEquals("eng", documents.getJSONObject(0).getString("language"));
Assert.assertEquals(1, documents.getJSONObject(0).getInt("file_count"));
Assert.assertEquals(1, tags.length());
Assert.assertEquals(tag1Id, tags.getJSONObject(0).getString("id"));
Assert.assertEquals("SuperTag", tags.getJSONObject(0).getString("name"));
Assert.assertEquals("#ffff00", tags.getJSONObject(0).getString("color"));
// Search documents by query in full content
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "full:uranium");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
// Search documents by query in full content
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "full:title");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
// Search documents by query in title/description
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "title");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
// Search documents by query in title/description
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "description");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
// Search documents by specific date
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "at:" + DateTimeFormat.forPattern("yyyy-MM").print(new Date().getTime()));
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
// Search documents by date span
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "after:2010 before:2040-08");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
// Search documents by tag
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "tag:super");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
// Search documents by shared status
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "shared:yes");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
Assert.assertEquals(true, documents.getJSONObject(0).getBoolean("shared"));
// Search documents by language
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "lang:eng");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
Assert.assertEquals("eng", documents.getJSONObject(0).getString("language"));
// Search documents with multiple criteria
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "after:2010 before:2040-08 tag:super shared:yes lang:eng title description full:uranium");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 1);
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
// Search documents
Assert.assertEquals(1, searchDocuments("full:uranium full:einstein", document1Token));
Assert.assertEquals(1, searchDocuments("full:title", document1Token));
Assert.assertEquals(1, searchDocuments("title", document1Token));
Assert.assertEquals(1, searchDocuments("super description", document1Token));
Assert.assertEquals(1, searchDocuments("at:" + DateTimeFormat.forPattern("yyyy").print(new Date().getTime()), document1Token));
Assert.assertEquals(1, searchDocuments("at:" + DateTimeFormat.forPattern("yyyy-MM").print(new Date().getTime()), document1Token));
Assert.assertEquals(1, searchDocuments("at:" + DateTimeFormat.forPattern("yyyy-MM-dd").print(new Date().getTime()), document1Token));
Assert.assertEquals(1, searchDocuments("after:2010 before:2040-08", document1Token));
Assert.assertEquals(1, searchDocuments("tag:super", document1Token));
Assert.assertEquals(1, searchDocuments("shared:yes", document1Token));
Assert.assertEquals(1, searchDocuments("lang:eng", document1Token));
Assert.assertEquals(1, searchDocuments("after:2010 before:2040-08 tag:super shared:yes lang:eng title description full:uranium", document1Token));
// Search documents (nothing)
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "random");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 0);
// Search documents (nothing)
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "full:random");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 0);
// Search documents (nothing)
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "after:2010 before:2011-05-20");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 0);
// Search documents (nothing)
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "tag:Nop");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 0);
// Search documents (nothing)
documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
getParams = new MultivaluedMapImpl();
getParams.putSingle("search", "lang:fra");
response = documentResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
documents = json.getJSONArray("documents");
Assert.assertTrue(documents.length() == 0);
Assert.assertEquals(0, searchDocuments("random", document1Token));
Assert.assertEquals(0, searchDocuments("full:random", document1Token));
Assert.assertEquals(0, searchDocuments("after:2010 before:2011-05-20", document1Token));
Assert.assertEquals(0, searchDocuments("at:2040-05-35", document1Token));
Assert.assertEquals(0, searchDocuments("after:2010-18 before:2040-05-38", document1Token));
Assert.assertEquals(0, searchDocuments("after:2010-18", document1Token));
Assert.assertEquals(0, searchDocuments("before:2040-05-38", document1Token));
Assert.assertEquals(0, searchDocuments("tag:Nop", document1Token));
Assert.assertEquals(0, searchDocuments("lang:fra", document1Token));
// Get a document
documentResource = resource().path("/document/" + document1Id);
@@ -348,6 +204,14 @@ public class TestDocumentResource extends BaseJerseyTest {
json = response.getEntity(JSONObject.class);
Assert.assertEquals("ok", json.getString("status"));
// Check that the associated files are deleted from FS
java.io.File storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile();
java.io.File webFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_web").toFile();
java.io.File thumbnailFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_thumb").toFile();
Assert.assertFalse(storedFile.exists());
Assert.assertFalse(webFile.exists());
Assert.assertFalse(thumbnailFile.exists());
// Get a document (KO)
documentResource = resource().path("/document/" + document1Id);
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
@@ -356,6 +220,25 @@ public class TestDocumentResource extends BaseJerseyTest {
Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus()));
}
/**
* Search documents and returns the number found.
*
* @param query Search query
* @param token Authentication token
* @return Number of documents found
* @throws Exception
*/
private int searchDocuments(String query, String token) throws Exception {
WebResource documentResource = resource().path("/document/list");
documentResource.addFilter(new CookieAuthenticationFilter(token));
MultivaluedMapImpl getParams = new MultivaluedMapImpl();
getParams.putSingle("search", query);
ClientResponse response = documentResource.queryParams(getParams).get(ClientResponse.class);
JSONObject json = response.getEntity(JSONObject.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
return json.getJSONArray("documents").length();
}
/**
* Test PDF extraction.
*
@@ -416,6 +299,7 @@ public class TestDocumentResource extends BaseJerseyTest {
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
InputStream is = response.getEntityInputStream();
byte[] fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(33691, fileBytes.length);
Assert.assertTrue(fileBytes.length > 0); // Images rendered from PDF differ in size from OS to OS due to font issues
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes));
}
}

View File

@@ -1,6 +1,7 @@
package com.sismics.docs.rest;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Paths;
@@ -15,6 +16,8 @@ import org.junit.Test;
import com.google.common.io.ByteStreams;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.rest.filter.CookieAuthenticationFilter;
import com.sismics.util.mime.MimeType;
import com.sismics.util.mime.MimeTypeUtil;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.api.client.WebResource;
@@ -88,6 +91,7 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
InputStream is = response.getEntityInputStream();
byte[] fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes));
Assert.assertEquals(163510, fileBytes.length);
// Get the thumbnail data
@@ -99,6 +103,7 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
is = response.getEntityInputStream();
fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes));
Assert.assertEquals(41935, fileBytes.length);
// Get the web data
@@ -110,45 +115,14 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
is = response.getEntityInputStream();
fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes));
Assert.assertEquals(551084, fileBytes.length);
// Regenerate file variations
String adminAuthenticationToken = clientUtil.login("admin", "admin", false);
WebResource appResource = resource().path("/app/batch/file_variations");
appResource.addFilter(new CookieAuthenticationFilter(adminAuthenticationToken));
response = appResource.post(ClientResponse.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
// Get the file data
fileResource = resource().path("/file/" + file1Id + "/data");
fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken));
response = fileResource.get(ClientResponse.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
is = response.getEntityInputStream();
fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(163510, fileBytes.length);
// Get the thumbnail data
fileResource = resource().path("/file/" + file1Id + "/data");
fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken));
getParams = new MultivaluedMapImpl();
getParams.putSingle("size", "thumb");
response = fileResource.queryParams(getParams).get(ClientResponse.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
is = response.getEntityInputStream();
fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(41935, fileBytes.length);
// Get the web data
fileResource = resource().path("/file/" + file1Id + "/data");
fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken));
getParams = new MultivaluedMapImpl();
getParams.putSingle("size", "web");
response = fileResource.queryParams(getParams).get(ClientResponse.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
is = response.getEntityInputStream();
fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(551084, fileBytes.length);
// Check that the files are not readable directly from FS
java.io.File storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile();
InputStream storedFileInputStream = new BufferedInputStream(new FileInputStream(storedFile));
Assert.assertNull(MimeTypeUtil.guessMimeType(storedFileInputStream));
storedFileInputStream.close();
// Get all files from a document
fileResource = resource().path("/file/list");
@@ -195,7 +169,7 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals("ok", json.getString("status"));
// Check that files are deleted from FS
java.io.File storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile();
storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile();
java.io.File webFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_web").toFile();
java.io.File thumbnailFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_thumb").toFile();
Assert.assertFalse(storedFile.exists());

View File

@@ -7,3 +7,4 @@ log4j.appender.MEMORY.size=1000
log4j.logger.com.sismics=DEBUG
log4j.logger.org.hibernate=INFO
log4j.logger.org.apache.pdfbox=INFO