mirror of
https://github.com/sismics/docs.git
synced 2025-12-15 18:56:18 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60beb0e2f5 | ||
|
|
77f0368ba5 | ||
|
|
596dd4db13 | ||
|
|
d2ba291287 | ||
|
|
01cb3e611c | ||
|
|
bd9e918a62 | ||
|
|
56f6038c09 | ||
|
|
f29ed3e671 | ||
|
|
726121d8c8 | ||
|
|
c40e7e1cc9 | ||
|
|
b1f9b072f3 | ||
|
|
22cea20a90 | ||
|
|
8eb5b8066e | ||
|
|
e37eab3b7a | ||
|
|
1db54174d4 | ||
|
|
d5fa3a4e3a | ||
|
|
fc53758eb7 | ||
|
|
f5079e83cb | ||
|
|
b399e4081f | ||
|
|
a80bc27582 | ||
|
|
eb57af4029 | ||
|
|
ac3580fb4a | ||
|
|
6e3d2ea972 | ||
|
|
870a44da0d | ||
|
|
62a5840777 | ||
|
|
a858699391 | ||
|
|
fb0aec5fee | ||
|
|
1a495ac948 | ||
|
|
ba083fb57a | ||
|
|
4e22111f38 | ||
|
|
db7a9f0e4a | ||
|
|
906de329ae | ||
|
|
00b00f0d0c | ||
|
|
0bc658a396 | ||
|
|
464d43194b | ||
|
|
1c606ebf25 | ||
|
|
504c4dd815 | ||
|
|
ecb5a7abf6 | ||
|
|
cc6d599293 |
10
README.md
10
README.md
@@ -14,10 +14,11 @@ Features
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
- Responsive user interface
|
- Responsive user interface
|
||||||
- Optical characted recognition
|
- Optical character recognition
|
||||||
- Support image and PDF files
|
- Support image and PDF files
|
||||||
- Flexible search engine
|
- Flexible search engine
|
||||||
- Full text search in image and PDF
|
- Full text search in image and PDF
|
||||||
|
- SHA-256 encryption
|
||||||
- Tag system
|
- Tag system
|
||||||
- Multi-users
|
- Multi-users
|
||||||
- Document sharing
|
- Document sharing
|
||||||
@@ -26,13 +27,13 @@ Features
|
|||||||
License
|
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>.
|
information or see <http://opensource.org/licenses/GPL-2.0>.
|
||||||
|
|
||||||
How to build Docs from the sources
|
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:
|
Docs is organized in several Maven modules:
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ or download the sources from GitHub.
|
|||||||
|
|
||||||
From the `docs-parent` directory:
|
From the `docs-parent` directory:
|
||||||
|
|
||||||
|
mvn -Pinit validate -N
|
||||||
mvn clean -DskipTests install
|
mvn clean -DskipTests install
|
||||||
|
|
||||||
#### Run a stand-alone version
|
#### Run a stand-alone version
|
||||||
|
|||||||
4
docs-android/.gitignore
vendored
Normal file
4
docs-android/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
1
docs-android/app/.gitignore
vendored
Normal file
1
docs-android/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
76
docs-android/app/app.iml
Normal file
76
docs-android/app/app.iml
Normal 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>
|
||||||
|
|
||||||
40
docs-android/app/build.gradle
Normal file
40
docs-android/app/build.gradle
Normal 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'
|
||||||
|
}
|
||||||
17
docs-android/app/proguard-rules.txt
Normal file
17
docs-android/app/proguard-rules.txt
Normal 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 *;
|
||||||
|
#}
|
||||||
29
docs-android/app/src/main/AndroidManifest.xml
Normal file
29
docs-android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs-android/app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
docs-android/app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
BIN
docs-android/app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
docs-android/app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
docs-android/app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
docs-android/app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docs-android/app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
docs-android/app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -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" />
|
||||||
10
docs-android/app/src/main/res/layout/activity_doc_list.xml
Normal file
10
docs-android/app/src/main/res/layout/activity_doc_list.xml
Normal 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" />
|
||||||
@@ -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>
|
||||||
@@ -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" />
|
||||||
10
docs-android/app/src/main/res/values-large/refs.xml
Normal file
10
docs-android/app/src/main/res/values-large/refs.xml
Normal 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>
|
||||||
10
docs-android/app/src/main/res/values-sw600dp/refs.xml
Normal file
10
docs-android/app/src/main/res/values-sw600dp/refs.xml
Normal 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>
|
||||||
7
docs-android/app/src/main/res/values/strings.xml
Normal file
7
docs-android/app/src/main/res/values/strings.xml
Normal 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>
|
||||||
8
docs-android/app/src/main/res/values/styles.xml
Normal file
8
docs-android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="android:Theme.Holo.Light">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
||||||
1
docs-android/build.gradle
Normal file
1
docs-android/build.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
18
docs-android/gradle.properties
Normal file
18
docs-android/gradle.properties
Normal 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
|
||||||
BIN
docs-android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
docs-android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
docs-android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
docs-android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
164
docs-android/gradlew
vendored
Normal 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
90
docs-android/gradlew.bat
vendored
Normal 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
|
||||||
1
docs-android/settings.gradle
Normal file
1
docs-android/settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
include ':app'
|
||||||
@@ -122,6 +122,11 @@
|
|||||||
<artifactId>pdfbox</artifactId>
|
<artifactId>pdfbox</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- OCR dependencies -->
|
<!-- OCR dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jna</groupId>
|
<groupId>jna</groupId>
|
||||||
@@ -133,11 +138,6 @@
|
|||||||
<artifactId>imageio</artifactId>
|
<artifactId>imageio</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>tess4j</groupId>
|
|
||||||
<artifactId>tess4j</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Test dependencies -->
|
<!-- Test dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
|
|||||||
@@ -138,14 +138,15 @@ public class DocumentDao {
|
|||||||
*
|
*
|
||||||
* @param paginatedList List of documents (updated by side effects)
|
* @param paginatedList List of documents (updated by side effects)
|
||||||
* @param criteria Search criteria
|
* @param criteria Search criteria
|
||||||
* @return List of document
|
* @return List of documents
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public void findByCriteria(PaginatedList<DocumentDto> paginatedList, DocumentCriteria criteria, SortCriteria sortCriteria) throws Exception {
|
public void findByCriteria(PaginatedList<DocumentDto> paginatedList, DocumentCriteria criteria, SortCriteria sortCriteria) throws Exception {
|
||||||
Map<String, Object> parameterMap = new HashMap<String, Object>();
|
Map<String, Object> parameterMap = new HashMap<String, Object>();
|
||||||
List<String> criteriaList = new ArrayList<String>();
|
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(" 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 ");
|
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.setCreateTimestamp(((Timestamp) o[i++]).getTime());
|
||||||
documentDto.setLanguage((String) o[i++]);
|
documentDto.setLanguage((String) o[i++]);
|
||||||
documentDto.setShared((Boolean) o[i++]);
|
documentDto.setShared((Boolean) o[i++]);
|
||||||
|
documentDto.setFileCount(((Number) o[i++]).intValue());
|
||||||
documentDtoList.add(documentDto);
|
documentDtoList.add(documentDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ public class DocumentDto {
|
|||||||
*/
|
*/
|
||||||
private Boolean shared;
|
private Boolean shared;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File count.
|
||||||
|
*/
|
||||||
|
private Integer fileCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter de id.
|
* Getter de id.
|
||||||
*
|
*
|
||||||
@@ -146,4 +151,20 @@ public class DocumentDto {
|
|||||||
public void setLanguage(String language) {
|
public void setLanguage(String language) {
|
||||||
this.language = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import java.util.Set;
|
|||||||
import org.apache.lucene.document.Field;
|
import org.apache.lucene.document.Field;
|
||||||
import org.apache.lucene.document.StringField;
|
import org.apache.lucene.document.StringField;
|
||||||
import org.apache.lucene.document.TextField;
|
import org.apache.lucene.document.TextField;
|
||||||
|
import org.apache.lucene.index.DirectoryReader;
|
||||||
import org.apache.lucene.index.IndexWriter;
|
import org.apache.lucene.index.IndexWriter;
|
||||||
import org.apache.lucene.index.Term;
|
import org.apache.lucene.index.Term;
|
||||||
import org.apache.lucene.queries.TermsFilter;
|
import org.apache.lucene.queries.TermsFilter;
|
||||||
@@ -171,12 +172,17 @@ public class LuceneDao {
|
|||||||
TermsFilter userFilter = new TermsFilter(terms);
|
TermsFilter userFilter = new TermsFilter(terms);
|
||||||
|
|
||||||
// Search
|
// 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);
|
TopDocs topDocs = searcher.search(query, userFilter, Integer.MAX_VALUE);
|
||||||
ScoreDoc[] docs = topDocs.scoreDocs;
|
ScoreDoc[] docs = topDocs.scoreDocs;
|
||||||
|
|
||||||
// Extract document IDs
|
// Extract document IDs
|
||||||
Set<String> documentIdList = new HashSet<String>();
|
|
||||||
for (int i = 0; i < docs.length; i++) {
|
for (int i = 0; i < docs.length; i++) {
|
||||||
org.apache.lucene.document.Document document = searcher.doc(docs[i].doc);
|
org.apache.lucene.document.Document document = searcher.doc(docs[i].doc);
|
||||||
String type = document.get("type");
|
String type = document.get("type");
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.sismics.docs.core.event;
|
package com.sismics.docs.core.event;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
import com.sismics.docs.core.model.jpa.Document;
|
import com.sismics.docs.core.model.jpa.Document;
|
||||||
import com.sismics.docs.core.model.jpa.File;
|
import com.sismics.docs.core.model.jpa.File;
|
||||||
@@ -20,6 +22,11 @@ public class FileCreatedAsyncEvent {
|
|||||||
*/
|
*/
|
||||||
private Document document;
|
private Document document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unencrypted input stream containing the file.
|
||||||
|
*/
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter of file.
|
* Getter of file.
|
||||||
*
|
*
|
||||||
@@ -56,6 +63,24 @@ public class FileCreatedAsyncEvent {
|
|||||||
this.document = document;
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return Objects.toStringHelper(this)
|
return Objects.toStringHelper(this)
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,7 @@ public class FileCreatedAsyncListener {
|
|||||||
// OCR the file
|
// OCR the file
|
||||||
final File file = fileCreatedAsyncEvent.getFile();
|
final File file = fileCreatedAsyncEvent.getFile();
|
||||||
long startTime = System.currentTimeMillis();
|
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));
|
log.info(MessageFormat.format("File content extracted in {0}ms", System.currentTimeMillis() - startTime));
|
||||||
|
|
||||||
// Store the OCR-ization result in the database
|
// Store the OCR-ization result in the database
|
||||||
|
|||||||
@@ -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.DocumentUpdatedAsyncListener;
|
||||||
import com.sismics.docs.core.listener.async.FileCreatedAsyncListener;
|
import com.sismics.docs.core.listener.async.FileCreatedAsyncListener;
|
||||||
import com.sismics.docs.core.listener.async.FileDeletedAsyncListener;
|
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.async.RebuildIndexAsyncListener;
|
||||||
import com.sismics.docs.core.listener.sync.DeadEventListener;
|
import com.sismics.docs.core.listener.sync.DeadEventListener;
|
||||||
import com.sismics.docs.core.model.jpa.Config;
|
import com.sismics.docs.core.model.jpa.Config;
|
||||||
@@ -82,7 +81,6 @@ public class AppContext {
|
|||||||
asyncEventBus.register(new DocumentUpdatedAsyncListener());
|
asyncEventBus.register(new DocumentUpdatedAsyncListener());
|
||||||
asyncEventBus.register(new DocumentDeletedAsyncListener());
|
asyncEventBus.register(new DocumentDeletedAsyncListener());
|
||||||
asyncEventBus.register(new RebuildIndexAsyncListener());
|
asyncEventBus.register(new RebuildIndexAsyncListener());
|
||||||
asyncEventBus.register(new ExtractFileAsyncListener());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class Tag {
|
|||||||
/**
|
/**
|
||||||
* Tag name.
|
* Tag name.
|
||||||
*/
|
*/
|
||||||
@Column(name = "TAG_COLOR_C", nullable = false, length = 6)
|
@Column(name = "TAG_COLOR_C", nullable = false, length = 7)
|
||||||
private String color;
|
private String color;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ public class User {
|
|||||||
@Column(name = "USE_PASSWORD_C", nullable = false, length = 100)
|
@Column(name = "USE_PASSWORD_C", nullable = false, length = 100)
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's private key.
|
||||||
|
*/
|
||||||
|
@Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100)
|
||||||
|
private String privateKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Email address.
|
* Email address.
|
||||||
*/
|
*/
|
||||||
@@ -257,6 +263,22 @@ public class User {
|
|||||||
this.deleteDate = deleteDate;
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return Objects.toStringHelper(this)
|
return Objects.toStringHelper(this)
|
||||||
|
|||||||
@@ -128,7 +128,6 @@ public class IndexingService extends AbstractScheduledService {
|
|||||||
public DirectoryReader getDirectoryReader() {
|
public DirectoryReader getDirectoryReader() {
|
||||||
if (directoryReader == null) {
|
if (directoryReader == null) {
|
||||||
if (!DirectoryReader.indexExists(directory)) {
|
if (!DirectoryReader.indexExists(directory)) {
|
||||||
log.info("Lucene directory not yet created");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
package com.sismics.docs.core.util;
|
package com.sismics.docs.core.util;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherInputStream;
|
||||||
|
import javax.crypto.CipherOutputStream;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
import net.sourceforge.tess4j.Tesseract;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.util.PDFTextStripper;
|
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.Document;
|
||||||
import com.sismics.docs.core.model.jpa.File;
|
import com.sismics.docs.core.model.jpa.File;
|
||||||
|
import com.sismics.tess4j.Tesseract;
|
||||||
import com.sismics.util.ImageUtil;
|
import com.sismics.util.ImageUtil;
|
||||||
import com.sismics.util.mime.MimeType;
|
import com.sismics.util.mime.MimeType;
|
||||||
|
|
||||||
@@ -42,36 +46,36 @@ public class FileUtil {
|
|||||||
*
|
*
|
||||||
* @param document Document linked to the file
|
* @param document Document linked to the file
|
||||||
* @param file File to extract
|
* @param file File to extract
|
||||||
|
* @param inputStream Unencrypted input stream
|
||||||
* @return Content extract
|
* @return Content extract
|
||||||
*/
|
*/
|
||||||
public static String extractContent(Document document, File file) {
|
public static String extractContent(Document document, File file, InputStream inputStream) {
|
||||||
String content = null;
|
String content = null;
|
||||||
|
|
||||||
if (ImageUtil.isImage(file.getMimeType())) {
|
if (ImageUtil.isImage(file.getMimeType())) {
|
||||||
content = ocrFile(document, file);
|
content = ocrFile(inputStream, document);
|
||||||
} else if (file.getMimeType().equals(MimeType.APPLICATION_PDF)) {
|
} else if (file.getMimeType().equals(MimeType.APPLICATION_PDF)) {
|
||||||
content = extractPdf(file);
|
content = extractPdf(inputStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
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 document Document linked to the file
|
||||||
* @param file File to OCR
|
|
||||||
* @return Content extracted
|
* @return Content extracted
|
||||||
*/
|
*/
|
||||||
private static String ocrFile(Document document, File file) {
|
private static String ocrFile(InputStream inputStream, Document document) {
|
||||||
Tesseract instance = Tesseract.getInstance();
|
Tesseract instance = Tesseract.getInstance();
|
||||||
java.io.File storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId()).toFile();
|
|
||||||
String content = null;
|
String content = null;
|
||||||
BufferedImage image = null;
|
BufferedImage image = null;
|
||||||
try {
|
try {
|
||||||
image = ImageIO.read(storedfile);
|
image = ImageIO.read(inputStream);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error reading the image " + storedfile, e);
|
log.error("Error reading the image", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upscale and grayscale the image
|
// Upscale and grayscale the image
|
||||||
@@ -85,7 +89,7 @@ public class FileUtil {
|
|||||||
instance.setLanguage(document.getLanguage());
|
instance.setLanguage(document.getLanguage());
|
||||||
content = instance.doOCR(image);
|
content = instance.doOCR(image);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error while OCR-izing the file " + storedfile, e);
|
log.error("Error while OCR-izing the image", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
@@ -94,19 +98,18 @@ public class FileUtil {
|
|||||||
/**
|
/**
|
||||||
* Extract text from a PDF.
|
* Extract text from a PDF.
|
||||||
*
|
*
|
||||||
* @param file File to extract
|
* @param inputStream Unencrypted input stream
|
||||||
* @return Content extracted
|
* @return Content extracted
|
||||||
*/
|
*/
|
||||||
private static String extractPdf(File file) {
|
private static String extractPdf(InputStream inputStream) {
|
||||||
String content = null;
|
String content = null;
|
||||||
PDDocument pdfDocument = null;
|
PDDocument pdfDocument = null;
|
||||||
java.io.File storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId()).toFile();
|
|
||||||
try {
|
try {
|
||||||
PDFTextStripper stripper = new PDFTextStripper();
|
PDFTextStripper stripper = new PDFTextStripper();
|
||||||
pdfDocument = PDDocument.load(storedfile.getAbsolutePath(), true);
|
pdfDocument = PDDocument.load(inputStream, true);
|
||||||
content = stripper.getText(pdfDocument);
|
content = stripper.getText(pdfDocument);
|
||||||
} catch (IOException e) {
|
} 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 {
|
} finally {
|
||||||
if (pdfDocument != null) {
|
if (pdfDocument != null) {
|
||||||
try {
|
try {
|
||||||
@@ -123,39 +126,39 @@ public class FileUtil {
|
|||||||
/**
|
/**
|
||||||
* Save a file on the storage filesystem.
|
* Save a file on the storage filesystem.
|
||||||
*
|
*
|
||||||
* @param is InputStream
|
* @param inputStream Unencrypted input stream
|
||||||
* @param file File to save
|
* @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());
|
Path path = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId());
|
||||||
Files.copy(is, path);
|
Files.copy(new CipherInputStream(inputStream, cipher), path);
|
||||||
|
|
||||||
// Generate file variations
|
// Generate file variations
|
||||||
try {
|
inputStream.reset();
|
||||||
saveVariations(file, path.toFile());
|
saveVariations(file, inputStream, cipher);
|
||||||
} catch (IOException e) {
|
inputStream.reset();
|
||||||
// Don't rethrow Exception from file variations generation
|
|
||||||
log.error("Error creating file variations", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate file variations.
|
* Generate file variations.
|
||||||
*
|
*
|
||||||
* @param file File from database
|
* @param file File from database
|
||||||
* @param originalFile Original file
|
* @param inputStream Unencrypted input stream
|
||||||
* @throws IOException
|
* @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;
|
BufferedImage image = null;
|
||||||
if (ImageUtil.isImage(file.getMimeType())) {
|
if (ImageUtil.isImage(file.getMimeType())) {
|
||||||
image = ImageIO.read(originalFile);
|
image = ImageIO.read(inputStream);
|
||||||
} else if(file.getMimeType().equals(MimeType.APPLICATION_PDF)) {
|
} else if(file.getMimeType().equals(MimeType.APPLICATION_PDF)) {
|
||||||
// Generate preview from the first page of the PDF
|
// Generate preview from the first page of the PDF
|
||||||
PDDocument pdfDocument = null;
|
PDDocument pdfDocument = null;
|
||||||
try {
|
try {
|
||||||
pdfDocument = PDDocument.load(originalFile.getAbsolutePath(), true);
|
pdfDocument = PDDocument.load(inputStream, true);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<PDPage> pageList = pdfDocument.getDocumentCatalog().getAllPages();
|
List<PDPage> pageList = pdfDocument.getDocumentCatalog().getAllPages();
|
||||||
if (pageList.size() > 0) {
|
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 web = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 1280, Scalr.OP_ANTIALIAS);
|
||||||
BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 256, Scalr.OP_ANTIALIAS);
|
BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 256, Scalr.OP_ANTIALIAS);
|
||||||
image.flush();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
173
docs-core/src/main/java/com/sismics/tess4j/ImageHelper.java
Normal file
173
docs-core/src/main/java/com/sismics/tess4j/ImageHelper.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
docs-core/src/main/java/com/sismics/tess4j/ImageIOHelper.java
Normal file
143
docs-core/src/main/java/com/sismics/tess4j/ImageIOHelper.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
686
docs-core/src/main/java/com/sismics/tess4j/TessAPI.java
Normal file
686
docs-core/src/main/java/com/sismics/tess4j/TessAPI.java
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
256
docs-core/src/main/java/com/sismics/tess4j/Tesseract.java
Normal file
256
docs-core/src/main/java/com/sismics/tess4j/Tesseract.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package com.sismics.util;
|
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.IIOImage;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.imageio.ImageWriteParam;
|
import javax.imageio.ImageWriteParam;
|
||||||
import javax.imageio.ImageWriter;
|
import javax.imageio.ImageWriter;
|
||||||
import javax.imageio.stream.FileImageOutputStream;
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.File;
|
import com.sismics.util.mime.MimeType;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image processing utilities.
|
* Image processing utilities.
|
||||||
@@ -23,26 +24,26 @@ public class ImageUtil {
|
|||||||
* Write a high quality JPEG.
|
* Write a high quality JPEG.
|
||||||
*
|
*
|
||||||
* @param image
|
* @param image
|
||||||
* @param file
|
* @param outputStream Output stream
|
||||||
* @throws IOException
|
* @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");
|
Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName("jpeg");
|
||||||
ImageWriter writer = null;
|
ImageWriter writer = null;
|
||||||
FileImageOutputStream output = null;
|
ImageOutputStream imageOutputStream = null;
|
||||||
try {
|
try {
|
||||||
writer = (ImageWriter) iter.next();
|
writer = (ImageWriter) iter.next();
|
||||||
ImageWriteParam iwp = writer.getDefaultWriteParam();
|
ImageWriteParam iwp = writer.getDefaultWriteParam();
|
||||||
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||||
iwp.setCompressionQuality(1.f);
|
iwp.setCompressionQuality(1.f);
|
||||||
output = new FileImageOutputStream(file);
|
imageOutputStream = ImageIO.createImageOutputStream(outputStream);
|
||||||
writer.setOutput(output);
|
writer.setOutput(imageOutputStream);
|
||||||
IIOImage iioImage = new IIOImage(image, null, null);
|
IIOImage iioImage = new IIOImage(image, null, null);
|
||||||
writer.write(null, iioImage, iwp);
|
writer.write(null, iioImage, iwp);
|
||||||
} finally {
|
} finally {
|
||||||
if (output != null) {
|
if (imageOutputStream != null) {
|
||||||
try {
|
try {
|
||||||
output.close();
|
imageOutputStream.close();
|
||||||
} catch (Exception inner) {
|
} catch (Exception inner) {
|
||||||
// NOP
|
// NOP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,28 +54,33 @@ public class ResourceUtil {
|
|||||||
// Extract the JAR path
|
// Extract the JAR path
|
||||||
String jarPath = dirUrl.getPath().substring(5, dirUrl.getPath().indexOf("!"));
|
String jarPath = dirUrl.getPath().substring(5, dirUrl.getPath().indexOf("!"));
|
||||||
JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
|
JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
|
||||||
|
|
||||||
Enumeration<JarEntry> entries = jar.entries();
|
|
||||||
Set<String> fileSet = new HashSet<String>();
|
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)) {
|
try {
|
||||||
fileSet.add(name);
|
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);
|
return Lists.newArrayList(fileSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ public class MemoryAppender extends AppenderSkeleton {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void append(LoggingEvent event) {
|
public synchronized void append(LoggingEvent event) {
|
||||||
// TODO Don't use size()
|
|
||||||
while (logQueue.size() > size) {
|
while (logQueue.size() > size) {
|
||||||
logQueue.remove();
|
logQueue.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.sismics.util.mime;
|
package com.sismics.util.mime;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to check MIME types.
|
* Utility to check MIME types.
|
||||||
@@ -24,6 +25,18 @@ public class MimeTypeUtil {
|
|||||||
if (readCount <= 0) {
|
if (readCount <= 0) {
|
||||||
throw new Exception("Cannot read input file");
|
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");
|
String header = new String(headerBytes, "US-ASCII");
|
||||||
|
|
||||||
if (header.startsWith("PK")) {
|
if (header.startsWith("PK")) {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
db.version=5
|
db.version=6
|
||||||
@@ -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';
|
||||||
@@ -21,6 +21,7 @@ public class TestJpa extends BaseTransactionalTest {
|
|||||||
user.setEmail("toto@docs.com");
|
user.setEmail("toto@docs.com");
|
||||||
user.setLocaleId("fr");
|
user.setLocaleId("fr");
|
||||||
user.setRoleId("admin");
|
user.setRoleId("admin");
|
||||||
|
user.setPrivateKey("AwesomePrivateKey");
|
||||||
String id = userDao.create(user);
|
String id = userDao.create(user);
|
||||||
|
|
||||||
TransactionUtil.commit();
|
TransactionUtil.commit();
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs-core/src/test/resources/file/udhr.pdf
Normal file
BIN
docs-core/src/test/resources/file/udhr.pdf
Normal file
Binary file not shown.
BIN
docs-core/src/test/resources/file/udhr_encrypted.pdf
Normal file
BIN
docs-core/src/test/resources/file/udhr_encrypted.pdf
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
- Automatic backup system using Quartz (server)
|
||||||
|
|||||||
Binary file not shown.
@@ -63,6 +63,7 @@
|
|||||||
<org.vafer.jdeb.version>1.0.1</org.vafer.jdeb.version>
|
<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>
|
<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.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>
|
</properties>
|
||||||
|
|
||||||
<scm>
|
<scm>
|
||||||
@@ -443,6 +444,12 @@
|
|||||||
<version>${org.apache.pdfbox.pdfbox.version}</version>
|
<version>${org.apache.pdfbox.pdfbox.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
<version>${org.bouncycastle.bcprov-jdk15on.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- OCR dependencies -->
|
<!-- OCR dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jna</groupId>
|
<groupId>jna</groupId>
|
||||||
@@ -456,11 +463,6 @@
|
|||||||
<version>1.0</version>
|
<version>1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>tess4j</groupId>
|
|
||||||
<artifactId>tess4j</artifactId>
|
|
||||||
<version>1.0</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
@@ -524,23 +526,6 @@
|
|||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</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>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ public class ValidationUtil {
|
|||||||
* @throws JSONException
|
* @throws JSONException
|
||||||
*/
|
*/
|
||||||
public static void validateHexColor(String s, String name, boolean nullable) 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);
|
ValidationUtil.validateLength(s, "name", 7, 7, nullable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class JerseyTestWebAppDescriptorFactory {
|
|||||||
.addFilter(TokenBasedSecurityFilter.class, "tokenBasedSecurityFilter")
|
.addFilter(TokenBasedSecurityFilter.class, "tokenBasedSecurityFilter")
|
||||||
.initParam("com.sun.jersey.spi.container.ContainerRequestFilters", "com.sun.jersey.api.container.filter.LoggingFilter")
|
.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.spi.container.ContainerResponseFilters", "com.sun.jersey.api.container.filter.LoggingFilter")
|
||||||
|
.initParam("com.sun.jersey.config.feature.logging.DisableEntitylogging", "true")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
api.current_version=${project.version}
|
api.current_version=${project.version}
|
||||||
api.min_version=1.0
|
api.min_version=1.0
|
||||||
db.version=5
|
db.version=6
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.sismics.docs.rest.resource;
|
package com.sismics.docs.rest.resource;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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.FileDao;
|
||||||
import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria;
|
import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria;
|
||||||
import com.sismics.docs.core.dao.jpa.dto.DocumentDto;
|
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.context.AppContext;
|
||||||
import com.sismics.docs.core.model.jpa.File;
|
import com.sismics.docs.core.model.jpa.File;
|
||||||
import com.sismics.docs.core.util.ConfigUtil;
|
import com.sismics.docs.core.util.ConfigUtil;
|
||||||
import com.sismics.docs.core.util.DirectoryUtil;
|
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.PaginatedList;
|
||||||
import com.sismics.docs.core.util.jpa.PaginatedLists;
|
import com.sismics.docs.core.util.jpa.PaginatedLists;
|
||||||
import com.sismics.docs.core.util.jpa.SortCriteria;
|
import com.sismics.docs.core.util.jpa.SortCriteria;
|
||||||
@@ -113,7 +109,7 @@ public class AppResource extends BaseResource {
|
|||||||
if (!authenticate()) {
|
if (!authenticate()) {
|
||||||
throw new ForbiddenClientException();
|
throw new ForbiddenClientException();
|
||||||
}
|
}
|
||||||
checkBaseFunction(BaseFunction.ADMIN);
|
// TODO Change level by minLevel (returns all logs above)
|
||||||
|
|
||||||
// Get the memory appender
|
// Get the memory appender
|
||||||
Logger logger = Logger.getRootLogger();
|
Logger logger = Logger.getRootLogger();
|
||||||
@@ -147,29 +143,6 @@ public class AppResource extends BaseResource {
|
|||||||
return Response.ok().entity(response).build();
|
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.
|
* Destroy and rebuild Lucene index.
|
||||||
*
|
*
|
||||||
@@ -197,7 +170,7 @@ public class AppResource extends BaseResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy and rebuild Lucene index.
|
* Clean storage.
|
||||||
*
|
*
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws JSONException
|
* @throws JSONException
|
||||||
@@ -233,38 +206,4 @@ public class AppResource extends BaseResource {
|
|||||||
response.put("status", "ok");
|
response.put("status", "ok");
|
||||||
return Response.ok().entity(response).build();
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import com.google.common.base.Joiner;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.sismics.docs.core.constant.Constants;
|
import com.sismics.docs.core.constant.Constants;
|
||||||
import com.sismics.docs.core.dao.jpa.DocumentDao;
|
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.ShareDao;
|
||||||
import com.sismics.docs.core.dao.jpa.TagDao;
|
import com.sismics.docs.core.dao.jpa.TagDao;
|
||||||
import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria;
|
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.DocumentCreatedAsyncEvent;
|
||||||
import com.sismics.docs.core.event.DocumentDeletedAsyncEvent;
|
import com.sismics.docs.core.event.DocumentDeletedAsyncEvent;
|
||||||
import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent;
|
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.context.AppContext;
|
||||||
import com.sismics.docs.core.model.jpa.Document;
|
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.Share;
|
||||||
import com.sismics.docs.core.model.jpa.Tag;
|
import com.sismics.docs.core.model.jpa.Tag;
|
||||||
import com.sismics.docs.core.util.jpa.PaginatedList;
|
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("create_date", documentDto.getCreateTimestamp());
|
||||||
document.put("shared", documentDto.getShared());
|
document.put("shared", documentDto.getShared());
|
||||||
document.put("language", documentDto.getLanguage());
|
document.put("language", documentDto.getLanguage());
|
||||||
|
document.put("file_count", documentDto.getFileCount());
|
||||||
|
|
||||||
// Get tags
|
// Get tags
|
||||||
List<TagDto> tagDtoList = tagDao.getByDocumentId(documentDto.getId());
|
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());
|
if (params[0].equals("before")) documentCriteria.setCreateDateMax(date.toDate());
|
||||||
else documentCriteria.setCreateDateMin(date.toDate());
|
else documentCriteria.setCreateDateMin(date.toDate());
|
||||||
} catch (IllegalArgumentException e) {
|
} 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")) {
|
} else if (params[0].equals("at")) {
|
||||||
// New specific date criteria
|
// New specific date criteria
|
||||||
@@ -262,7 +268,9 @@ public class DocumentResource extends BaseResource {
|
|||||||
documentCriteria.setCreateDateMax(date.plusYears(1).minusSeconds(1).toDate());
|
documentCriteria.setCreateDateMax(date.plusYears(1).minusSeconds(1).toDate());
|
||||||
}
|
}
|
||||||
} catch (IllegalArgumentException e) {
|
} 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")) {
|
} else if (params[0].equals("shared")) {
|
||||||
// New shared state criteria
|
// New shared state criteria
|
||||||
@@ -292,6 +300,9 @@ public class DocumentResource extends BaseResource {
|
|||||||
*
|
*
|
||||||
* @param title Title
|
* @param title Title
|
||||||
* @param description Description
|
* @param description Description
|
||||||
|
* @param tags Tags
|
||||||
|
* @param language Language
|
||||||
|
* @param createDateStr Creation date
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws JSONException
|
* @throws JSONException
|
||||||
*/
|
*/
|
||||||
@@ -455,21 +466,31 @@ public class DocumentResource extends BaseResource {
|
|||||||
|
|
||||||
// Get the document
|
// Get the document
|
||||||
DocumentDao documentDao = new DocumentDao();
|
DocumentDao documentDao = new DocumentDao();
|
||||||
|
FileDao fileDao = new FileDao();
|
||||||
Document document;
|
Document document;
|
||||||
|
List<File> fileList;
|
||||||
try {
|
try {
|
||||||
document = documentDao.getDocument(id, principal.getId());
|
document = documentDao.getDocument(id, principal.getId());
|
||||||
|
fileList = fileDao.getByDocumentId(id);
|
||||||
} catch (NoResultException e) {
|
} catch (NoResultException e) {
|
||||||
throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", id));
|
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
|
// Raise a document deleted event
|
||||||
DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent();
|
DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent();
|
||||||
documentDeletedAsyncEvent.setDocument(document);
|
documentDeletedAsyncEvent.setDocument(document);
|
||||||
AppContext.getInstance().getAsyncEventBus().post(documentDeletedAsyncEvent);
|
AppContext.getInstance().getAsyncEventBus().post(documentDeletedAsyncEvent);
|
||||||
|
|
||||||
// Delete the document
|
|
||||||
documentDao.delete(document.getId());
|
|
||||||
|
|
||||||
// Always return ok
|
// Always return ok
|
||||||
JSONObject response = new JSONObject();
|
JSONObject response = new JSONObject();
|
||||||
response.put("status", "ok");
|
response.put("status", "ok");
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.sismics.docs.rest.resource;
|
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.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
@@ -20,22 +23,28 @@ import javax.ws.rs.Path;
|
|||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.StreamingOutput;
|
||||||
|
|
||||||
import org.codehaus.jettison.json.JSONException;
|
import org.codehaus.jettison.json.JSONException;
|
||||||
import org.codehaus.jettison.json.JSONObject;
|
import org.codehaus.jettison.json.JSONObject;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
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.DocumentDao;
|
||||||
import com.sismics.docs.core.dao.jpa.FileDao;
|
import com.sismics.docs.core.dao.jpa.FileDao;
|
||||||
import com.sismics.docs.core.dao.jpa.ShareDao;
|
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.FileCreatedAsyncEvent;
|
||||||
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
|
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
|
||||||
import com.sismics.docs.core.model.context.AppContext;
|
import com.sismics.docs.core.model.context.AppContext;
|
||||||
import com.sismics.docs.core.model.jpa.Document;
|
import com.sismics.docs.core.model.jpa.Document;
|
||||||
import com.sismics.docs.core.model.jpa.File;
|
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.DirectoryUtil;
|
||||||
|
import com.sismics.docs.core.util.EncryptionUtil;
|
||||||
import com.sismics.docs.core.util.FileUtil;
|
import com.sismics.docs.core.util.FileUtil;
|
||||||
import com.sismics.rest.exception.ClientException;
|
import com.sismics.rest.exception.ClientException;
|
||||||
import com.sismics.rest.exception.ForbiddenClientException;
|
import com.sismics.rest.exception.ForbiddenClientException;
|
||||||
@@ -77,20 +86,30 @@ public class FileResource extends BaseResource {
|
|||||||
|
|
||||||
// Get the document
|
// Get the document
|
||||||
DocumentDao documentDao = new DocumentDao();
|
DocumentDao documentDao = new DocumentDao();
|
||||||
|
FileDao fileDao = new FileDao();
|
||||||
|
UserDao userDao = new UserDao();
|
||||||
Document document;
|
Document document;
|
||||||
|
User user;
|
||||||
try {
|
try {
|
||||||
document = documentDao.getDocument(documentId, principal.getId());
|
document = documentDao.getDocument(documentId, principal.getId());
|
||||||
|
user = userDao.getById(principal.getId());
|
||||||
} catch (NoResultException e) {
|
} catch (NoResultException e) {
|
||||||
throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId));
|
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
|
// Validate mime type
|
||||||
InputStream is = new BufferedInputStream(fileBodyPart.getValueAs(InputStream.class));
|
|
||||||
String mimeType;
|
String mimeType;
|
||||||
try {
|
try {
|
||||||
mimeType = MimeTypeUtil.guessMimeType(is);
|
mimeType = MimeTypeUtil.guessMimeType(fileInputStream);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new ServerException("ErrorGuessMime", "Error guessing mime type", e);
|
throw new ServerException("ErrorGuessMime", "Error guessing mime type", e);
|
||||||
}
|
}
|
||||||
@@ -113,12 +132,13 @@ public class FileResource extends BaseResource {
|
|||||||
String fileId = fileDao.create(file);
|
String fileId = fileDao.create(file);
|
||||||
|
|
||||||
// Save the file
|
// Save the file
|
||||||
FileUtil.save(is, file);
|
FileUtil.save(fileInputStream, file, user.getPrivateKey());
|
||||||
|
|
||||||
// Raise a new file created event
|
// Raise a new file created event
|
||||||
FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent();
|
FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent();
|
||||||
fileCreatedAsyncEvent.setDocument(document);
|
fileCreatedAsyncEvent.setDocument(document);
|
||||||
fileCreatedAsyncEvent.setFile(file);
|
fileCreatedAsyncEvent.setFile(file);
|
||||||
|
fileCreatedAsyncEvent.setInputStream(fileInputStream);
|
||||||
AppContext.getInstance().getAsyncEventBus().post(fileCreatedAsyncEvent);
|
AppContext.getInstance().getAsyncEventBus().post(fileCreatedAsyncEvent);
|
||||||
|
|
||||||
// Always return ok
|
// Always return ok
|
||||||
@@ -249,14 +269,14 @@ public class FileResource extends BaseResource {
|
|||||||
throw new ClientException("FileNotFound", MessageFormat.format("File not found: {0}", id));
|
throw new ClientException("FileNotFound", MessageFormat.format("File not found: {0}", id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete the file
|
||||||
|
fileDao.delete(file.getId());
|
||||||
|
|
||||||
// Raise a new file deleted event
|
// Raise a new file deleted event
|
||||||
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
|
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
|
||||||
fileDeletedAsyncEvent.setFile(file);
|
fileDeletedAsyncEvent.setFile(file);
|
||||||
AppContext.getInstance().getAsyncEventBus().post(fileDeletedAsyncEvent);
|
AppContext.getInstance().getAsyncEventBus().post(fileDeletedAsyncEvent);
|
||||||
|
|
||||||
// Delete the file
|
|
||||||
fileDao.delete(file.getId());
|
|
||||||
|
|
||||||
// Always return ok
|
// Always return ok
|
||||||
JSONObject response = new JSONObject();
|
JSONObject response = new JSONObject();
|
||||||
response.put("status", "ok");
|
response.put("status", "ok");
|
||||||
@@ -288,10 +308,12 @@ public class FileResource extends BaseResource {
|
|||||||
// Get the file
|
// Get the file
|
||||||
FileDao fileDao = new FileDao();
|
FileDao fileDao = new FileDao();
|
||||||
DocumentDao documentDao = new DocumentDao();
|
DocumentDao documentDao = new DocumentDao();
|
||||||
|
UserDao userDao = new UserDao();
|
||||||
File file;
|
File file;
|
||||||
|
Document document;
|
||||||
try {
|
try {
|
||||||
file = fileDao.getFile(fileId);
|
file = fileDao.getFile(fileId);
|
||||||
Document document = documentDao.getDocument(file.getDocumentId());
|
document = documentDao.getDocument(file.getDocumentId());
|
||||||
|
|
||||||
// Check document visibility
|
// Check document visibility
|
||||||
ShareDao shareDao = new ShareDao();
|
ShareDao shareDao = new ShareDao();
|
||||||
@@ -306,21 +328,48 @@ public class FileResource extends BaseResource {
|
|||||||
// Get the stored file
|
// Get the stored file
|
||||||
java.io.File storedfile;
|
java.io.File storedfile;
|
||||||
String mimeType;
|
String mimeType;
|
||||||
|
boolean decrypt = false;
|
||||||
if (size != null) {
|
if (size != null) {
|
||||||
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId + "_" + size).toFile();
|
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId + "_" + size).toFile();
|
||||||
mimeType = MimeType.IMAGE_JPEG; // Thumbnails are JPEG
|
mimeType = MimeType.IMAGE_JPEG; // Thumbnails are JPEG
|
||||||
|
decrypt = true; // Thumbnails are encrypted
|
||||||
if (!storedfile.exists()) {
|
if (!storedfile.exists()) {
|
||||||
storedfile = new java.io.File(getClass().getResource("/image/file.png").getFile());
|
storedfile = new java.io.File(getClass().getResource("/image/file.png").getFile());
|
||||||
mimeType = MimeType.IMAGE_PNG;
|
mimeType = MimeType.IMAGE_PNG;
|
||||||
|
decrypt = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId).toFile();
|
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId).toFile();
|
||||||
mimeType = file.getMimeType();
|
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("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();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.dao.jpa.dto.UserDto;
|
||||||
import com.sismics.docs.core.model.jpa.AuthenticationToken;
|
import com.sismics.docs.core.model.jpa.AuthenticationToken;
|
||||||
import com.sismics.docs.core.model.jpa.User;
|
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.PaginatedList;
|
||||||
import com.sismics.docs.core.util.jpa.PaginatedLists;
|
import com.sismics.docs.core.util.jpa.PaginatedLists;
|
||||||
import com.sismics.docs.core.util.jpa.SortCriteria;
|
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.MediaType;
|
||||||
import javax.ws.rs.core.NewCookie;
|
import javax.ws.rs.core.NewCookie;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -76,6 +79,11 @@ public class UserResource extends BaseResource {
|
|||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setPassword(password);
|
user.setPassword(password);
|
||||||
user.setEmail(email);
|
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());
|
user.setCreateDate(new Date());
|
||||||
|
|
||||||
if (localeId == null) {
|
if (localeId == null) {
|
||||||
|
|||||||
1
docs-web/src/main/webapp/.gitignore
vendored
1
docs-web/src/main/webapp/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/sismicsdocs
|
/sismicsdocs
|
||||||
|
/.idea
|
||||||
@@ -219,4 +219,5 @@ var App = angular.module('docs',
|
|||||||
.run(function($rootScope, $state, $stateParams) {
|
.run(function($rootScope, $state, $stateParams) {
|
||||||
$rootScope.$state = $state;
|
$rootScope.$state = $state;
|
||||||
$rootScope.$stateParams = $stateParams;
|
$rootScope.$stateParams = $stateParams;
|
||||||
|
$rootScope.pageTitle = 'Sismics Docs';
|
||||||
});
|
});
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
/**
|
/**
|
||||||
* Document edition controller.
|
* 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
|
// Alerts
|
||||||
$scope.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()) {
|
$scope.resetForm = function() {
|
||||||
Restangular.one('document', $stateParams.id).get().then(function(data) {
|
|
||||||
$scope.document = data;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$scope.document = {
|
$scope.document = {
|
||||||
tags: [],
|
tags: [],
|
||||||
language: 'fra'
|
language: 'fra'
|
||||||
};
|
};
|
||||||
}
|
$scope.newFiles = [];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit a document.
|
* Edit a document.
|
||||||
@@ -84,16 +81,17 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
|
|||||||
// When all files upload are over, move on
|
// When all files upload are over, move on
|
||||||
var navigateNext = function() {
|
var navigateNext = function() {
|
||||||
if ($scope.isEdit()) {
|
if ($scope.isEdit()) {
|
||||||
|
// Go back to the edited document
|
||||||
$scope.pageDocuments();
|
$scope.pageDocuments();
|
||||||
$state.transitionTo('document.view', { id: $stateParams.id });
|
$state.transitionTo('document.view', { id: $stateParams.id });
|
||||||
} else {
|
} else {
|
||||||
|
// Reset the scope and stay here
|
||||||
var fileUploadCount = _.size($scope.newFiles);
|
var fileUploadCount = _.size($scope.newFiles);
|
||||||
$scope.alerts.unshift({
|
$scope.alerts.unshift({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
msg: 'Document successfully added (with ' + fileUploadCount + ' file' + (fileUploadCount > 1 ? 's' : '') + ')'
|
msg: 'Document successfully added (with ' + fileUploadCount + ' file' + (fileUploadCount > 1 ? 's' : '') + ')'
|
||||||
});
|
});
|
||||||
$scope.document = { tags: [] };
|
$scope.resetForm();
|
||||||
$scope.newFiles = [];
|
|
||||||
$scope.loadDocuments();
|
$scope.loadDocuments();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +100,7 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
|
|||||||
navigateNext();
|
navigateNext();
|
||||||
} else {
|
} else {
|
||||||
$scope.fileIsUploading = true;
|
$scope.fileIsUploading = true;
|
||||||
|
$rootScope.pageTitle = '0% - Sismics Docs';
|
||||||
|
|
||||||
// Send a file from the input file array and return a promise
|
// Send a file from the input file array and return a promise
|
||||||
var sendFile = function(key) {
|
var sendFile = function(key) {
|
||||||
@@ -122,6 +121,7 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
|
|||||||
|
|
||||||
promiseFile.then(function() {
|
promiseFile.then(function() {
|
||||||
$scope.fileProgress += 100 / _.size($scope.newFiles);
|
$scope.fileProgress += 100 / _.size($scope.newFiles);
|
||||||
|
$rootScope.pageTitle = Math.round($scope.fileProgress) + '% - Sismics Docs';
|
||||||
});
|
});
|
||||||
|
|
||||||
return promiseFile;
|
return promiseFile;
|
||||||
@@ -136,6 +136,7 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
|
|||||||
} else {
|
} else {
|
||||||
$scope.fileIsUploading = false;
|
$scope.fileIsUploading = false;
|
||||||
$scope.fileProgress = 0;
|
$scope.fileProgress = 0;
|
||||||
|
$rootScope.pageTitle = 'Sismics Docs';
|
||||||
navigateNext();
|
navigateNext();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -154,4 +155,15 @@ App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams,
|
|||||||
$state.transitionTo('document.default');
|
$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();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
App.controller('FileView', function($dialog, $state, $stateParams) {
|
App.controller('FileView', function($dialog, $state, $stateParams) {
|
||||||
var dialog = $dialog.dialog({
|
var dialog = $dialog.dialog({
|
||||||
keyboard: true,
|
keyboard: true,
|
||||||
|
dialogClass: 'modal modal-fileview',
|
||||||
templateUrl: 'partial/docs/file.view.html',
|
templateUrl: 'partial/docs/file.view.html',
|
||||||
controller: function($scope, $state, $stateParams, Restangular, dialog) {
|
controller: function($scope, $state, $stateParams, Restangular, dialog) {
|
||||||
// Load files
|
// Load files
|
||||||
|
|||||||
@@ -6,6 +6,42 @@
|
|||||||
App.controller('Navigation', function($scope, $http, $state, $rootScope, User, Restangular) {
|
App.controller('Navigation', function($scope, $http, $state, $rootScope, User, Restangular) {
|
||||||
$rootScope.userInfo = User.userInfo();
|
$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.
|
* User logout.
|
||||||
*/
|
*/
|
||||||
@@ -17,6 +53,9 @@ App.controller('Navigation', function($scope, $http, $state, $rootScope, User, R
|
|||||||
$event.preventDefault();
|
$event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if at least an asynchronous request is in progress.
|
||||||
|
*/
|
||||||
$scope.isLoading = function() {
|
$scope.isLoading = function() {
|
||||||
return $http.pendingRequests.length > 0;
|
return $http.pendingRequests.length > 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ App.controller('Tag', function($scope, $dialog, $state, Tag, Restangular) {
|
|||||||
* Add a tag.
|
* Add a tag.
|
||||||
*/
|
*/
|
||||||
$scope.addTag = function() {
|
$scope.addTag = function() {
|
||||||
// TODO Check if the tag don't already exists
|
|
||||||
Restangular.one('tag').put($scope.tag).then(function(data) {
|
Restangular.one('tag').put($scope.tag).then(function(data) {
|
||||||
$scope.tags.push({ id: data.id, name: $scope.tag.name, color: $scope.tag.color });
|
$scope.tags.push({ id: data.id, name: $scope.tag.name, color: $scope.tag.color });
|
||||||
$scope.tag = { name: '', color: '#3a87ad' };
|
$scope.tag = { name: '', color: '#3a87ad' };
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ App.directive('selectTag', function() {
|
|||||||
replace: true,
|
replace: true,
|
||||||
scope: {
|
scope: {
|
||||||
tags: '=',
|
tags: '=',
|
||||||
ref: '@'
|
ref: '@',
|
||||||
|
ngDisabled: '='
|
||||||
},
|
},
|
||||||
controller: function($scope, Tag) {
|
controller: function($scope, Tag) {
|
||||||
// Retrieve tags
|
// Retrieve tags
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
App.controller('FileView', function($dialog, $state, $stateParams) {
|
App.controller('FileView', function($dialog, $state, $stateParams) {
|
||||||
var dialog = $dialog.dialog({
|
var dialog = $dialog.dialog({
|
||||||
keyboard: true,
|
keyboard: true,
|
||||||
|
dialogClass: 'modal modal-fileview',
|
||||||
templateUrl: 'partial/share/file.view.html',
|
templateUrl: 'partial/share/file.view.html',
|
||||||
controller: function($scope, $state, $stateParams, Restangular, dialog) {
|
controller: function($scope, $state, $stateParams, Restangular, dialog) {
|
||||||
// Load files
|
// Load files
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html ng-app="docs">
|
<html ng-app="docs">
|
||||||
<head>
|
<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 http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="shortcut icon" href="favicon.png" />
|
<link rel="shortcut icon" href="favicon.png" />
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<div class="navbar" ng-controller="Navigation">
|
<div class="navbar" ng-controller="Navigation">
|
||||||
<div class="navbar-inner">
|
<div class="navbar-inner">
|
||||||
<div class="container">
|
<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" />
|
<img src="img/loader.gif" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,6 +72,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="nav pull-right" ng-show="!userInfo.anonymous">
|
<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 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>
|
<li><a href="#" ng-click="logout($event)"><span class="icon-off"></span><span class="hidden-phone"> Logout</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
1
docs-web/src/main/webapp/lib/jquery.js
vendored
1
docs-web/src/main/webapp/lib/jquery.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<ul class="inline">
|
<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>
|
</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()" />
|
autocomplete="off" typeahead="tag.name for tag in allTags | filter: $viewValue" typeahead-on-select="addTag()" />
|
||||||
</div>
|
</div>
|
||||||
@@ -1,56 +1,63 @@
|
|||||||
<form class="form-horizontal" name="documentForm">
|
<img src="img/loader.gif" ng-show="!document && isEdit()" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="row-fluid" ng-show="fileIsUploading">
|
<div ng-show="document || !isEdit()">
|
||||||
<h4>Uploading files...</h4>
|
<form class="form-horizontal" name="documentForm">
|
||||||
<div class="span6"><progress percent="fileProgress" class="progress-info active"></progress></div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-click="viewDocument(document.id)" ng-repeat="document in documents">
|
<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>{{ document.create_date | date: 'yyyy-MM-dd' }}</td>
|
||||||
<td class="hidden-phone cell-tags">
|
<td class="hidden-phone cell-tags">
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
|
|||||||
@@ -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="text-right">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-danger" ng-click="deleteDocument(document)"><span class="icon-trash icon-white"></span> Delete</button>
|
<button class="btn btn-danger" ng-click="deleteDocument(document)"><span class="icon-trash icon-white"></span> Delete</button>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-header">
|
<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>
|
<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 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>
|
<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>
|
||||||
|
|||||||
@@ -1,62 +1,66 @@
|
|||||||
<h2 ng-show="isEdit()">Edit
|
<img src="img/loader.gif" ng-show="!user && isEdit()" />
|
||||||
<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">
|
<div ng-show="user || !isEdit()">
|
||||||
<input name="username" type="text" id="inputUsername" required ng-disabled="isEdit()"
|
<h2 ng-show="isEdit()">Edit
|
||||||
ng-minlength="3" ng-maxlength="50" placeholder="Username" ng-model="user.username"/>
|
<small>"{{ user.username }}"</small>
|
||||||
<span class="help-inline" ng-show="editUserForm.username.$error.required">Required</span>
|
</h2>
|
||||||
<span class="help-inline" ng-show="editUserForm.username.$error.minlength">Too short</span>
|
<h2 ng-show="!isEdit()">Add
|
||||||
<span class="help-inline" ng-show="editUserForm.username.$error.maxlength">Too long</span>
|
<small>user</small>
|
||||||
</div>
|
</h2>
|
||||||
</div>
|
<form class="form-horizontal" name="editUserForm" novalidate>
|
||||||
<div class="control-group" ng-class="{ error: !editUserForm.email.$valid, success: editUserForm.email.$valid }">
|
<div class="control-group" ng-class="{ error: !editUserForm.username.$valid, success: editUserForm.username.$valid }">
|
||||||
<label class="control-label" for="inputEmail">E-mail</label>
|
<label class="control-label" for="inputUsername">Username</label>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<input name="email" type="email" id="inputEmail" required
|
<input name="username" type="text" id="inputUsername" required ng-disabled="isEdit()"
|
||||||
ng-minlength="3" ng-maxlength="50" placeholder="E-mail" ng-model="user.email"/>
|
ng-minlength="3" ng-maxlength="50" placeholder="Username" ng-model="user.username"/>
|
||||||
<span class="help-inline" ng-show="editUserForm.email.$error.required">Required</span>
|
<span class="help-inline" ng-show="editUserForm.username.$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.username.$error.minlength">Too short</span>
|
||||||
<span class="help-inline" ng-show="editUserForm.email.$error.minlength">Too short</span>
|
<span class="help-inline" ng-show="editUserForm.username.$error.maxlength">Too long</span>
|
||||||
<span class="help-inline" ng-show="editUserForm.email.$error.maxlength">Too long</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="control-group" ng-class="{ error: !editUserForm.email.$valid, success: editUserForm.email.$valid }">
|
||||||
<div class="control-group" ng-class="{ error: !editUserForm.password.$valid, success: editUserForm.password.$valid }">
|
<label class="control-label" for="inputEmail">E-mail</label>
|
||||||
<label class="control-label" for="inputPassword">Password</label>
|
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<input name="password" type="password" id="inputPassword" ng-required="!isEdit()"
|
<input name="email" type="email" id="inputEmail" required
|
||||||
ng-minlength="8" ng-maxlength="50" placeholder="Password" ng-model="user.password"/>
|
ng-minlength="3" ng-maxlength="50" placeholder="E-mail" ng-model="user.email"/>
|
||||||
<span class="help-inline" ng-show="editUserForm.password.$error.required">Required</span>
|
<span class="help-inline" ng-show="editUserForm.email.$error.required">Required</span>
|
||||||
<span class="help-inline" ng-show="editUserForm.password.$error.minlength">Too short</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.password.$error.maxlength">Too long</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>
|
<div class="control-group" ng-class="{ error: !editUserForm.password.$valid, success: editUserForm.password.$valid }">
|
||||||
<div class="control-group"
|
<label class="control-label" for="inputPassword">Password</label>
|
||||||
ng-class="{ error: !editUserForm.passwordconfirm.$valid, success: editUserForm.passwordconfirm.$valid }">
|
|
||||||
<label class="control-label" for="inputPasswordConfirm">Password (confirm)</label>
|
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<input name="passwordconfirm" type="password" id="inputPasswordConfirm" ng-required="!isEdit()"
|
<input name="password" type="password" id="inputPassword" ng-required="!isEdit()"
|
||||||
ui-validate="'$value == user.password'" ui-validate-watch="'user.password'"
|
ng-minlength="8" ng-maxlength="50" placeholder="Password" ng-model="user.password"/>
|
||||||
placeholder="Password (confirm)" ng-model="user.passwordconfirm"/>
|
<span class="help-inline" ng-show="editUserForm.password.$error.required">Required</span>
|
||||||
<span class="help-inline" ng-show="editUserForm.passwordconfirm.$error.validator">Password and password confirmation must match</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>
|
<div class="control-group"
|
||||||
<div class="form-actions">
|
ng-class="{ error: !editUserForm.passwordconfirm.$valid, success: editUserForm.passwordconfirm.$valid }">
|
||||||
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editUserForm.$valid">
|
<label class="control-label" for="inputPasswordConfirm">Password (confirm)</label>
|
||||||
<span class="icon-pencil icon-white"></span> {{ isEdit() ? 'Edit' : 'Add' }}
|
|
||||||
</button>
|
<div class="controls">
|
||||||
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit()">
|
<input name="passwordconfirm" type="password" id="inputPasswordConfirm" ng-required="!isEdit()"
|
||||||
<span class="icon-trash icon-white"></span> Delete
|
ui-validate="'$value == user.password'" ui-validate-watch="'user.password'"
|
||||||
</button>
|
placeholder="Password (confirm)" ng-model="user.passwordconfirm"/>
|
||||||
</div>
|
<span class="help-inline" ng-show="editUserForm.passwordconfirm.$error.validator">Password and password confirmation must match</span>
|
||||||
</form>
|
</div>
|
||||||
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
|
</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>
|
||||||
@@ -80,6 +80,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File view
|
||||||
|
.modal-fileview {
|
||||||
|
top: 2%;
|
||||||
|
max-height: 96%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
// File thumbnails
|
// File thumbnails
|
||||||
.thumbnail-file {
|
.thumbnail-file {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -134,3 +141,7 @@ input[readonly].share-link {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-text-error {
|
||||||
|
color: #b94a48 !important;
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
api.current_version=${project.version}
|
api.current_version=${project.version}
|
||||||
api.min_version=1.0
|
api.min_version=1.0
|
||||||
db.version=5
|
db.version=6
|
||||||
@@ -43,13 +43,6 @@ public class TestAppResource extends BaseJerseyTest {
|
|||||||
Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory);
|
Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory);
|
||||||
Assert.assertEquals(0, json.getInt("document_count"));
|
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
|
// Rebuild Lucene index
|
||||||
appResource = resource().path("/app/batch/reindex");
|
appResource = resource().path("/app/batch/reindex");
|
||||||
appResource.addFilter(new CookieAuthenticationFilter(adminAuthenticationToken));
|
appResource.addFilter(new CookieAuthenticationFilter(adminAuthenticationToken));
|
||||||
@@ -83,7 +76,7 @@ public class TestAppResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
||||||
JSONObject json = response.getEntity(JSONObject.class);
|
JSONObject json = response.getEntity(JSONObject.class);
|
||||||
JSONArray logs = json.getJSONArray("logs");
|
JSONArray logs = json.getJSONArray("logs");
|
||||||
Assert.assertTrue(logs.length() == 10);
|
Assert.assertTrue(logs.length() > 0);
|
||||||
Long date1 = logs.optJSONObject(0).optLong("date");
|
Long date1 = logs.optJSONObject(0).optLong("date");
|
||||||
Long date2 = logs.optJSONObject(9).optLong("date");
|
Long date2 = logs.optJSONObject(9).optLong("date");
|
||||||
Assert.assertTrue(date1 > date2);
|
Assert.assertTrue(date1 > date2);
|
||||||
@@ -99,7 +92,7 @@ public class TestAppResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
||||||
json = response.getEntity(JSONObject.class);
|
json = response.getEntity(JSONObject.class);
|
||||||
logs = json.getJSONArray("logs");
|
logs = json.getJSONArray("logs");
|
||||||
Assert.assertTrue(logs.length() == 10);
|
Assert.assertTrue(logs.length() > 0);
|
||||||
Long date3 = logs.optJSONObject(0).optLong("date");
|
Long date3 = logs.optJSONObject(0).optLong("date");
|
||||||
Long date4 = logs.optJSONObject(9).optLong("date");
|
Long date4 = logs.optJSONObject(9).optLong("date");
|
||||||
Assert.assertTrue(date3 > date4);
|
Assert.assertTrue(date3 > date4);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.sismics.docs.rest;
|
|||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
@@ -10,13 +11,15 @@ import javax.ws.rs.core.MediaType;
|
|||||||
import junit.framework.Assert;
|
import junit.framework.Assert;
|
||||||
|
|
||||||
import org.codehaus.jettison.json.JSONArray;
|
import org.codehaus.jettison.json.JSONArray;
|
||||||
import org.codehaus.jettison.json.JSONException;
|
|
||||||
import org.codehaus.jettison.json.JSONObject;
|
import org.codehaus.jettison.json.JSONObject;
|
||||||
import org.joda.time.format.DateTimeFormat;
|
import org.joda.time.format.DateTimeFormat;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
|
import com.sismics.docs.core.util.DirectoryUtil;
|
||||||
import com.sismics.docs.rest.filter.CookieAuthenticationFilter;
|
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;
|
||||||
import com.sun.jersey.api.client.ClientResponse.Status;
|
import com.sun.jersey.api.client.ClientResponse.Status;
|
||||||
import com.sun.jersey.api.client.WebResource;
|
import com.sun.jersey.api.client.WebResource;
|
||||||
@@ -32,11 +35,10 @@ import com.sun.jersey.multipart.FormDataMultiPart;
|
|||||||
public class TestDocumentResource extends BaseJerseyTest {
|
public class TestDocumentResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the document resource.
|
* Test the document resource.
|
||||||
*
|
* @throws Exception
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testDocumentResource() throws JSONException {
|
public void testDocumentResource() throws Exception {
|
||||||
// Login document1
|
// Login document1
|
||||||
clientUtil.createUser("document1");
|
clientUtil.createUser("document1");
|
||||||
String document1Token = clientUtil.login("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);
|
response = fileResource.type(MediaType.MULTIPART_FORM_DATA).put(ClientResponse.class, form);
|
||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
||||||
json = response.getEntity(JSONObject.class);
|
json = response.getEntity(JSONObject.class);
|
||||||
|
String file1Id = json.getString("id");
|
||||||
|
|
||||||
// Share this document
|
// Share this document
|
||||||
WebResource fileShareResource = resource().path("/share");
|
WebResource fileShareResource = resource().path("/share");
|
||||||
@@ -106,183 +109,36 @@ public class TestDocumentResource extends BaseJerseyTest {
|
|||||||
Assert.assertTrue(documents.length() == 1);
|
Assert.assertTrue(documents.length() == 1);
|
||||||
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
|
Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id"));
|
||||||
Assert.assertEquals("eng", documents.getJSONObject(0).getString("language"));
|
Assert.assertEquals("eng", documents.getJSONObject(0).getString("language"));
|
||||||
|
Assert.assertEquals(1, documents.getJSONObject(0).getInt("file_count"));
|
||||||
Assert.assertEquals(1, tags.length());
|
Assert.assertEquals(1, tags.length());
|
||||||
Assert.assertEquals(tag1Id, tags.getJSONObject(0).getString("id"));
|
Assert.assertEquals(tag1Id, tags.getJSONObject(0).getString("id"));
|
||||||
Assert.assertEquals("SuperTag", tags.getJSONObject(0).getString("name"));
|
Assert.assertEquals("SuperTag", tags.getJSONObject(0).getString("name"));
|
||||||
Assert.assertEquals("#ffff00", tags.getJSONObject(0).getString("color"));
|
Assert.assertEquals("#ffff00", tags.getJSONObject(0).getString("color"));
|
||||||
|
|
||||||
// Search documents by query in full content
|
// Search documents
|
||||||
documentResource = resource().path("/document/list");
|
Assert.assertEquals(1, searchDocuments("full:uranium full:einstein", document1Token));
|
||||||
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
|
Assert.assertEquals(1, searchDocuments("full:title", document1Token));
|
||||||
getParams = new MultivaluedMapImpl();
|
Assert.assertEquals(1, searchDocuments("title", document1Token));
|
||||||
getParams.putSingle("search", "full:uranium");
|
Assert.assertEquals(1, searchDocuments("super description", document1Token));
|
||||||
response = documentResource.queryParams(getParams).get(ClientResponse.class);
|
Assert.assertEquals(1, searchDocuments("at:" + DateTimeFormat.forPattern("yyyy").print(new Date().getTime()), document1Token));
|
||||||
json = response.getEntity(JSONObject.class);
|
Assert.assertEquals(1, searchDocuments("at:" + DateTimeFormat.forPattern("yyyy-MM").print(new Date().getTime()), document1Token));
|
||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(1, searchDocuments("at:" + DateTimeFormat.forPattern("yyyy-MM-dd").print(new Date().getTime()), document1Token));
|
||||||
documents = json.getJSONArray("documents");
|
Assert.assertEquals(1, searchDocuments("after:2010 before:2040-08", document1Token));
|
||||||
Assert.assertTrue(documents.length() == 1);
|
Assert.assertEquals(1, searchDocuments("tag:super", document1Token));
|
||||||
|
Assert.assertEquals(1, searchDocuments("shared:yes", document1Token));
|
||||||
// Search documents by query in full content
|
Assert.assertEquals(1, searchDocuments("lang:eng", document1Token));
|
||||||
documentResource = resource().path("/document/list");
|
Assert.assertEquals(1, searchDocuments("after:2010 before:2040-08 tag:super shared:yes lang:eng title description full:uranium", document1Token));
|
||||||
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 (nothing)
|
// Search documents (nothing)
|
||||||
documentResource = resource().path("/document/list");
|
Assert.assertEquals(0, searchDocuments("random", document1Token));
|
||||||
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
|
Assert.assertEquals(0, searchDocuments("full:random", document1Token));
|
||||||
getParams = new MultivaluedMapImpl();
|
Assert.assertEquals(0, searchDocuments("after:2010 before:2011-05-20", document1Token));
|
||||||
getParams.putSingle("search", "random");
|
Assert.assertEquals(0, searchDocuments("at:2040-05-35", document1Token));
|
||||||
response = documentResource.queryParams(getParams).get(ClientResponse.class);
|
Assert.assertEquals(0, searchDocuments("after:2010-18 before:2040-05-38", document1Token));
|
||||||
json = response.getEntity(JSONObject.class);
|
Assert.assertEquals(0, searchDocuments("after:2010-18", document1Token));
|
||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(0, searchDocuments("before:2040-05-38", document1Token));
|
||||||
documents = json.getJSONArray("documents");
|
Assert.assertEquals(0, searchDocuments("tag:Nop", document1Token));
|
||||||
Assert.assertTrue(documents.length() == 0);
|
Assert.assertEquals(0, searchDocuments("lang:fra", document1Token));
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Get a document
|
// Get a document
|
||||||
documentResource = resource().path("/document/" + document1Id);
|
documentResource = resource().path("/document/" + document1Id);
|
||||||
@@ -348,6 +204,14 @@ public class TestDocumentResource extends BaseJerseyTest {
|
|||||||
json = response.getEntity(JSONObject.class);
|
json = response.getEntity(JSONObject.class);
|
||||||
Assert.assertEquals("ok", json.getString("status"));
|
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)
|
// Get a document (KO)
|
||||||
documentResource = resource().path("/document/" + document1Id);
|
documentResource = resource().path("/document/" + document1Id);
|
||||||
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
|
documentResource.addFilter(new CookieAuthenticationFilter(document1Token));
|
||||||
@@ -356,6 +220,25 @@ public class TestDocumentResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus()));
|
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.
|
* Test PDF extraction.
|
||||||
*
|
*
|
||||||
@@ -416,6 +299,7 @@ public class TestDocumentResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
||||||
InputStream is = response.getEntityInputStream();
|
InputStream is = response.getEntityInputStream();
|
||||||
byte[] fileBytes = ByteStreams.toByteArray(is);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.sismics.docs.rest;
|
package com.sismics.docs.rest;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ import org.junit.Test;
|
|||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
import com.sismics.docs.core.util.DirectoryUtil;
|
import com.sismics.docs.core.util.DirectoryUtil;
|
||||||
import com.sismics.docs.rest.filter.CookieAuthenticationFilter;
|
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;
|
||||||
import com.sun.jersey.api.client.ClientResponse.Status;
|
import com.sun.jersey.api.client.ClientResponse.Status;
|
||||||
import com.sun.jersey.api.client.WebResource;
|
import com.sun.jersey.api.client.WebResource;
|
||||||
@@ -88,6 +91,7 @@ public class TestFileResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
||||||
InputStream is = response.getEntityInputStream();
|
InputStream is = response.getEntityInputStream();
|
||||||
byte[] fileBytes = ByteStreams.toByteArray(is);
|
byte[] fileBytes = ByteStreams.toByteArray(is);
|
||||||
|
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes));
|
||||||
Assert.assertEquals(163510, fileBytes.length);
|
Assert.assertEquals(163510, fileBytes.length);
|
||||||
|
|
||||||
// Get the thumbnail data
|
// Get the thumbnail data
|
||||||
@@ -99,6 +103,7 @@ public class TestFileResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
||||||
is = response.getEntityInputStream();
|
is = response.getEntityInputStream();
|
||||||
fileBytes = ByteStreams.toByteArray(is);
|
fileBytes = ByteStreams.toByteArray(is);
|
||||||
|
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes));
|
||||||
Assert.assertEquals(41935, fileBytes.length);
|
Assert.assertEquals(41935, fileBytes.length);
|
||||||
|
|
||||||
// Get the web data
|
// Get the web data
|
||||||
@@ -110,45 +115,14 @@ public class TestFileResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
|
||||||
is = response.getEntityInputStream();
|
is = response.getEntityInputStream();
|
||||||
fileBytes = ByteStreams.toByteArray(is);
|
fileBytes = ByteStreams.toByteArray(is);
|
||||||
|
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes));
|
||||||
Assert.assertEquals(551084, fileBytes.length);
|
Assert.assertEquals(551084, fileBytes.length);
|
||||||
|
|
||||||
// Regenerate file variations
|
// Check that the files are not readable directly from FS
|
||||||
String adminAuthenticationToken = clientUtil.login("admin", "admin", false);
|
java.io.File storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile();
|
||||||
WebResource appResource = resource().path("/app/batch/file_variations");
|
InputStream storedFileInputStream = new BufferedInputStream(new FileInputStream(storedFile));
|
||||||
appResource.addFilter(new CookieAuthenticationFilter(adminAuthenticationToken));
|
Assert.assertNull(MimeTypeUtil.guessMimeType(storedFileInputStream));
|
||||||
response = appResource.post(ClientResponse.class);
|
storedFileInputStream.close();
|
||||||
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);
|
|
||||||
|
|
||||||
// Get all files from a document
|
// Get all files from a document
|
||||||
fileResource = resource().path("/file/list");
|
fileResource = resource().path("/file/list");
|
||||||
@@ -195,7 +169,7 @@ public class TestFileResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals("ok", json.getString("status"));
|
Assert.assertEquals("ok", json.getString("status"));
|
||||||
|
|
||||||
// Check that files are deleted from FS
|
// 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 webFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_web").toFile();
|
||||||
java.io.File thumbnailFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_thumb").toFile();
|
java.io.File thumbnailFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_thumb").toFile();
|
||||||
Assert.assertFalse(storedFile.exists());
|
Assert.assertFalse(storedFile.exists());
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ log4j.appender.MEMORY.size=1000
|
|||||||
|
|
||||||
log4j.logger.com.sismics=DEBUG
|
log4j.logger.com.sismics=DEBUG
|
||||||
log4j.logger.org.hibernate=INFO
|
log4j.logger.org.hibernate=INFO
|
||||||
|
log4j.logger.org.apache.pdfbox=INFO
|
||||||
Reference in New Issue
Block a user