-
-* Zanata now uses Infinispan as its cache provider, and the cache needs to be configured in Jboss' `standalone.xml` file. Please see the [Infinispan](user-guide/system-admin/configuration/infinispan) section for more information.
+
Deployment
-* This release adds a one-time migration of some data, which can cause a timeout during server startup. This applies to
-all plain text and libreoffice formats, so is only a concern for servers that are upgrading from an earlier version and
-already have several hundred such documents. To avoid the timeout, add or change the following property in
-`standalone.xml`. A value of 1000 seconds is sufficient in our tests. Since the migration is performed only once, the
-property can safely be reverted or removed before subsequent startups.
+* Deployment for this release may require a longer timeout due to underlying database schema changes and data migration. This is dependent on database size and system performance, and the system administrator should consider increasing the JBoss timeout value in standalone.xml. This example sets a timeout of two hours, which should be more than enough:
...
-
+
+ ...
+
+
+* The Zanata administrator will also need to reindex HProject table via the Administration menu. See [Manage search](user-guide/admin/manage-search) for more information.
+
+
+
Infrastructure Changes
+
+* Zanata now uses Infinispan as its cache provider, and the cache needs to be configured in Jboss' `standalone.xml` file. Please see the [Infinispan](user-guide/system-admin/configuration/infinispan) section for more information.
* [1207423](https://bugzilla.redhat.com/show_bug.cgi?id=1207423) - zanata-assets(javascipts and css style) now are packaged as jar and is part of zanata-server dependency.
[Release](http://repository-zanata.forge.cloudbees.com/release/org/zanata/zanata-assets/) and [snapshot](http://repository-zanata.forge.cloudbees.com/snapshot/org/zanata/zanata-assets/)
@@ -25,19 +28,7 @@ Example usage in html file: `Deployment
-
-* Deployment for this release may require a longer timeout due to underlying database changes. This is dependent on database size and the system administrator should consider increasing the JBoss timeout value in standalone.xml.
-
- ...
-
-
-
-
-* The Zanata administrator will also need to reindex HProject table via the Administration menu. See [Manage search](user-guide/admin/manage-search) for more information.
-
-
Bug fixes
* [1194543](https://bugzilla.redhat.com/show_bug.cgi?id=1194543) - Manual document re-upload makes previous translations fuzzy
* [1029734](https://bugzilla.redhat.com/show_bug.cgi?id=1029734) - po header contains invalid entry will cause upload/push failure
@@ -62,6 +53,10 @@ Example usage in html file: `New Features
@@ -77,6 +72,8 @@ Example usage in html file: `sflaniga@redhat.com
+ */
+
+import groovy.util.AntBuilder
+
+// NB: project is a MavenProject
+// http://maven.apache.org/ref/3-LATEST/maven-core/apidocs/org/apache/maven/project/MavenProject.html
+
+String downloadDir = project.properties.get('download.dir') // ~/Downloads
+String cargoExtractDir = project.properties.get('cargo.extract.dir') // target/cargo/installs
+String url = project.properties.get('cargo.installation') // http://example.com/jbosseap6.zip
+
+String filename = url.substring(url.lastIndexOf('/')+1)
+String filePath = "${downloadDir}/${filename}"
+String basename = filename.substring(0, filename.lastIndexOf('.'))
+String extractDir = "${cargoExtractDir}/${basename}"
+
+def ant = new AntBuilder()
+ant.mkdir(dir: downloadDir)
+ant.get(src: url, dest: filePath, skipexisting: "true")
+ant.unzip(src: filePath, dest:"${extractDir}")
+
+def files = new File(extractDir).listFiles()
+if (files.length != 1) {
+ throw new Exception('zip should contain exactly one top-level dir; see ' + extractDir)
+}
+def topLevelDir = files[0].path
+
+project.properties.put('appserver.home', topLevelDir)
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000000..94f223f4bb
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,30 @@
+## This is a module to build zanata frontend javascript projects
+
+At the moment it only contains "user profile page" bundle.
+
+To build it, just run
+
+```mvn install```
+
+It will build and deploy to local maven repository a jar file containing the javascript bundle.
+The jar file can be used directly under any servlet 3 compatible container and the bundle is accessible as static resources.
+See [Servlet 3 static resources](http://www.webjars.org/documentation#servlet3).
+
+The following Maven properties can be overridden on the command line with ```-Dkey=value```:
+
+```
+v0.12.2
+2.7.6
+${download.dir}/zanata-frontend/node-${node.version}-npm-${npm.version}
+${node.install.directory}/node/npm/bin/npm-cli.js
+```
+
+By default it will try to install npm modules from npm registry (default cache TTL is 10 seconds).
+If you activate profile ```-DnpmOffline``` the cache-min option will become 9999999 which means it will try to install npm modules from cache first.
+
+## NPM shrinkwrap
+
+Currently the user profile page module has been "shrinkwrapped" which means its npm module dependencies has been fixed to certain version. If you want to add or upgrade an individual version, you will need to consult [npm shrinkwrap documentation](https://docs.npmjs.com/cli/shrinkwrap#building-shrinkwrapped-packages) for detail instruction.
+
+Since we use maven to copy our source to target/ then run npm from maven, you will need to run above commands under target/ then copy the new npm-shrinkwrap.json file back to src/.
+
diff --git a/frontend/pom.xml b/frontend/pom.xml
index 4a3b2992b2..f4e9f91e73 100644
--- a/frontend/pom.xml
+++ b/frontend/pom.xml
@@ -1,11 +1,10 @@
-
+4.0.0org.zanataserver
- 3.7.0-SNAPSHOT
+ 3.8.0-SNAPSHOTfrontendfrontend
@@ -15,10 +14,11 @@
v0.12.22.7.6${project.build.directory}/web
- ${project.build.directory}/build
- ${bundle.output}/META-INF/resources
- ${project.build.directory}
+ ${project.build.outputDirectory}/META-INF/resources
+ ${download.dir}/zanata-frontend/node-${node.version}-npm-${npm.version}${node.install.directory}/node/npm/bin/npm-cli.js
+
+ 10user-profile-page
@@ -85,6 +85,8 @@
${npm.cli.script}install
+ --cache-min
+ ${npm.cache.min}
@@ -100,7 +102,7 @@
${npm.cli.script}runbuild
- bundleDest=${bundle.dest}
+ --env.bundleDest=${bundle.dest}
@@ -115,14 +117,21 @@
-
- org.apache.maven.plugins
- maven-jar-plugin
-
- ${bundle.output}
-
-
+
+
+ npmOffline
+
+
+ npmOffline
+
+
+
+ 9999999
+
+
+
+
diff --git a/frontend/src/main/web/user-profile-page/npm-shrinkwrap.json b/frontend/src/main/web/user-profile-page/npm-shrinkwrap.json
new file mode 100644
index 0000000000..121a290c52
--- /dev/null
+++ b/frontend/src/main/web/user-profile-page/npm-shrinkwrap.json
@@ -0,0 +1,377 @@
+{
+ "name": "react-profile",
+ "version": "1.0.0",
+ "dependencies": {
+ "lodash": {
+ "version": "3.9.3",
+ "from": "lodash@>=3.2.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.9.3.tgz"
+ },
+ "moment": {
+ "version": "2.10.3",
+ "from": "moment@>=2.9.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.10.3.tgz"
+ },
+ "moment-range": {
+ "version": "1.2.0",
+ "from": "moment-range@>=1.0.6 <2.0.0",
+ "resolved": "https://registry.npmjs.org/moment-range/-/moment-range-1.2.0.tgz"
+ },
+ "react": {
+ "version": "0.12.2",
+ "from": "react@>=0.12.2 <0.13.0",
+ "dependencies": {
+ "envify": {
+ "version": "3.4.0",
+ "from": "envify@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/envify/-/envify-3.4.0.tgz",
+ "dependencies": {
+ "through": {
+ "version": "2.3.7",
+ "from": "through@>=2.3.4 <2.4.0",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.7.tgz"
+ },
+ "jstransform": {
+ "version": "10.1.0",
+ "from": "jstransform@>=10.0.1 <11.0.0",
+ "dependencies": {
+ "base62": {
+ "version": "0.1.1",
+ "from": "base62@0.1.1"
+ },
+ "esprima-fb": {
+ "version": "13001.1001.0-dev-harmony-fb",
+ "from": "esprima-fb@13001.1001.0-dev-harmony-fb"
+ },
+ "source-map": {
+ "version": "0.1.31",
+ "from": "source-map@0.1.31",
+ "dependencies": {
+ "amdefine": {
+ "version": "0.1.1",
+ "from": "amdefine@>=0.0.4",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-0.1.1.tgz"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "react-chartjs": {
+ "version": "0.4.0",
+ "from": "react-chartjs@>=0.4.0 <0.5.0"
+ },
+ "superagent": {
+ "version": "0.21.0",
+ "from": "superagent@>=0.21.0 <0.22.0",
+ "dependencies": {
+ "qs": {
+ "version": "1.2.0",
+ "from": "qs@1.2.0"
+ },
+ "formidable": {
+ "version": "1.0.14",
+ "from": "formidable@1.0.14",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz"
+ },
+ "mime": {
+ "version": "1.2.11",
+ "from": "mime@1.2.11"
+ },
+ "component-emitter": {
+ "version": "1.1.2",
+ "from": "component-emitter@1.1.2"
+ },
+ "methods": {
+ "version": "1.0.1",
+ "from": "methods@1.0.1"
+ },
+ "cookiejar": {
+ "version": "2.0.1",
+ "from": "cookiejar@2.0.1"
+ },
+ "debug": {
+ "version": "2.2.0",
+ "from": "debug@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+ "dependencies": {
+ "ms": {
+ "version": "0.7.1",
+ "from": "ms@0.7.1"
+ }
+ }
+ },
+ "reduce-component": {
+ "version": "1.0.1",
+ "from": "reduce-component@1.0.1"
+ },
+ "extend": {
+ "version": "1.2.1",
+ "from": "extend@>=1.2.1 <1.3.0"
+ },
+ "form-data": {
+ "version": "0.1.3",
+ "from": "form-data@0.1.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.3.tgz",
+ "dependencies": {
+ "combined-stream": {
+ "version": "0.0.7",
+ "from": "combined-stream@>=0.0.4 <0.1.0",
+ "dependencies": {
+ "delayed-stream": {
+ "version": "0.0.5",
+ "from": "delayed-stream@0.0.5"
+ }
+ }
+ },
+ "async": {
+ "version": "0.9.2",
+ "from": "async@>=0.9.0 <0.10.0",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz"
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "1.0.27-1",
+ "from": "readable-stream@1.0.27-1",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.27-1.tgz",
+ "dependencies": {
+ "core-util-is": {
+ "version": "1.0.1",
+ "from": "core-util-is@>=1.0.0 <1.1.0"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1"
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "from": "string_decoder@>=0.10.0 <0.11.0"
+ },
+ "inherits": {
+ "version": "2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0"
+ }
+ }
+ }
+ }
+ },
+ "flux": {
+ "version": "2.0.3",
+ "from": "flux@>=2.0.1 <2.1.0",
+ "resolved": "https://registry.npmjs.org/flux/-/flux-2.0.3.tgz"
+ },
+ "object-assign": {
+ "version": "2.0.0",
+ "from": "object-assign@>=2.0.0 <2.1.0"
+ },
+ "events": {
+ "version": "1.0.2",
+ "from": "events@>=1.0.2 <1.1.0"
+ },
+ "es6-promise": {
+ "version": "2.0.1",
+ "from": "es6-promise@>=2.0.1 <2.1.0"
+ },
+ "keymirror": {
+ "version": "0.1.1",
+ "from": "keymirror@>=0.1.1 <0.2.0"
+ },
+ "chart.js": {
+ "version": "1.0.2",
+ "from": "git://github.com/huangp/Chart.js.git",
+ "resolved": "git://github.com/huangp/Chart.js.git#065c5d7206aac81acfcd823e7598e49147398649"
+ },
+ "node-libs-browser": {
+ "version": "0.5.2",
+ "from": "node-libs-browser@>=0.4.0 <=0.6.0",
+ "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-0.5.2.tgz",
+ "dependencies": {
+ "assert": {
+ "version": "1.3.0",
+ "from": "assert@>=1.1.1 <2.0.0"
+ },
+ "browserify-zlib": {
+ "version": "0.1.4",
+ "from": "browserify-zlib@>=0.1.4 <0.2.0",
+ "dependencies": {
+ "pako": {
+ "version": "0.2.7",
+ "from": "pako@>=0.2.0 <0.3.0"
+ }
+ }
+ },
+ "buffer": {
+ "version": "3.2.2",
+ "from": "buffer@>=3.0.3 <4.0.0",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.2.2.tgz",
+ "dependencies": {
+ "base64-js": {
+ "version": "0.0.8",
+ "from": "base64-js@0.0.8"
+ },
+ "ieee754": {
+ "version": "1.1.6",
+ "from": "ieee754@>=1.1.4 <2.0.0"
+ },
+ "is-array": {
+ "version": "1.0.1",
+ "from": "is-array@>=1.0.1 <2.0.0"
+ }
+ }
+ },
+ "console-browserify": {
+ "version": "1.1.0",
+ "from": "console-browserify@>=1.1.0 <2.0.0",
+ "dependencies": {
+ "date-now": {
+ "version": "0.1.4",
+ "from": "date-now@>=0.1.4 <0.2.0"
+ }
+ }
+ },
+ "constants-browserify": {
+ "version": "0.0.1",
+ "from": "constants-browserify@0.0.1"
+ },
+ "crypto-browserify": {
+ "version": "3.2.8",
+ "from": "crypto-browserify@>=3.2.6 <3.3.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.2.8.tgz",
+ "dependencies": {
+ "pbkdf2-compat": {
+ "version": "2.0.1",
+ "from": "pbkdf2-compat@2.0.1"
+ },
+ "ripemd160": {
+ "version": "0.2.0",
+ "from": "ripemd160@0.2.0"
+ },
+ "sha.js": {
+ "version": "2.2.6",
+ "from": "sha.js@2.2.6"
+ }
+ }
+ },
+ "domain-browser": {
+ "version": "1.1.4",
+ "from": "domain-browser@>=1.1.1 <2.0.0"
+ },
+ "http-browserify": {
+ "version": "1.7.0",
+ "from": "http-browserify@>=1.3.2 <2.0.0",
+ "dependencies": {
+ "Base64": {
+ "version": "0.2.1",
+ "from": "Base64@>=0.2.0 <0.3.0"
+ },
+ "inherits": {
+ "version": "2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0"
+ }
+ }
+ },
+ "https-browserify": {
+ "version": "0.0.0",
+ "from": "https-browserify@0.0.0"
+ },
+ "os-browserify": {
+ "version": "0.1.2",
+ "from": "os-browserify@>=0.1.2 <0.2.0"
+ },
+ "path-browserify": {
+ "version": "0.0.0",
+ "from": "path-browserify@0.0.0"
+ },
+ "process": {
+ "version": "0.11.1",
+ "from": "process@>=0.11.0 <0.12.0",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.1.tgz"
+ },
+ "punycode": {
+ "version": "1.3.2",
+ "from": "punycode@>=1.2.4 <2.0.0"
+ },
+ "querystring-es3": {
+ "version": "0.2.1",
+ "from": "querystring-es3@>=0.2.0 <0.3.0"
+ },
+ "readable-stream": {
+ "version": "1.1.13",
+ "from": "readable-stream@>=1.1.13 <2.0.0",
+ "dependencies": {
+ "core-util-is": {
+ "version": "1.0.1",
+ "from": "core-util-is@>=1.0.0 <1.1.0"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1"
+ },
+ "inherits": {
+ "version": "2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0"
+ }
+ }
+ },
+ "stream-browserify": {
+ "version": "1.0.0",
+ "from": "stream-browserify@>=1.0.0 <2.0.0",
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0"
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "from": "string_decoder@>=0.10.25 <0.11.0"
+ },
+ "timers-browserify": {
+ "version": "1.4.1",
+ "from": "timers-browserify@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.1.tgz"
+ },
+ "tty-browserify": {
+ "version": "0.0.0",
+ "from": "tty-browserify@0.0.0"
+ },
+ "url": {
+ "version": "0.10.3",
+ "from": "url@>=0.10.1 <0.11.0",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
+ "dependencies": {
+ "querystring": {
+ "version": "0.2.0",
+ "from": "querystring@0.2.0"
+ }
+ }
+ },
+ "util": {
+ "version": "0.10.3",
+ "from": "util@>=0.10.3 <0.11.0",
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.1",
+ "from": "inherits@2.0.1"
+ }
+ }
+ },
+ "vm-browserify": {
+ "version": "0.0.4",
+ "from": "vm-browserify@0.0.4",
+ "dependencies": {
+ "indexof": {
+ "version": "0.0.1",
+ "from": "indexof@0.0.1"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/main/web/user-profile-page/package.json b/frontend/src/main/web/user-profile-page/package.json
index 91cfff54f9..0423b049ea 100644
--- a/frontend/src/main/web/user-profile-page/package.json
+++ b/frontend/src/main/web/user-profile-page/package.json
@@ -6,7 +6,7 @@
"scripts": {
"test": "jest",
"js": "webpack",
- "build": "NODE_ENV=production webpack -p --config webpack.prod.config.js",
+ "build": "NODE_ENV=production webpack -p --config webpack.prod.config.js --bail --display-error-details",
"start": "node server.js"
},
"author": "Patrick Huang ",
@@ -16,7 +16,7 @@
"babel-loader": "^4.0.0",
"jsx-loader": "^0.12.2",
"react-hot-loader": "^1.1.1",
- "webpack": "^1.5.3",
+ "webpack": "^1.9.11",
"webpack-dev-server": "^1.7.0",
"jest-cli": "~0.4.0",
"react-tools": "~0.13.0"
diff --git a/frontend/src/main/web/user-profile-page/webpack.prod.config.js b/frontend/src/main/web/user-profile-page/webpack.prod.config.js
index d43d3d6823..44f2731372 100644
--- a/frontend/src/main/web/user-profile-page/webpack.prod.config.js
+++ b/frontend/src/main/web/user-profile-page/webpack.prod.config.js
@@ -1,18 +1,13 @@
var webpack = require('webpack');
var path = require('path');
-// default destination
-var bundleDest = __dirname;
-process.argv.forEach(function(arg) {
- if (/^bundleDest=.+$/.test(arg)) {
- bundleDest = arg.split('=')[1];
- }
-});
+// bundle destination (default is current directory)
+var bundleDest = process.env.npm_config_env_bundleDest || __dirname;
module.exports = {
context: __dirname,
entry: [
- './index.js',
+ './index.js'
],
output: {
path: bundleDest,
@@ -31,7 +26,11 @@ module.exports = {
plugins: [
new webpack.DefinePlugin({ "global.GENTLY": false }),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
- new webpack.optimize.UglifyJsPlugin(),
+ new webpack.optimize.UglifyJsPlugin({
+ compress: {
+ warnings: false
+ }
+ }),
new webpack.optimize.DedupePlugin(),
new webpack.DefinePlugin({
"process.env": {
diff --git a/functional-test/pom.xml b/functional-test/pom.xml
index af0c3ab9e9..8560c5a8fa 100644
--- a/functional-test/pom.xml
+++ b/functional-test/pom.xml
@@ -4,7 +4,7 @@
org.zanataserver
- 3.7.0-SNAPSHOT
+ 3.8.0-SNAPSHOTfunctional-test
@@ -476,6 +476,16 @@
+
+ org.codehaus.gmavenplus
+ gmavenplus-plugin
+
+
+ extract-appserver
+ prepare-package
+
+
+ maven-dependency-plugin
@@ -585,10 +595,6 @@
-
- cargo-install
- prepare-package
- cargo-startpre-integration-test
@@ -819,12 +825,9 @@
-DskipArqTests : to skip Arquillian integration tests (if building zanata-war)Unless skipping tests, you must choose an appserver:
- -Dappserver=jbosseap6 -Dcargo.installation=http://example.com/jbosseap640.zip -Dcargo.basename=jbosseap640
- or -Dappserver=wildfly8
- NB: cargo.basename needs to match the basename of the file given in cargo.installation.
- For example, if cargo.installation is http://example.com/download/jboss-6.4.0.zip, cargo.basename should be jboss-6.4.0.
- appserver.dir.name is top-level dir inside zip. For jbosseap6, default is jboss-eap-6.3. Override for later versions.
-
+ -Dappserver=jbosseap6 or -Dappserver=wildfly8
+ For jbosseap6, env var EAP6_URL should point to an EAP zip file.
+ -DallFuncTests to enable all functional tests (defaults to smoke tests)-Dcargo.debug.jvm.args : If not set by default will listen to port 8787. Need to set to empty on jenkins
@@ -854,7 +857,7 @@
-
+
@@ -903,6 +906,7 @@
installed
+ ${appserver.home}${project.build.directory}/ehcache
diff --git a/functional-test/src/test/java/org/zanata/feature/account/ChangePasswordTest.java b/functional-test/src/test/java/org/zanata/feature/account/ChangePasswordTest.java
index 554c7d1ebb..318ca1377f 100644
--- a/functional-test/src/test/java/org/zanata/feature/account/ChangePasswordTest.java
+++ b/functional-test/src/test/java/org/zanata/feature/account/ChangePasswordTest.java
@@ -21,6 +21,7 @@
package org.zanata.feature.account;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
@@ -55,6 +56,7 @@ public void setUp() {
tcmsTestPlanIds = 5316, tcmsTestCaseIds = 86823)
@Test(timeout = ZanataTestCase.MAX_SHORT_TEST_DURATION)
@Category(BasicAcceptanceTest.class)
+ @Ignore("Flaky test")
public void changePasswordSuccessful() throws Exception {
DashboardBasePage dashboard = new LoginWorkFlow()
.signIn("translator", "translator")
diff --git a/pom.xml b/pom.xml
index 5225774ba6..2c77137654 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,13 +2,13 @@
4.0.0server
- 3.7.0-SNAPSHOT
+ 3.8.0-SNAPSHOTZanata server modulespomorg.zanatazanata-parent
- 21
+ 24-SNAPSHOT../parent
@@ -56,11 +56,11 @@
0.227.0-SNAPSHOT
- 3.7.0-SNAPSHOT
+ 3.7.23.4.13.7.0-SNAPSHOT
- 3.6.1-SNAPSHOT
+ 3.7.14.5.4.Final
@@ -1181,6 +1181,34 @@
+
+ org.codehaus.gmavenplus
+ gmavenplus-plugin
+
+
+
+ extract-appserver
+
+ none
+ execute
+
+
+
+
+
+
+
+
+
+
+ org.apache.ant
+ ant-nodeps
+ 1.8.1
+
+
+ org.zanatazanata-maven-plugin
@@ -1244,14 +1272,14 @@
nonerun
-
+
-
-
@@ -1277,32 +1305,12 @@
org.codehaus.cargocargo-maven2-plugin
- 1.4.5
+ 1.4.14${cargo.container}
-
-
- ${cargo.installation}
- ${download.dir}
- ${cargo.extract.dir}
-
-
-
-
-
-
- cargo-install
-
- none
-
- install
-
-
- maven-failsafe-plugin
@@ -1391,6 +1399,11 @@
2.0
+
+ org.codehaus.gmavenplus
+ gmavenplus-plugin
+ 1.1
+
@@ -1487,10 +1500,7 @@
jboss72x
-
- jboss-eap-6.4
-
- ${cargo.extract.dir}/${cargo.basename}/${appserver.dir.name}
+ ${env.EAP6_URL}
@@ -1505,18 +1515,16 @@
wildfly8x
+
8.1.0.Final
- 9.0.0.CR1
+ 9.0.0.CR2wildfly8http://download.jboss.org/wildfly/${wildfly.version}/wildfly-${wildfly.version}.zip
-
- wildfly-${wildfly.version}
-
- ${cargo.extract.dir}/${appserver.dir.name}/${appserver.dir.name}
+
8.1.0.Final2.1.29-01wildfly-${module.wildfly.version}-module-mojarra-${mojarra.module.version}.zip
diff --git a/zanata-liquibase/pom.xml b/zanata-liquibase/pom.xml
index 5a23594721..8ce9a804c0 100644
--- a/zanata-liquibase/pom.xml
+++ b/zanata-liquibase/pom.xml
@@ -1,11 +1,9 @@
-
+serverorg.zanata
- 3.7.0-SNAPSHOT
+ 3.8.0-SNAPSHOT4.0.0
diff --git a/zanata-model/pom.xml b/zanata-model/pom.xml
index c98182046f..e97047e817 100644
--- a/zanata-model/pom.xml
+++ b/zanata-model/pom.xml
@@ -4,7 +4,7 @@
org.zanataserver
- 3.7.0-SNAPSHOT
+ 3.8.0-SNAPSHOTzanata-modelZanata model
diff --git a/zanata-test-war/pom.xml b/zanata-test-war/pom.xml
index e25d6d1349..509af1dc2d 100644
--- a/zanata-test-war/pom.xml
+++ b/zanata-test-war/pom.xml
@@ -4,7 +4,7 @@
org.zanataserver
- 3.7.0-SNAPSHOT
+ 3.8.0-SNAPSHOTzanata-test-warzanata-test-war
diff --git a/zanata-war/pom.xml b/zanata-war/pom.xml
index eb31ea9fc7..960cbaf0f6 100644
--- a/zanata-war/pom.xml
+++ b/zanata-war/pom.xml
@@ -4,7 +4,7 @@
org.zanataserver
- 3.7.0-SNAPSHOT
+ 3.8.0-SNAPSHOTzanata-warwar
@@ -50,13 +50,19 @@
src/main/resourcestrue
+
+
+ ${project.build.directory}/generated-resources/deps
+
+ dependencies.properties
+
+ org.codehaus.gmavenplusgmavenplus-plugin
- 1.1default
@@ -79,24 +85,8 @@
-
+
+
@@ -190,8 +180,8 @@
- com.ning.maven.plugins
- maven-duplicate-finder-plugin
+ org.basepom.maven
+ duplicate-finder-maven-plugin
@@ -199,16 +189,18 @@
gwt-user
-
+
- META-INF/.*
+ META-INF/.*
- com/lowagie/text/pdf/fonts/cmap_info.txt
+ com/lowagie/text/pdf/fonts/cmap_info.txt
- build.properties
+ build.properties
- seam.properties
-
+ seam.properties
+
+ schema/xml.xsd
+
@@ -401,7 +393,7 @@
1.5C
-->
1
- false
+ true**/*Test.groovy**/*Test.java
@@ -1029,18 +1021,18 @@
-
+
- org.codehaus.cargo
- cargo-maven2-plugin
+ org.codehaus.gmavenplus
+ gmavenplus-plugin
- cargo-install
+ extract-appserverprepare-package
diff --git a/zanata-war/src/etc/dependencyVersions.groovy b/zanata-war/src/etc/dependencyVersions.groovy
new file mode 100644
index 0000000000..e594e51c7f
--- /dev/null
+++ b/zanata-war/src/etc/dependencyVersions.groovy
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+/**
+ * @author Sean Flanigan sflaniga@redhat.com
+ */
+def props = new Properties()
+// NB: project is a MavenProject
+// http://maven.apache.org/ref/3-LATEST/maven-core/apidocs/org/apache/maven/project/MavenProject.html
+project.artifacts.each { a ->
+ def coord = a.groupId + ":" + a.artifactId + ":" + a.type + (a.classifier ? ":" + a.classifier : "")
+ // Make version available to the build:
+ project.properties.put "version." + coord, a.version
+ // This can be expanded to other deps if required:
+ if (a.groupId.startsWith('org.webjars')) {
+ // Make webjar version available at runtime:
+ props.put coord, a.version
+ }
+}
+
+// NB: this has a corresponding resource directory
+// declaration above.
+def genDir = new File(project.build.directory,
+ 'generated-resources/deps')
+genDir.mkdirs()
+new File(genDir, 'dependencies.properties').withWriter { out ->
+ props.store out, 'Zanata dependency versions'
+}
diff --git a/zanata-war/src/main/java/org/zanata/action/ActivityAction.java b/zanata-war/src/main/java/org/zanata/action/ActivityAction.java
index 1d9552f56b..66b430576c 100644
--- a/zanata-war/src/main/java/org/zanata/action/ActivityAction.java
+++ b/zanata-war/src/main/java/org/zanata/action/ActivityAction.java
@@ -21,34 +21,18 @@
package org.zanata.action;
import java.io.Serializable;
-import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
-import org.apache.commons.lang.StringEscapeUtils;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.security.Restrict;
import org.jboss.seam.security.management.JpaIdentityStore;
-import org.zanata.common.ActivityType;
-import org.zanata.dao.DocumentDAO;
-import org.zanata.i18n.Messages;
import org.zanata.model.Activity;
import org.zanata.model.HAccount;
-import org.zanata.model.HDocument;
-import org.zanata.model.HProjectIteration;
-import org.zanata.model.HTextFlowTarget;
-import org.zanata.model.type.EntityType;
import org.zanata.service.ActivityService;
-import org.zanata.util.DateUtil;
-import org.zanata.util.ShortString;
-import org.zanata.util.UrlUtil;
-
-import static org.zanata.common.ActivityType.REVIEWED_TRANSLATION;
-import static org.zanata.common.ActivityType.UPDATE_TRANSLATION;
-import static org.zanata.common.ActivityType.UPLOAD_SOURCE_DOCUMENT;
-import static org.zanata.common.ActivityType.UPLOAD_TRANSLATION_DOCUMENT;
/**
* @author Alex Eng aeng@redhat.com
@@ -59,18 +43,9 @@
public class ActivityAction implements Serializable {
private static final long serialVersionUID = 1L;
- @In
- private DocumentDAO documentDAO;
-
- @In
- private UrlUtil urlUtil;
-
@In
private ActivityService activityServiceImpl;
- @In
- private Messages msgs;
-
@In(required = false, value = JpaIdentityStore.AUTHENTICATED_USER)
private HAccount authenticatedAccount;
@@ -80,331 +55,26 @@ public class ActivityAction implements Serializable {
private int activityPageIndex = 0;
public List getActivities() {
- List activities = new ArrayList();
-
if (authenticatedAccount != null) {
int count = (activityPageIndex + 1) * ACTIVITY_COUNT_PER_LOAD;
- activities =
- activityServiceImpl.findLatestActivities(
+ return activityServiceImpl.findLatestActivities(
authenticatedAccount.getPerson().getId(), 0, count);
}
- return activities;
- }
-
- public String getActivityTypeIconClass(Activity activity) {
- return activity.getActivityType() == UPDATE_TRANSLATION ? "i--translate" :
- activity.getActivityType() == REVIEWED_TRANSLATION ? "i--review" :
- activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT ? "i--document" :
- activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT ? "i--translate-up" :
- "";
- }
-
- public String getActivityTitle(Activity activity) {
- return activity.getActivityType() == UPDATE_TRANSLATION ?
- msgs.get("jsf.Translation") :
- activity.getActivityType() == REVIEWED_TRANSLATION ?
- msgs.get("jsf.Reviewed") :
- activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT ?
- msgs.get("jsf.UploadedSource") :
- activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT ?
- msgs.get("jsf.UploadedTranslations") :
- "";
- }
-
- public String getActivityMessage(Activity activity) {
- switch (activity.getActivityType()) {
- case UPDATE_TRANSLATION:
- return msgs.format("jsf.dashboard.activity.translate.message",
- activity.getWordCount(), getProjectUrl(activity),
- getProjectName(activity), getEditorUrl(activity),
- StringEscapeUtils
- .escapeHtml(getLastTextFlowContent(activity)));
-
- case REVIEWED_TRANSLATION:
- return msgs.format("jsf.dashboard.activity.review.message",
- activity.getWordCount(), getProjectUrl(activity),
- getProjectName(activity), getEditorUrl(activity),
- StringEscapeUtils
- .escapeHtml(getLastTextFlowContent(activity)));
-
- case UPLOAD_SOURCE_DOCUMENT:
- return msgs
- .format("jsf.dashboard.activity.uploadSource.message",
- activity.getWordCount(),
- getProjectUrl(activity),
- getProjectName(activity));
-
- case UPLOAD_TRANSLATION_DOCUMENT:
- return msgs
- .format("jsf.dashboard.activity.uploadTranslation.message",
- activity.getWordCount(),
- getProjectUrl(activity),
- getProjectName(activity));
-
- default:
- return "";
- }
- }
-
- public String getHowLongAgoDescription(Activity activity) {
- return DateUtil.getHowLongAgoDescription(activity.getLastChanged());
- }
-
- public String getProjectName(Activity activity) {
- Object context =
- getEntity(activity.getContextType(), activity.getContextId());
-
- if (isTranslationUpdateActivity(activity.getActivityType())
- || activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT
- || activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HProjectIteration version = (HProjectIteration) context;
- return version.getProject().getName();
- }
- return "";
- }
-
- public String getProjectUrl(Activity activity) {
- Object context =
- getEntity(activity.getContextType(), activity.getContextId());
-
- if (isTranslationUpdateActivity(activity.getActivityType())
- || activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT
- || activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HProjectIteration version = (HProjectIteration) context;
- return urlUtil.projectUrl(version.getProject().getSlug());
- }
- return "";
- }
-
- public String getLastTextFlowContent(Activity activity) {
- String content = "";
- Object lastTarget =
- getEntity(activity.getLastTargetType(),
- activity.getLastTargetId());
-
- if (isTranslationUpdateActivity(activity.getActivityType())) {
- HTextFlowTarget tft = (HTextFlowTarget) lastTarget;
- content = tft.getTextFlow().getContents().get(0);
- }
-
- return ShortString.shorten(content);
- }
-
- public String getEditorUrl(Activity activity) {
- String url = "";
- Object context =
- getEntity(activity.getContextType(), activity.getContextId());
- Object lastTarget =
- getEntity(activity.getLastTargetType(),
- activity.getLastTargetId());
-
- if (isTranslationUpdateActivity(activity.getActivityType())) {
- HProjectIteration version = (HProjectIteration) context;
- HTextFlowTarget tft = (HTextFlowTarget) lastTarget;
-
- url =
- urlUtil.editorTransUnitUrl(version.getProject().getSlug(),
- version.getSlug(), tft.getLocaleId(), tft
- .getTextFlow().getLocale(), tft
- .getTextFlow().getDocument().getDocId(),
- tft.getTextFlow().getId());
- } else if (activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT) {
- // not supported for upload source action
- } else if (activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HProjectIteration version = (HProjectIteration) context;
- HDocument document = (HDocument) lastTarget;
- HTextFlowTarget tft =
- documentDAO.getLastTranslatedTargetOrNull(document.getId());
-
- if (tft != null) {
- url =
- urlUtil.editorTransUnitUrl(version.getProject()
- .getSlug(), version.getSlug(), tft
- .getLocaleId(), document.getSourceLocaleId(),
- tft.getTextFlow().getDocument().getDocId(), tft
- .getTextFlow().getId());
- }
- }
- return url;
- }
-
- public String getDocumentUrl(Activity activity) {
- String url = "";
- Object context =
- getEntity(activity.getContextType(), activity.getContextId());
- Object lastTarget =
- getEntity(activity.getLastTargetType(),
- activity.getLastTargetId());
-
- if (isTranslationUpdateActivity(activity.getActivityType())) {
- HProjectIteration version = (HProjectIteration) context;
- HTextFlowTarget tft = (HTextFlowTarget) lastTarget;
-
- url =
- urlUtil.editorDocumentUrl(version.getProject().getSlug(),
- version.getSlug(), tft.getLocaleId(), tft
- .getTextFlow().getLocale(), tft
- .getTextFlow().getDocument().getDocId());
- } else if (activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT) {
- HProjectIteration version = (HProjectIteration) context;
- url =
- urlUtil.sourceFilesViewUrl(version.getProject().getSlug(),
- version.getSlug());
- } else if (activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HProjectIteration version = (HProjectIteration) context;
- HDocument document = (HDocument) lastTarget;
- HTextFlowTarget tft =
- documentDAO.getLastTranslatedTargetOrNull(document.getId());
-
- if (tft != null) {
- url =
- urlUtil.editorDocumentUrl(version.getProject()
- .getSlug(), version.getSlug(), tft
- .getLocaleId(), document.getSourceLocaleId(),
- tft.getTextFlow().getDocument().getDocId());
- }
- }
- return url;
- }
-
- public String getDocumentName(Activity activity) {
- Object lastTarget =
- getEntity(activity.getLastTargetType(),
- activity.getLastTargetId());
- String docName = "";
-
- if (isTranslationUpdateActivity(activity.getActivityType())) {
- HTextFlowTarget tft = (HTextFlowTarget) lastTarget;
- docName = tft.getTextFlow().getDocument().getName();
- } else if (activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT
- || activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HDocument document = (HDocument) lastTarget;
- docName = document.getName();
- }
- return docName;
- }
-
- public String getVersionUrl(Activity activity) {
- Object context =
- getEntity(activity.getContextType(), activity.getContextId());
- String url = "";
-
- if (isTranslationUpdateActivity(activity.getActivityType())
- || activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT
- || activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HProjectIteration version = (HProjectIteration) context;
- url =
- urlUtil.versionUrl(version.getProject().getSlug(),
- version.getSlug());
- }
-
- return url;
- }
-
- public String getVersionName(Activity activity) {
- Object context =
- getEntity(activity.getContextType(), activity.getContextId());
- String name = "";
-
- if (isTranslationUpdateActivity(activity.getActivityType())
- || activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT
- || activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HProjectIteration version = (HProjectIteration) context;
- name = version.getSlug();
- }
- return name;
- }
-
- public String getDocumentListUrl(Activity activity) {
- Object context =
- getEntity(activity.getContextType(), activity.getContextId());
- Object lastTarget =
- getEntity(activity.getLastTargetType(),
- activity.getLastTargetId());
- String url = "";
-
- if (isTranslationUpdateActivity(activity.getActivityType())) {
- HProjectIteration version = (HProjectIteration) context;
- HTextFlowTarget tft = (HTextFlowTarget) lastTarget;
-
- url =
- urlUtil.editorDocumentListUrl(version.getProject()
- .getSlug(), version.getSlug(), tft.getLocaleId(),
- tft.getTextFlow().getLocale());
- } else if (activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT) {
- // not supported for upload source action
- } else if (activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HProjectIteration version = (HProjectIteration) context;
- HDocument document = (HDocument) lastTarget;
- HTextFlowTarget tft =
- documentDAO.getLastTranslatedTargetOrNull(document.getId());
-
- if (tft != null) {
- url =
- urlUtil.editorDocumentListUrl(version.getProject()
- .getSlug(), version.getSlug(), tft
- .getLocaleId(), tft.getTextFlow().getLocale());
- }
- }
- return url;
- }
-
- public String getLanguageName(Activity activity) {
- Object lastTarget =
- getEntity(activity.getLastTargetType(),
- activity.getLastTargetId());
- String name = "";
-
- if (isTranslationUpdateActivity(activity.getActivityType())) {
- HTextFlowTarget tft = (HTextFlowTarget) lastTarget;
- name = tft.getLocaleId().getId();
- } else if (activity.getActivityType() == UPLOAD_SOURCE_DOCUMENT) {
- // not supported for upload source action
- } else if (activity.getActivityType() == UPLOAD_TRANSLATION_DOCUMENT) {
- HDocument document = (HDocument) lastTarget;
- HTextFlowTarget tft =
- documentDAO.getLastTranslatedTargetOrNull(document.getId());
-
- if (tft != null) {
- name = tft.getLocaleId().getId();
- }
- }
-
- return name;
+ return Collections.emptyList();
}
public void loadNextActivity() {
activityPageIndex++;
}
- public String getWordsCountMessage(int wordCount) {
- if (wordCount == 1) {
- return wordCount + " word";
- }
- return wordCount + " words";
- }
-
public boolean hasMoreActivities() {
int loadedActivitiesCount =
(activityPageIndex + 1) * ACTIVITY_COUNT_PER_LOAD;
- int totalActivitiesCount =
- activityServiceImpl
+ int totalActivitiesCount = activityServiceImpl
.getActivityCountByActor(authenticatedAccount
.getPerson().getId());
- if ((loadedActivitiesCount < totalActivitiesCount)
- && (loadedActivitiesCount < MAX_ACTIVITIES_COUNT_PER_PAGE)) {
- return true;
- }
- return false;
- }
-
- private Object getEntity(EntityType contextType, long id) {
- return activityServiceImpl.getEntity(contextType, id);
- }
-
- private boolean isTranslationUpdateActivity(ActivityType activityType) {
- return activityType == UPDATE_TRANSLATION
- || activityType == REVIEWED_TRANSLATION;
+ return ((loadedActivitiesCount < totalActivitiesCount)
+ && (loadedActivitiesCount < MAX_ACTIVITIES_COUNT_PER_PAGE));
}
}
diff --git a/zanata-war/src/main/java/org/zanata/action/QueryProjectPagedListDataModel.java b/zanata-war/src/main/java/org/zanata/action/QueryProjectPagedListDataModel.java
deleted file mode 100644
index 2f9b386f32..0000000000
--- a/zanata-war/src/main/java/org/zanata/action/QueryProjectPagedListDataModel.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- *
- * * Copyright 2013, Red Hat, Inc. and individual contributors as indicated by the
- * * @author tags. See the copyright.txt file in the distribution for a full
- * * listing of individual contributors.
- * *
- * * This is free software; you can redistribute it and/or modify it under the
- * * terms of the GNU Lesser General Public License as published by the Free
- * * Software Foundation; either version 2.1 of the License, or (at your option)
- * * any later version.
- * *
- * * This software is distributed in the hope that it will be useful, but WITHOUT
- * * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
- * * details.
- * *
- * * You should have received a copy of the GNU Lesser General Public License
- * * along with this software; if not, write to the Free Software Foundation,
- * * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
- * * site: http://www.fsf.org.
- */
-package org.zanata.action;
-
-import java.io.Serializable;
-import java.util.List;
-
-import org.apache.lucene.queryParser.ParseException;
-import org.zanata.dao.ProjectDAO;
-import org.zanata.model.HProject;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.zanata.util.ServiceLocator;
-
-/**
- * @see org.zanata.action.EntityPagedListDataModel
- */
-public class QueryProjectPagedListDataModel extends
- PagedListDataModel implements Serializable {
- private static final long serialVersionUID = 1L;
-
- private final boolean includeObsolete = false;
-
- @Setter
- @Getter
- private String query;
-
- public QueryProjectPagedListDataModel(int pageSize) {
- setPageSize(pageSize);
- }
-
- @Override
- public DataPage fetchPage(int startRow, int pageSize) {
- ProjectDAO projectDAO =
- ServiceLocator.instance().getInstance(ProjectDAO.class);
-
- try {
- List proj =
- projectDAO.searchProjects(query, pageSize, startRow,
- includeObsolete);
-
- int projectSize =
- projectDAO.getQueryProjectSize(query, includeObsolete);
-
- return new DataPage<>(projectSize, startRow, proj);
-
- } catch (ParseException e) {
- return null;
- }
- }
-}
diff --git a/zanata-war/src/main/java/org/zanata/action/ZanataSearch.java b/zanata-war/src/main/java/org/zanata/action/ZanataSearch.java
index 3262fc5c71..dcdb539bf2 100644
--- a/zanata-war/src/main/java/org/zanata/action/ZanataSearch.java
+++ b/zanata-war/src/main/java/org/zanata/action/ZanataSearch.java
@@ -6,8 +6,6 @@
import java.util.Date;
import java.util.List;
-import javax.faces.model.DataModel;
-
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -22,7 +20,6 @@
import org.zanata.dao.AccountDAO;
import org.zanata.dao.ProjectDAO;
import org.zanata.model.HAccount;
-import org.zanata.model.HLocale;
import org.zanata.model.HProject;
import com.google.common.collect.Lists;
@@ -42,13 +39,14 @@ public class ZanataSearch implements Serializable {
private static final long serialVersionUID = 1L;
- private final static int DEFAULT_PAGE_SIZE = 30;
-
private final boolean includeObsolete = false;
@In
private ProjectDAO projectDAO;
+ @In
+ private AccountDAO accountDAO;
+
@Getter
private ProjectUserAutocomplete autocomplete = new ProjectUserAutocomplete();
@@ -57,8 +55,9 @@ public class ZanataSearch implements Serializable {
Lists.newArrayList(SortingType.SortOption.ALPHABETICAL,
SortingType.SortOption.CREATED_DATE));
- private QueryProjectPagedListDataModel queryProjectPagedListDataModel =
- new QueryProjectPagedListDataModel(DEFAULT_PAGE_SIZE);
+ @Getter
+ private SortingType UserSortingList = new SortingType(
+ Lists.newArrayList(SortingType.SortOption.ALPHABETICAL));
// Count of project to be return as part of autocomplete
private final static int INITIAL_RESULT_COUNT = 10;
@@ -69,9 +68,8 @@ public class ZanataSearch implements Serializable {
private final ProjectComparator projectComparator =
new ProjectComparator(getProjectSortingList());
- public DataModel getProjectPagedListDataModel() {
- return queryProjectPagedListDataModel;
- }
+ private final UserComparator userComparator =
+ new UserComparator(getUserSortingList());
@AllArgsConstructor
@NoArgsConstructor
@@ -141,57 +139,92 @@ public void onSelectItemAction() {
@Override
public void setQuery(String query) {
- queryProjectPagedListDataModel.setQuery(query);
super.setQuery(query);
}
}
@Getter
private final AbstractListFilter projectTabProjectFilter =
- new AbstractListFilter() {
-
+ new InMemoryListFilter() {
+
private ProjectDAO projectDAO = ServiceLocator.instance()
.getInstance(ProjectDAO.class);
-
+ /**
+ * Fetches all records.
+ *
+ * @return A list of all records to be managed by the filter.
+ */
@Override
- protected List fetchRecords(int start, int max,
- String filter) {
+ protected List fetchAll() {
+ if (StringUtils.isEmpty(getAutocomplete().getQuery())) {
+ return Collections.emptyList();
+ }
try {
- String search = filter;
- if (StringUtils.isEmpty(search)) {
- search = getAutocomplete().getQuery();
- if(StringUtils.isEmpty(search)) {
- return Collections.emptyList();
- }
- }
- List projects = projectDAO.searchProjects(search, -1, 0,
- includeObsolete);
+ List projects =
+ projectDAO.searchProjects(getAutocomplete()
+ .getQuery(), -1, 0,
+ includeObsolete);
Collections.sort(projects, projectComparator);
return projects;
- } catch (ParseException ex) {
+ } catch (ParseException e) {
return Collections.emptyList();
}
}
+ /**
+ * Indicates whether the element should be included in the results.
+ *
+ * @param elem The element to analyze
+ * @param filter The filter string being used.
+ * @return True if the element passes the filter. False otherwise.
+ */
@Override
- protected long fetchTotalRecords(String filter) {
- try {
- String search = filter;
- if (StringUtils.isEmpty(search)) {
- search = getAutocomplete().getQuery();
- if(StringUtils.isEmpty(search)) {
- return 0L;
- }
- }
- return projectDAO.getQueryProjectSize(search,
- includeObsolete);
- } catch (ParseException ex) {
- return 0L;
+ protected boolean include(HProject elem, String filter) {
+ return true; //no internal filter
+ }
+ };
+
+ @Getter
+ private final AbstractListFilter userTabUserFilter =
+ new InMemoryListFilter() {
+ private AccountDAO accountDAO = ServiceLocator.instance()
+ .getInstance(AccountDAO.class);
+
+ /**
+ * Fetches all records.
+ *
+ * @return A list of all records to be managed by the filter.
+ */
+ @Override
+ protected List fetchAll() {
+ if (StringUtils.isEmpty(getAutocomplete().getQuery())) {
+ return Collections.emptyList();
}
+ List hAccounts =
+ accountDAO.searchQuery(getAutocomplete().getQuery(),
+ -1, 0);
+ Collections.sort(hAccounts, userComparator);
+ return hAccounts;
+ }
+
+ /**
+ * Indicates whether the element should be included in the results.
+ *
+ * @param elem The element to analyze
+ * @param filter The filter string being used.
+ * @return True if the element passes the filter. False otherwise.
+ */
+ @Override
+ protected boolean include(HAccount elem, String filter) {
+ return true; //no internal filter
}
};
-
+
+
public int getTotalProjectCount() {
+ if(StringUtils.isEmpty(getAutocomplete().getQuery())) {
+ return 0;
+ }
try {
return projectDAO.getQueryProjectSize(getAutocomplete().getQuery(),
includeObsolete);
@@ -201,13 +234,21 @@ public int getTotalProjectCount() {
}
public int getTotalUserCount() {
- return 0;
+ if(StringUtils.isEmpty(getAutocomplete().getQuery())) {
+ return 0;
+ }
+ return accountDAO.searchQuery(getAutocomplete().getQuery(), -1, 0)
+ .size();
}
public String getHowLongAgoDescription(Date date) {
return DateUtil.getHowLongAgoDescription(date);
}
+ public String formatDate(Date date) {
+ return DateUtil.formatShortDate(date);
+ }
+
/**
* Sort project list
*/
@@ -215,6 +256,10 @@ public void sortProjectList() {
projectTabProjectFilter.reset();
}
+ public void sortUserList() {
+ userTabUserFilter.reset();
+ }
+
private class ProjectComparator implements Comparator {
private SortingType sortingType;
@@ -241,4 +286,26 @@ public int compare(HProject o1, HProject o2) {
}
}
}
+
+ private class UserComparator implements Comparator {
+ private SortingType sortingType;
+
+ public UserComparator(SortingType sortingType) {
+ this.sortingType = sortingType;
+ }
+
+ @Override
+ public int compare(HAccount o1, HAccount o2) {
+ SortingType.SortOption selectedSortOption =
+ sortingType.getSelectedSortOption();
+
+ if (!selectedSortOption.isAscending()) {
+ HAccount temp = o1;
+ o1 = o2;
+ o2 = temp;
+ }
+ return o1.getPerson().getName().toLowerCase().compareTo(
+ o2.getPerson().getName().toLowerCase());
+ }
+ }
}
diff --git a/zanata-war/src/main/java/org/zanata/dao/AccountDAO.java b/zanata-war/src/main/java/org/zanata/dao/AccountDAO.java
index 2d06503670..b4786db937 100644
--- a/zanata-war/src/main/java/org/zanata/dao/AccountDAO.java
+++ b/zanata-war/src/main/java/org/zanata/dao/AccountDAO.java
@@ -138,7 +138,9 @@ List searchQuery(String searchQuery, int maxResults, int firstResult)
getSession().createQuery(
"from HAccount as a where lower(a.username) like lower(:username)");
query.setParameter("username", userName);
- query.setMaxResults(maxResults);
+ if(maxResults > 0) {
+ query.setMaxResults(maxResults);
+ }
query.setFirstResult(firstResult);
query.setComment("AccountDAO.searchQuery/username");
return query.list();
diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/MediaTypes.java b/zanata-war/src/main/java/org/zanata/rest/editor/MediaTypes.java
index 4af23cc0b9..f62638fa00 100644
--- a/zanata-war/src/main/java/org/zanata/rest/editor/MediaTypes.java
+++ b/zanata-war/src/main/java/org/zanata/rest/editor/MediaTypes.java
@@ -32,6 +32,9 @@ public String toString() {
public static final String APPLICATION_ZANATA_LOCALES_JSON =
APPLICATION_ZANATA_LOCALES + JSON;
+ public static final String APPLICATION_ZANATA_SUGGESTIONS_JSON =
+ APPLICATION_VND_ZANATA + ".suggestions" + JSON;
+
public static final String APPLICATION_ZANATA_PROJECT_VERSION =
APPLICATION_VND_ZANATA + ".version";
public static final String APPLICATION_ZANATA_PROJECT_VERSION_JSON =
diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/JsonDateSerializer.java b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/JsonDateSerializer.java
new file mode 100644
index 0000000000..ed014397ce
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/JsonDateSerializer.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.zanata.rest.editor.dto.suggestion;
+
+import org.codehaus.jackson.JsonGenerator;
+import org.codehaus.jackson.map.JsonSerializer;
+import org.codehaus.jackson.map.SerializerProvider;
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+import java.io.IOException;
+import java.util.Date;
+
+/**
+ * Serializer to output dates in ISO-8601 format.
+ *
+ * This format is used for the JSON API because it is compatible with
+ * JavaScript Date.parse() and is a widely used standard.
+ */
+public class JsonDateSerializer extends JsonSerializer {
+
+ private static final DateTimeFormatter ISO8601Format = ISODateTimeFormat.dateTime();
+
+ @Override
+ public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
+ String dateString = ISO8601Format.print(new DateTime(date));
+ jsonGenerator.writeString(dateString);
+ }
+
+}
diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/Suggestion.java b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/Suggestion.java
new file mode 100644
index 0000000000..751514d457
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/Suggestion.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.zanata.rest.editor.dto.suggestion;
+
+import lombok.Getter;
+import org.codehaus.jackson.annotate.JsonPropertyOrder;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a single suggested translation.
+ *
+ * This could be from translation memory or other sources.
+ *
+ * This representation is designed for use with the pure JavaScript editor.
+ */
+@Getter
+@JsonPropertyOrder({ "relevanceScore", "similarityPercent", "sourceContents", "targetContents", "matchDetails" })
+@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
+public class Suggestion implements Serializable {
+
+ private final double relevanceScore;
+ private final double similarityPercent;
+
+ private final List sourceContents;
+ private final List targetContents;
+
+ private final List matchDetails;
+
+ public Suggestion(double relevanceScore, double similarityPercent,
+ List sourceContents, List targetContents) {
+ this.relevanceScore = relevanceScore;
+ this.similarityPercent = similarityPercent;
+ this.sourceContents = sourceContents;
+ this.targetContents = targetContents;
+ this.matchDetails = new ArrayList<>();
+ }
+}
diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/SuggestionDetail.java b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/SuggestionDetail.java
new file mode 100644
index 0000000000..b4a3711f06
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/SuggestionDetail.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.zanata.rest.editor.dto.suggestion;
+
+import java.io.Serializable;
+
+/**
+ * Detailed information about a suggestion of a specific type.
+ */
+public interface SuggestionDetail extends Serializable {
+
+ /**
+ * Possible types of suggestions from different resources.
+ *
+ * Different types may present different information, so use
+ * different class representations.
+ */
+ enum SuggestionType {
+
+ /**
+ * A suggestion from a project on this Zanata server.
+ */
+ LOCAL_PROJECT,
+
+ /**
+ * A suggestion from an imported translation memory.
+ */
+ IMPORTED_TM
+ }
+
+ /**
+ * @return the type of suggestion.
+ */
+ SuggestionType getType();
+}
diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/TextFlowSuggestionDetail.java b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/TextFlowSuggestionDetail.java
new file mode 100644
index 0000000000..58799d6e31
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/TextFlowSuggestionDetail.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.zanata.rest.editor.dto.suggestion;
+
+import lombok.Getter;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+import org.zanata.common.ContentState;
+import org.zanata.model.*;
+
+import java.util.Date;
+
+/**
+ * Detailed information about a suggestion from a project on this server.
+ */
+@Getter
+@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
+public class TextFlowSuggestionDetail implements SuggestionDetail {
+
+ private final SuggestionType type = SuggestionType.LOCAL_PROJECT;
+
+ private final Long textFlowId;
+
+ private final String sourceComment;
+ private final String targetComment;
+
+ private final ContentState contentState;
+
+ private final String projectId;
+ private final String projectName;
+ private final String version;
+ private final String documentName;
+ private final String documentPath;
+
+ private final String resId;
+
+ // TODO use @JsonFormat for date format when jackson is updated to 2+
+ @JsonSerialize(using = JsonDateSerializer.class)
+ private final Date lastModifiedDate;
+ private final String lastModifiedBy;
+
+ /**
+ * Create a detail object based on a given text flow target.
+ *
+ * @param tft for which to create a detail object.
+ */
+ public TextFlowSuggestionDetail(HTextFlowTarget tft) {
+ HTextFlow tf = tft.getTextFlow();
+ final HDocument document = tf.getDocument();
+ final HProjectIteration version = document.getProjectIteration();
+ final HProject project = version.getProject();
+ final HPerson lastModifiedPerson = tft.getLastModifiedBy();
+ final boolean haveLastModifiedUsername = lastModifiedPerson != null && lastModifiedPerson.hasAccount();
+
+ this.textFlowId = tf.getId();
+
+ this.sourceComment = HSimpleComment.toString(tf.getComment());
+ this.targetComment = HSimpleComment.toString(tft.getComment());
+
+ this.contentState = tft.getState();
+
+ this.projectId = project.getSlug();
+ this.projectName = project.getName();
+ this.version = version.getSlug();
+ this.documentName = document.getName();
+ this.documentPath = document.getPath();
+ this.resId = tf.getResId();
+
+ this.lastModifiedDate = tft.getLastChanged();
+ this.lastModifiedBy = haveLastModifiedUsername ?
+ lastModifiedPerson.getAccount().getUsername() : null;
+ }
+
+}
diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/TransMemoryUnitSuggestionDetail.java b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/TransMemoryUnitSuggestionDetail.java
new file mode 100644
index 0000000000..404e3292e8
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/rest/editor/dto/suggestion/TransMemoryUnitSuggestionDetail.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.zanata.rest.editor.dto.suggestion;
+
+import lombok.Getter;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+import org.zanata.model.tm.TransMemoryUnit;
+
+import java.util.Date;
+
+/**
+ * Detailed information about a suggestion from an imported translation memory.
+ */
+@Getter
+@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
+public class TransMemoryUnitSuggestionDetail implements SuggestionDetail {
+
+ private final SuggestionType type = SuggestionType.IMPORTED_TM;
+
+ /**
+ * The database id that can be used to look up the TransMemoryUnit.
+ */
+ private final Long transMemoryUnitId;
+
+ private final String transMemorySlug;
+ private final String transUnitId;
+
+ @JsonSerialize(using = JsonDateSerializer.class)
+ private final Date lastChanged;
+
+ /**
+ * Create a detail object based on a given trans memory unit.
+ *
+ * @param tmUnit for which to create a detail object
+ */
+ public TransMemoryUnitSuggestionDetail(TransMemoryUnit tmUnit) {
+ this.transMemoryUnitId = tmUnit.getId();
+ this.transMemorySlug = tmUnit.getTranslationMemory().getSlug();
+ this.transUnitId = tmUnit.getTransUnitId();
+ this.lastChanged = tmUnit.getLastChanged();
+ }
+}
diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/service/SuggestionsService.java b/zanata-war/src/main/java/org/zanata/rest/editor/service/SuggestionsService.java
new file mode 100644
index 0000000000..badf6c1da3
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/rest/editor/service/SuggestionsService.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.zanata.rest.editor.service;
+
+import com.google.common.base.Joiner;
+import com.googlecode.totallylazy.Either;
+import com.googlecode.totallylazy.Option;
+import org.jboss.seam.annotations.In;
+import org.jboss.seam.annotations.Name;
+import org.jboss.seam.annotations.Transactional;
+import org.zanata.common.LocaleId;
+import org.zanata.model.HLocale;
+import org.zanata.rest.editor.dto.suggestion.Suggestion;
+import org.zanata.rest.editor.service.resource.SuggestionsResource;
+import org.zanata.service.LocaleService;
+import org.zanata.service.TranslationMemoryService;
+import org.zanata.webtrans.shared.model.TransMemoryQuery;
+import org.zanata.webtrans.shared.rpc.HasSearchType;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.GenericEntity;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+import static org.zanata.webtrans.shared.rpc.HasSearchType.*;
+
+/**
+ * @see org.zanata.rest.editor.service.resource.SuggestionsResource
+ */
+@Name("editor.suggestionsService")
+@Path(SuggestionsResource.SERVICE_PATH)
+@Transactional
+public class SuggestionsService implements SuggestionsResource {
+
+ public static final String SEARCH_TYPES = Joiner.on(", ").join(SearchType.values());
+
+ @In("translationMemoryServiceImpl")
+ private TranslationMemoryService transMemoryService;
+
+ @In("localeServiceImpl")
+ private LocaleService localeService;
+
+ @Override
+ public Response query(List query, String sourceLocaleString, String transLocaleString, String searchTypeString) {
+
+ Option searchType = getSearchType(searchTypeString);
+ if (searchType.isEmpty()) {
+ return unknownSearchTypeResponse(searchTypeString);
+ }
+
+ Option sourceLocale = getLocale(sourceLocaleString);
+ if (sourceLocale.isEmpty()) {
+ return Response.status(BAD_REQUEST)
+ .entity(String.format("Unrecognized source locale: \"%s\"", sourceLocaleString))
+ .build();
+ }
+
+ Option transLocale = getLocale(transLocaleString);
+ if (transLocale.isEmpty()) {
+ return Response.status(BAD_REQUEST)
+ .entity(String.format("Unrecognized translation locale: \"%s\"", transLocaleString))
+ .build();
+ }
+
+ List suggestions = transMemoryService.searchTransMemoryWithDetails(transLocale.get(),
+ sourceLocale.get(), new TransMemoryQuery(query, searchType.get()));
+
+ // Wrap in generic entity to prevent type erasure, so that an
+ // appropriate MessageBodyReader can be used.
+ // see docs for GenericEntity
+ GenericEntity> entity = new GenericEntity>(suggestions) {};
+
+ return Response.ok(entity).build();
+ }
+
+ /**
+ * Try to get a valid locale for a given string.
+ *
+ * @param localeString used to look up the locale
+ * @return a wrapped LocaleId if the given string matches one, otherwise an empty option.
+ */
+ private Option getLocale(String localeString) {
+ @Nullable HLocale hLocale = localeService.getByLocaleId(localeString);
+ if (hLocale == null) {
+ return Option.none();
+ }
+ return Option.option(hLocale.getLocaleId());
+ }
+
+ /**
+ * Try to get a valid search type constant for a given string.
+ *
+ * @param searchTypeString used to look up the search type. Case insensitive.
+ * @return A wrapped SearchType if the given string matches one, otherwise an empty option.
+ */
+ private Option getSearchType(String searchTypeString) {
+ for (SearchType type : SearchType.values()) {
+ if (type.name().equalsIgnoreCase(searchTypeString)) {
+ return Option.option(type);
+ }
+ }
+ return Option.none();
+ }
+
+ /**
+ * Generate and build an error response that reports the search type being unrecognized.
+ *
+ * @param searchTypeString shown in the error message as the unrecognized string
+ * @return a built Response.
+ */
+ private Response unknownSearchTypeResponse(String searchTypeString) {
+ String error = String.format("Unrecognized search type: \"%s\". Expected one of: %s",
+ searchTypeString, SEARCH_TYPES);
+ return Response.status(BAD_REQUEST).entity(error).build();
+ }
+}
diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/service/resource/SuggestionsResource.java b/zanata-war/src/main/java/org/zanata/rest/editor/service/resource/SuggestionsResource.java
new file mode 100644
index 0000000000..8c7d02306e
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/rest/editor/service/resource/SuggestionsResource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.zanata.rest.editor.service.resource;
+
+import org.zanata.rest.editor.MediaTypes;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * Endpoint to search for suggestions from translation memory and other sources.
+ */
+@Produces({ MediaType.APPLICATION_JSON })
+@Consumes({ MediaType.APPLICATION_JSON })
+public interface SuggestionsResource {
+
+ public static final String SERVICE_PATH = "/suggestions";
+
+ /**
+ * Retrieves a list of suggestions for a a query in the body of the request.
+ *
+ * POST is used to allow the potentially long query strings to be sent in the
+ * body rather than the query string.
+ *
+ * @param query a JSON array of query strings in the body of the request,
+ * used to look up similar or identical strings based on
+ * sourceLocale, that have been translated to transLocale.
+ * @param sourceLocale locale id in the form lang[-country[-modifier]]
+ * @param transLocale locale id in the form lang[-country[-modifier]]
+ * @param searchType the search type to use, determines how similar source
+ * strings must be to be considered a match. Valid
+ * values are "EXACT", "FUZZY", "RAW", "FUZZY_PLURAL"
+ * and "CONTENT_HASH" as defined in
+ * {@link org.zanata.webtrans.shared.rpc.HasSearchType.SearchType}.
+ * @return The following response status codes will be returned from this
+ * operation:
+ * OK (200) - Response containing a list of suggestions.
+ * BAD REQUEST (400) - If searchType is not a valid search type, or if
+ * sourceLocale or transLocale are malformed or not available
+ * on the server.
+ * INTERNAL SERVER ERROR (500) - If there is an unexpected error in
+ * the server while performing this operation.
+ */
+ @POST
+ @Produces({ MediaTypes.APPLICATION_ZANATA_SUGGESTIONS_JSON, MediaType.APPLICATION_JSON })
+ public Response query(List query,
+ @QueryParam("from") String sourceLocale,
+ @QueryParam("to") String transLocale,
+ @QueryParam("searchType") @DefaultValue("FUZZY_PLURAL") String searchType);
+}
diff --git a/zanata-war/src/main/java/org/zanata/search/LevenshteinTokenUtil.java b/zanata-war/src/main/java/org/zanata/search/LevenshteinTokenUtil.java
index de2278f33d..c0dbc693c6 100644
--- a/zanata-war/src/main/java/org/zanata/search/LevenshteinTokenUtil.java
+++ b/zanata-war/src/main/java/org/zanata/search/LevenshteinTokenUtil.java
@@ -122,6 +122,7 @@ public static double getSimilarity(final String s1, final String s2) {
int levDistance = getLevenshteinDistanceInWords(s1s, s2s);
int maxDistance = Math.max(s1s.length, s2s.length);
+ // FIXME maxDistance can be 0, leading to divide-by-zero
double similarity = (maxDistance - levDistance) / (double) maxDistance;
return similarity;
}
@@ -129,8 +130,8 @@ public static double getSimilarity(final String s1, final String s2) {
/**
* Splits into tokens (lower-case).
*
- * @param s
- * @return
+ * @param s the string to tokenise
+ * @return an array of lowercase tokens (words)
*/
static String[] tokenise(String s) {
String[] tokens = s.toLowerCase().split(SPLIT_REGEX);
@@ -159,56 +160,86 @@ private static int countExtraStringLengths(List strings,
* strings. Returns the mean similarity of s1 against each string in the
* list.
*
- * @param s1
- * @param strings2
- * @return
+ * @param s1 string to compare against each other string
+ * @param strings2 other strings to compare s1 against
+ * @return mean similarity between s1 and each of strings2
*/
public static double getSimilarity(final String s1,
final List strings2) {
double totalSimilarity = 0.0;
- int stringCount = strings2.size();
- for (int i = 0; i < stringCount; i++) {
- String s2 = strings2.get(i);
+ for (String s2 : strings2) {
totalSimilarity += getSimilarity(s1, s2);
}
- double meanSimilarity = totalSimilarity / stringCount;
- return meanSimilarity;
+ return totalSimilarity / strings2.size();
}
+ /**
+ * Calculate the word-based case-insensitive similarity of two lists of
+ * strings (range 0.0 to 1.0).
+ *
+ * - Strings at the same index are compared.
+ * - Stop-words are ignored in comparisons. See #stopwords.
+ * - When both lists are empty, they are considered identical (returns 1.0)
+ * - Empty strings are considered identical to other empty strings.
+ *
+ * If a string is made up only of stop-words, the similarity will always be
+ * 0.0 regardless of the actual similarity of the stop-words.
+ *
+ * TODO review use of stop-words in these comparisons, since results can
+ * often be confusing to end-users.
+ *
+ * @param strings1 a list of strings to compare
+ * @param strings2 the other list of strings to compare
+ * @return average similarity between the strings, between 0.0 and 1.0
+ */
public static double getSimilarity(final List strings1,
final List strings2) {
- // length of the shorter list
- int minListSize;
-
- // count the extra strings first:
- int extraStringLengths; // total of "extra" strings in the longer list
- if (strings1.size() < strings2.size()) {
- minListSize = strings1.size();
- extraStringLengths = countExtraStringLengths(strings2, minListSize);
- } else {
- minListSize = strings2.size();
- extraStringLengths = countExtraStringLengths(strings1, minListSize);
+ // all empty lists are identical
+ if (strings1.isEmpty() && strings2.isEmpty()) {
+ return 1.0;
}
- // total of Levenshtein distance between corresponding strings in the
- // two lists, plus the length of any extra strings if one list is longer
- int totalLevDistance = extraStringLengths;
- // total of max editing distance between all the corresponding strings,
- // plus length of extra strings
- int totalMaxDistance = extraStringLengths;
+ // length of the shorter list
+ final int minListSize = Math.min(strings1.size(), strings2.size());
+ final List longestList = strings1.size() > minListSize ?
+ strings1 : strings2;
- // now count the strings which correspond between both lists
+ // total of "extra" strings in the longer list
+ final int extraStringLengths =
+ countExtraStringLengths(longestList, minListSize);
+
+ // running total of Levenshtein distance between corresponding strings
+ // in the two lists
+ int cumulativeLevDistance = 0;
+
+ // running total of max editing distance between all the corresponding
+ // strings.
+ int cumulativeMaxDistance = 0;
+
+ // count the strings which correspond between both lists
for (int i = 0; i < minListSize; i++) {
- String[] s1 = tokenise(strings1.get(i));
- String[] s2 = tokenise(strings2.get(i));
- int levenshteinDistance = getLevenshteinDistanceInWords(s1, s2);
- totalLevDistance += levenshteinDistance;
- totalMaxDistance += Math.max(s1.length, s2.length);
+ final String string1 = strings1.get(i);
+ final String string2 = strings2.get(i);
+ String[] tokens1 = tokenise(string1);
+ String[] tokens2 = tokenise(string2);
+ final int levenshteinDistance =
+ getLevenshteinDistanceInWords(tokens1, tokens2);
+ cumulativeLevDistance += levenshteinDistance;
+
+ // When a string contains only stop words, tokenise returns an empty
+ // array, so this value can remain at 0.
+ cumulativeMaxDistance += Math.max(tokens1.length, tokens2.length);
}
- double similarity =
- (totalMaxDistance - totalLevDistance)
- / (double) totalMaxDistance;
- return similarity;
+ final int totalLevDistance = cumulativeLevDistance + extraStringLengths;
+ final int totalMaxDistance = cumulativeMaxDistance + extraStringLengths;
+
+ // if there would be a divide-by-zero situation due to all strings being
+ // only stop-words, return 0 instead.
+ if (totalMaxDistance == 0) {
+ return 0.0;
+ }
+
+ return (totalMaxDistance - totalLevDistance) / (double) totalMaxDistance;
}
}
diff --git a/zanata-war/src/main/java/org/zanata/service/LocaleService.java b/zanata-war/src/main/java/org/zanata/service/LocaleService.java
index 2b58016a97..39cc2c3799 100644
--- a/zanata-war/src/main/java/org/zanata/service/LocaleService.java
+++ b/zanata-war/src/main/java/org/zanata/service/LocaleService.java
@@ -25,6 +25,7 @@
import java.util.Set;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.zanata.common.LocaleId;
import org.zanata.exception.ZanataServiceException;
@@ -52,6 +53,7 @@ public interface LocaleService {
HLocale getByLocaleId(@Nonnull LocaleId locale);
+ @Nullable
HLocale getByLocaleId(@Nonnull String localeId);
@Nonnull
diff --git a/zanata-war/src/main/java/org/zanata/service/TranslationMemoryService.java b/zanata-war/src/main/java/org/zanata/service/TranslationMemoryService.java
index 145b9e85b9..ac0da01ef4 100644
--- a/zanata-war/src/main/java/org/zanata/service/TranslationMemoryService.java
+++ b/zanata-war/src/main/java/org/zanata/service/TranslationMemoryService.java
@@ -1,25 +1,23 @@
/*
+ * Copyright 2014, Red Hat, Inc. and individual contributors as indicated by the
+ * @author tags. See the copyright.txt file in the distribution for a full
+ * listing of individual contributors.
*
- * * Copyright 2014, Red Hat, Inc. and individual contributors as indicated by the
- * * @author tags. See the copyright.txt file in the distribution for a full
- * * listing of individual contributors.
- * *
- * * This is free software; you can redistribute it and/or modify it under the
- * * terms of the GNU Lesser General Public License as published by the Free
- * * Software Foundation; either version 2.1 of the License, or (at your option)
- * * any later version.
- * *
- * * This software is distributed in the hope that it will be useful, but WITHOUT
- * * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
- * * details.
- * *
- * * You should have received a copy of the GNU Lesser General Public License
- * * along with this software; if not, write to the Free Software Foundation,
- * * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
- * * site: http://www.fsf.org.
+ * This is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this software; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
+ * site: http://www.fsf.org.
*/
-
package org.zanata.service;
import java.util.List;
@@ -28,6 +26,7 @@
import org.zanata.model.HLocale;
import org.zanata.model.HTextFlow;
import org.zanata.model.HTextFlowTarget;
+import org.zanata.rest.editor.dto.suggestion.Suggestion;
import org.zanata.webtrans.shared.model.TransMemoryDetails;
import org.zanata.webtrans.shared.model.TransMemoryQuery;
import org.zanata.webtrans.shared.model.TransMemoryResultItem;
@@ -54,4 +53,14 @@ Optional searchBestMatchTransMemory(
List searchTransMemory(LocaleId targetLocaleId,
LocaleId sourceLocaleId, TransMemoryQuery transMemoryQuery);
+
+ /**
+ * Run the given query to generate suggestions.
+ *
+ * @param transMemoryQuery the query type and text to search.
+ * @return a list of suggested translations for the query.
+ */
+ List searchTransMemoryWithDetails(
+ LocaleId targetLocaleId, LocaleId sourceLocaleId,
+ TransMemoryQuery transMemoryQuery);
}
diff --git a/zanata-war/src/main/java/org/zanata/service/impl/LocaleServiceImpl.java b/zanata-war/src/main/java/org/zanata/service/impl/LocaleServiceImpl.java
index e4a6137da8..baa3e82748 100644
--- a/zanata-war/src/main/java/org/zanata/service/impl/LocaleServiceImpl.java
+++ b/zanata-war/src/main/java/org/zanata/service/impl/LocaleServiceImpl.java
@@ -29,6 +29,7 @@
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.In;
@@ -263,8 +264,14 @@ public HLocale getByLocaleId(@Nonnull LocaleId locale) {
}
@Override
+ @Nullable
public HLocale getByLocaleId(@Nonnull String localeId) {
- return this.getByLocaleId(new LocaleId(localeId));
+ try {
+ return this.getByLocaleId(new LocaleId(localeId));
+ } catch (IllegalArgumentException e) {
+ log.warn("Tried to look up a locale with a malformed id", e);
+ return null;
+ }
}
@Override
diff --git a/zanata-war/src/main/java/org/zanata/service/impl/TranslationMemoryServiceImpl.java b/zanata-war/src/main/java/org/zanata/service/impl/TranslationMemoryServiceImpl.java
index fc15707632..550de23f04 100644
--- a/zanata-war/src/main/java/org/zanata/service/impl/TranslationMemoryServiceImpl.java
+++ b/zanata-war/src/main/java/org/zanata/service/impl/TranslationMemoryServiceImpl.java
@@ -21,14 +21,9 @@
package org.zanata.service.impl;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import lombok.Getter;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.Term;
@@ -60,6 +55,10 @@
import org.zanata.model.HTextFlow;
import org.zanata.model.HTextFlowTarget;
import org.zanata.model.tm.TransMemoryUnit;
+import org.zanata.rest.editor.dto.suggestion.Suggestion;
+import org.zanata.rest.editor.dto.suggestion.SuggestionDetail;
+import org.zanata.rest.editor.dto.suggestion.TextFlowSuggestionDetail;
+import org.zanata.rest.editor.dto.suggestion.TransMemoryUnitSuggestionDetail;
import org.zanata.search.LevenshteinTokenUtil;
import org.zanata.search.LevenshteinUtil;
import org.zanata.service.TranslationMemoryService;
@@ -76,8 +75,6 @@
import static com.google.common.collect.Collections2.filter;
import lombok.extern.slf4j.Slf4j;
-import javax.annotation.Nullable;
-
/**
* @author Alex Eng aeng@redhat.com
*/
@@ -219,6 +216,14 @@ public List searchTransMemory(
return results;
}
+ @Override
+ public List searchTransMemoryWithDetails(
+ LocaleId targetLocaleId, LocaleId sourceLocaleId,
+ TransMemoryQuery transMemoryQuery) {
+ return new QueryMatchProcessor(transMemoryQuery, sourceLocaleId, targetLocaleId)
+ .process();
+ }
+
private TransMemoryQuery buildTMQuery(HTextFlow textFlow,
HasSearchType.SearchType searchType, boolean checkContext,
boolean checkDocument, boolean checkProject,
@@ -305,9 +310,9 @@ private void processIndexMatch(TransMemoryQuery transMemoryQuery,
Lists.newArrayList(textFlowTarget.getContents());
TransMemoryResultItem.MatchType matchType =
fromContentState(textFlowTarget.getState());
- addOrIncrementResultItem(transMemoryQuery, matchesMap, match,
- matchType, textFlowContents, targetContents, textFlowTarget
- .getTextFlow().getId(), "");
+ TransMemoryResultItem item = createOrGetResultItem(transMemoryQuery, matchesMap, match, matchType,
+ textFlowContents, targetContents);
+ addTextFlowTargetToResultMatches(textFlowTarget, item);
} else if (entity instanceof TransMemoryUnit) {
TransMemoryUnit transUnit = (TransMemoryUnit) entity;
ArrayList sourceContents =
@@ -316,10 +321,9 @@ private void processIndexMatch(TransMemoryQuery transMemoryQuery,
ArrayList targetContents =
Lists.newArrayList(transUnit.getTransUnitVariants()
.get(targetLocaleId.getId()).getPlainTextSegment());
- addOrIncrementResultItem(transMemoryQuery, matchesMap, match,
- TransMemoryResultItem.MatchType.Imported, sourceContents,
- targetContents, transUnit.getId(), transUnit
- .getTranslationMemory().getSlug());
+ TransMemoryResultItem item = createOrGetResultItem(transMemoryQuery, matchesMap, match,
+ TransMemoryResultItem.MatchType.Imported, sourceContents, targetContents);
+ addTransMemoryUnitToResultMatches(item, transUnit);
}
}
@@ -370,11 +374,16 @@ private static TransMemoryResultItem.MatchType fromContentState(
}
}
- private void addOrIncrementResultItem(TransMemoryQuery transMemoryQuery,
- Map matchesMap, Object[] match,
- TransMemoryResultItem.MatchType matchType,
- ArrayList sourceContents, ArrayList targetContents,
- Long sourceId, String origin) {
+ /**
+ * Look up the result item for the given source and target contents.
+ *
+ * If no item is found, a new one is added to the map and returned.
+ *
+ * @return the item for the given source and target contents, which may be newly created.
+ */
+ private TransMemoryResultItem createOrGetResultItem(TransMemoryQuery transMemoryQuery, Map matchesMap, Object[] match, TransMemoryResultItem.MatchType matchType,
+ ArrayList sourceContents, ArrayList targetContents) {
TMKey key = new TMKey(sourceContents, targetContents);
TransMemoryResultItem item = matchesMap.get(key);
if (item == null) {
@@ -387,9 +396,29 @@ private void addOrIncrementResultItem(TransMemoryQuery transMemoryQuery,
matchType, score, percent);
matchesMap.put(key, item);
}
+ return item;
+ }
+
+ private void addTransMemoryUnitToResultMatches(TransMemoryResultItem item, TransMemoryUnit transMemoryUnit) {
+ item.incMatchCount();
+ item.addOrigin(transMemoryUnit.getTranslationMemory().getSlug());
+ }
+
+ private void addTextFlowTargetToResultMatches(HTextFlowTarget textFlowTarget, TransMemoryResultItem item) {
item.incMatchCount();
- item.addOrigin(origin);
- item.addSourceId(sourceId);
+
+ // TODO change sourceId to include type, then include the id of imported matches
+ item.addSourceId(textFlowTarget.getTextFlow().getId());
+
+ // Workaround: since Imported does not have a details view in the current editor,
+ // I am treating it as the lowest priority, so will be overwritten by
+ // other match types.
+ // A better fix is to have the DTO hold all the match types so the editor
+ // can show them in whatever way is most sensible.
+ ContentState state = textFlowTarget.getState();
+ if (state == ContentState.Approved || item.getMatchType() == TransMemoryResultItem.MatchType.Imported) {
+ item.setMatchType(fromContentState(state));
+ }
}
/**
@@ -782,4 +811,205 @@ public boolean apply(Object[] input) {
return true;
}
}
+
+ /**
+ * Responsible for running a query and collating the results.
+ *
+ * I am using a class to avoid having to pass several arguments through
+ * all the helper methods, since that makes the code very hard to read.
+ */
+ private class QueryMatchProcessor {
+ public static final boolean SORT_BY_DATE = false;
+
+ private final TransMemoryQuery query;
+ private final LocaleId srcLocale;
+ private final LocaleId transLocale;
+ private final Map suggestions;
+ private boolean processed;
+
+ public QueryMatchProcessor(TransMemoryQuery query, LocaleId srcLocale, LocaleId transLocale) {
+ this.query = query;
+ this.srcLocale = srcLocale;
+ this.transLocale = transLocale;
+ suggestions = new HashMap<>();
+ processed = false;
+ }
+
+ /**
+ * Run the query, process and collate the results.
+ *
+ * Results are cached, so subsequent calls will return cached results
+ * without running the query again.
+ *
+ * @return the collated results of the query.
+ */
+ public List process() {
+ if (!processed) {
+ runQueryAndCacheSuggestions();
+ }
+ return new ArrayList<>(suggestions.values());
+ }
+
+ /**
+ * When this has run, suggestions contains all the results of the query.
+ */
+ private void runQueryAndCacheSuggestions() {
+ for (Object[] resultRow : runQuery()) {
+ processResultRow(resultRow);
+ }
+ processed = true;
+ }
+
+ /**
+ * Convert a result row to a match (if possible) then process the match.
+ *
+ * If the row does not contain an appropriate entity, an error is logged
+ * and the row is skipped.
+ *
+ * @param resultRow in the form [Float score, Object entity]
+ */
+ private void processResultRow(Object[] resultRow) {
+ try {
+ final QueryMatch match = fromResultRow(resultRow);
+ processMatch(match);
+ } catch (IllegalArgumentException e) {
+ log.error(
+ "Skipped result row because it does not contain " +
+ "an expected entity type: {}", resultRow, e);
+ }
+ }
+
+ /**
+ * Run the full-text query.
+ * @return collection of [float, entity] where float is the match score
+ * and entity is a HTextFlowTarget or TransMemoryUnit.
+ */
+ private Collection